Menu
popagent
publicLatest change ac1b32888da9db0a129d0e91ff5cb4dac22c12f0 - Add workspace documentation RAG by AkurAI Build
import { handleChatStream } from "@mastra/ai-sdk";
import {
createUIMessageStream,
createUIMessageStreamResponse,
type UIMessage,
type UIMessageStreamWriter,
} from "ai";
import { z } from "zod";
import index from "./ui/index.html";
import {
AGENT_TOOL_NAMES,
DEFAULT_WORKSPACE_ID,
MAX_AGENT_INSTRUCTIONS_CHARACTERS,
type AgentActivity,
type AgentRuntimeSettingsInput,
type BrowserSettingsInput,
type AgentSettingsInput,
type AgentSkillInput,
type ChatMessage,
type MemorySettingsInput,
} from "./api-types";
import { agentActivity, AgentExecutionRuntime, createAgentTaskExecutor, createTraceId } from "./agent-execution";
import { agentWorkspaces, type AgentWorkspaceStore } from "./agent-workspaces";
import { agent, mastra } from "./agent";
import { agentRuntimeSettings, type AgentRuntimeSettingsStore } from "./agent-runtime-settings";
import { agentSettings, type AgentSettingsStore } from "./agent-settings";
import { agentSkills, type AgentSkillStore } from "./agent-skills";
import { browserRuntime, browserSettings, type BrowserRuntime, type BrowserSettingsStore } from "./browser-settings";
import { latestUserText, replaceLatestUserText } from "./chat-messages";
import { createChannelRoutes } from "./channel-routes";
import { channels } from "./channels";
import { hookAudit } from "./hook-audit";
import { createTaskLifecycleHooks } from "./hook-lifecycle";
import { createHookEvent, HookBlockedError, loadHookRuntime, type HookRuntime } from "./hooks";
import { longTermMemory, MAX_FACT_CHARACTERS, type LongTermMemoryStore } from "./long-term-memory";
import { memorySettings, type MemorySettingsStore } from "./memory-settings";
import { isKnownModel, listModelCatalog } from "./models";
import {
getObservabilityOverview,
getObservabilityTrace,
listObservabilityLogs,
listObservabilityTraces,
observabilityHealth,
parseObservabilityQuery,
} from "./observability-store";
import { RESOURCE_ID, SessionStore } from "./sessions";
import { AgentTaskRuntime, AgentTaskStore } from "./tasks";
import { requireApiKey } from "./auth";
import { startMaintenance } from "./maintenance";
import { appLogger, observability } from "./observability";
import { storage } from "./storage";
import { documentationIndex, type DocumentationIndex } from "./documentation-index";
import { DocumentationConflictError, MAX_DOCUMENTATION_CHARACTERS } from "./documentation-files";
const observabilityFeedbackSchema = z.object({
traceId: z.string().regex(/^[a-f0-9]{32}$/),
value: z.union([z.literal(1), z.literal(-1)]),
comment: z.string().trim().max(2_000).optional(),
}).strict();
const skillInputSchema: z.ZodType<AgentSkillInput> = z.object({
name: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).max(64),
description: z.string().min(1).max(1024),
instructions: z.string().min(1),
references: z.record(z.string(), z.string()),
enabled: z.boolean(),
userInvocable: z.boolean(),
});
const browserHostSchema = z.string().trim().min(1).max(255).regex(/^(?:\*\.)?[a-z0-9.-]+(?::\d{1,5})?$/i);
const browserSettingsSchema: z.ZodType<BrowserSettingsInput> = z.object({
enabled: z.boolean(),
scope: z.enum(["thread", "shared"]),
viewportWidth: z.number().int().min(320).max(3840),
viewportHeight: z.number().int().min(240).max(2160),
timeoutMs: z.number().int().min(1_000).max(120_000),
maxSessions: z.number().int().min(1).max(16),
idleTimeoutMs: z.number().int().min(60_000).max(86_400_000),
screencastEnabled: z.boolean(),
screenshotsEnabled: z.boolean(),
multiTabEnabled: z.boolean(),
formsEnabled: z.boolean(),
dialogsEnabled: z.boolean(),
dragEnabled: z.boolean(),
evaluateEnabled: z.boolean(),
recordingEnabled: z.boolean(),
recordingRetentionDays: z.number().int().min(1).max(90),
recordingMaxFiles: z.number().int().min(1).max(100),
allowHosts: z.array(browserHostSchema).max(100),
denyHosts: z.array(browserHostSchema).max(100),
}).strict();
const memorySettingsSchema: z.ZodType<MemorySettingsInput> = z.object({
autoCompact: z.boolean(),
observationTokens: z.number().int().min(4_000).max(200_000),
reflectionTokens: z.number().int().min(4_000).max(400_000),
recentMessagePercent: z.number().int().min(5).max(75),
bufferOnIdle: z.boolean(),
});
const agentSettingsInputSchema: z.ZodType<AgentSettingsInput> = z.object({
name: z.string().trim().min(1).max(120),
description: z.string().trim().min(1).max(1024),
instructions: z.string().min(1).max(MAX_AGENT_INSTRUCTIONS_CHARACTERS),
workspaceAccess: z.enum(["read-write", "read-only", "none"]),
browserAccess: z.enum(["interactive", "read-only", "none"]),
delegationEnabled: z.boolean(),
tools: z.array(z.enum(AGENT_TOOL_NAMES)).max(AGENT_TOOL_NAMES.length)
.refine((tools) => new Set(tools).size === tools.length, "Tools must be unique"),
}).strict();
const agentRuntimeSettingsSchema: z.ZodType<AgentRuntimeSettingsInput> = z.object({
defaultModel: z.string().trim().min(1).max(200),
supervisorMaxSteps: z.number().int().min(1).max(128),
specialistMaxSteps: z.number().int().min(1).max(128),
toolConcurrency: z.number().int().min(1).max(16),
delegationContextMessages: z.number().int().min(1).max(100),
delegationResultCharacters: z.number().int().min(1_000).max(100_000),
maxProcessorRetries: z.number().int().min(0).max(10),
finalResponseFeedback: z.string().trim().min(1).max(4_000),
delegationFailureFeedback: z.string().trim().min(1).max(4_000),
delegationResultTruncationMarker: z.string().min(1).max(500),
taskConcurrency: z.number().int().min(1).max(16),
taskPollIntervalMs: z.number().int().min(250).max(300_000),
taskTimeoutMs: z.number().int().min(1_000).max(86_400_000),
taskStaleAfterMs: z.number().int().min(60_000).max(604_800_000),
}).strict();
const memoryKeySchema = z.string().trim().min(1).max(128);
const memoryContentSchema = z.string().trim().min(1).max(MAX_FACT_CHARACTERS);
const memoryCreateSchema = z.object({
content: memoryContentSchema,
key: memoryKeySchema.optional(),
importance: z.number().min(0).max(1).default(0.8),
sessionId: z.string().trim().min(1).max(256).default("memory-manager"),
}).strict();
const memoryUpdateSchema = z.object({
content: memoryContentSchema,
key: memoryKeySchema.optional(),
importance: z.number().min(0).max(1),
}).strict();
const workspaceInputSchema = z.object({
name: z.string().trim().min(1).max(120),
repositoryPath: z.string().trim().min(1).max(512),
}).strict();
const documentationSaveSchema = z.object({
path: z.string().trim().min(1).max(512),
content: z.string().max(MAX_DOCUMENTATION_CHARACTERS),
revision: z.string().regex(/^[a-f0-9]{64}$/).optional(),
}).strict();
const documentationDeleteSchema = z.object({
path: z.string().trim().min(1).max(512),
revision: z.string().regex(/^[a-f0-9]{64}$/),
}).strict();
const documentationMoveSchema = documentationDeleteSchema.extend({
nextPath: z.string().trim().min(1).max(512),
}).strict();
const sessionPostSchema = z.object({
model: z.string().min(1).max(200).optional(),
workspaceId: z.string().min(1).max(256).default(DEFAULT_WORKSPACE_ID),
}).strict();
const messageSchema = z.object({
id: z.string().optional(),
role: z.enum(["user", "assistant", "system"]),
parts: z.array(z.unknown()),
}).passthrough();
const sessionPutSchema = z.object({
model: z.string().min(1).max(200),
messages: z.array(messageSchema).max(2000),
revision: z.number().int().min(0),
});
const chatPostSchema = z.object({
id: z.string().min(1).max(256).optional(),
model: z.string().min(1).max(200).optional(),
workspaceId: z.string().min(1).max(256).optional(),
messages: z.array(messageSchema).min(1).max(2000),
});
const taskPostSchema = z.object({
prompt: z.string().trim().min(1).max(8000),
model: z.string().min(1).max(200).optional(),
sessionId: z.string().min(1).max(256).optional(),
workspaceId: z.string().min(1).max(256).default(DEFAULT_WORKSPACE_ID),
});
const scheduleSchema = z.object({
name: z.string().trim().min(1).max(120),
prompt: z.string().trim().min(1).max(8000),
model: z.string().min(1).max(200).optional(),
sessionId: z.string().min(1).max(256).optional(),
workspaceId: z.string().min(1).max(256).default(DEFAULT_WORKSPACE_ID),
cron: z.string().trim().min(1).max(120),
enabled: z.boolean().optional(),
});
const MAX_BODY_BYTES = 2 * 1024 * 1024;
async function readJson(req: Request): Promise<unknown> {
const text = await req.text();
if (text.length > MAX_BODY_BYTES) throw new RangeError("body too large");
return JSON.parse(text);
}
function guardApiRoutes<T extends Record<string, unknown>>(routes: T): T {
const guarded = { ...routes } as T;
for (const [path, route] of Object.entries(routes)) {
if (!path.startsWith("/api/")) continue;
if (typeof route === "function") {
(guarded as Record<string, unknown>)[path] = async (req: Request) => requireApiKey(req) ?? route(req);
} else if (route && typeof route === "object") {
const methods = { ...(route as Record<string, unknown>) };
for (const [method, handler] of Object.entries(methods)) {
if (typeof handler === "function") methods[method] = async (req: Request) => requireApiKey(req) ?? handler(req);
}
(guarded as Record<string, unknown>)[path] = methods;
}
}
return guarded;
}
export const sessions = new SessionStore();
const hooks = loadHookRuntime((run) => hookAudit.record(run));
const taskStore = new AgentTaskStore();
const agentExecution = new AgentExecutionRuntime(
agent,
hooks,
longTermMemory,
browserRuntime,
memorySettings,
agentRuntimeSettings,
);
const taskRuntime = new AgentTaskRuntime(
taskStore,
createAgentTaskExecutor(agent, agentExecution),
createTaskLifecycleHooks(hooks),
);
type AgentActivityMessage = UIMessage<unknown, { agentActivity: AgentActivity }>;
export function startServer(
port = 3000,
sessionStore = sessions,
hookRuntime: HookRuntime = hooks,
memoryStore: LongTermMemoryStore = longTermMemory,
agentStore: AgentSettingsStore = agentSettings,
browserStore: BrowserSettingsStore = browserSettings,
activeBrowser: BrowserRuntime = browserRuntime,
skillStore: AgentSkillStore = agentSkills,
tasks: AgentTaskStore = taskStore,
taskRunner: AgentTaskRuntime = taskRuntime,
memoryConfigStore: MemorySettingsStore = memorySettings,
workspaceStore: AgentWorkspaceStore = agentWorkspaces,
runtimeConfigStore: Pick<AgentRuntimeSettingsStore, "get" | "update"> = agentRuntimeSettings,
documentation: DocumentationIndex = documentationIndex,
) {
const startedSessions = new Set<string>();
const executionRuntime = new AgentExecutionRuntime(
agent,
hookRuntime,
memoryStore,
activeBrowser,
memoryConfigStore,
runtimeConfigStore,
);
type ApiRequest = Request & { params: { id: string; skillId: string } };
type ApiHandler = (req: ApiRequest) => Response | Promise<Response>;
type RouteDef = typeof index | ApiHandler | Partial<Record<"GET" | "POST" | "PUT" | "PATCH" | "DELETE", ApiHandler>>;
const routes: Record<string, RouteDef> = {
"/": index,
...createChannelRoutes({
channelStore: channels,
workspaceStore,
runtimeSettings: runtimeConfigStore,
taskStore: tasks,
taskRunner,
}),
"/api/models": async () => {
const runtimeSettings = await runtimeConfigStore.get();
return Response.json({ ...(await listModelCatalog()), defaultModel: runtimeSettings.defaultModel });
},
"/api/workspaces": {
GET: async () => Response.json({ workspaces: await workspaceStore.list() }),
POST: async (req) => {
const input = workspaceInputSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) {
return Response.json({ error: "Invalid workspace", issues: input.error.issues }, { status: 400 });
}
try {
return Response.json(await workspaceStore.create(input.data), { status: 201 });
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
},
"/api/docs/search": async (req) => {
const url = new URL(req.url);
const query = url.searchParams.get("q")?.trim() ?? "";
const workspaceId = url.searchParams.get("workspaceId")?.trim() || undefined;
if (!query || query.length > 2_000) return Response.json({ error: "Invalid documentation query" }, { status: 400 });
if (workspaceId && !(await workspaceStore.get(workspaceId))) {
return Response.json({ error: "Workspace not found" }, { status: 404 });
}
return Response.json({ results: await documentation.search(query, { workspaceId }) });
},
"/api/workspaces/:id/docs": {
GET: async (req) => {
if (!(await workspaceStore.get(req.params.id))) return Response.json({ error: "Workspace not found" }, { status: 404 });
return Response.json({ pages: await documentation.files.list(req.params.id) });
},
POST: async (req) => {
if (!(await workspaceStore.get(req.params.id))) return Response.json({ error: "Workspace not found" }, { status: 404 });
const input = documentationSaveSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) return Response.json({ error: "Invalid documentation page", issues: input.error.issues }, { status: 400 });
try {
const page = await documentation.files.save(req.params.id, input.data);
let indexed = true;
await documentation.indexPage(req.params.id, page).catch((error) => {
indexed = false;
appLogger().warn("documentation.index.page.failed", { workspaceId: req.params.id, path: page.path, error });
});
return Response.json(page, {
status: input.data.revision ? 200 : 201,
headers: { "x-popagent-indexed": String(indexed) },
});
} catch (error) {
if (error instanceof DocumentationConflictError) {
return Response.json({ error: "revision conflict", page: error.current }, { status: 409 });
}
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
PATCH: async (req) => {
if (!(await workspaceStore.get(req.params.id))) return Response.json({ error: "Workspace not found" }, { status: 404 });
const input = documentationMoveSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) return Response.json({ error: "Invalid documentation move", issues: input.error.issues }, { status: 400 });
try {
const page = await documentation.files.move(
req.params.id,
input.data.path,
input.data.nextPath,
input.data.revision,
);
await documentation.removePage(req.params.id, input.data.path).catch((error) => {
appLogger().warn("documentation.index.move-delete.failed", { workspaceId: req.params.id, path: input.data.path, error });
});
let indexed = true;
await documentation.indexPage(req.params.id, page).catch((error) => {
indexed = false;
appLogger().warn("documentation.index.move.failed", { workspaceId: req.params.id, path: page.path, error });
});
return Response.json(page, { headers: { "x-popagent-indexed": String(indexed) } });
} catch (error) {
if (error instanceof DocumentationConflictError) {
return Response.json({ error: "revision conflict", page: error.current }, { status: 409 });
}
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
DELETE: async (req) => {
if (!(await workspaceStore.get(req.params.id))) return Response.json({ error: "Workspace not found" }, { status: 404 });
const input = documentationDeleteSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) return Response.json({ error: "Invalid documentation page", issues: input.error.issues }, { status: 400 });
try {
await documentation.files.remove(req.params.id, input.data.path, input.data.revision);
await documentation.removePage(req.params.id, input.data.path).catch((error) => {
appLogger().warn("documentation.index.delete.failed", { workspaceId: req.params.id, path: input.data.path, error });
});
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof DocumentationConflictError) {
return Response.json({ error: "revision conflict", page: error.current }, { status: 409 });
}
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
},
"/api/workspaces/:id/docs/content": async (req) => {
if (!(await workspaceStore.get(req.params.id))) return Response.json({ error: "Workspace not found" }, { status: 404 });
const path = new URL(req.url).searchParams.get("path");
if (!path) return Response.json({ error: "Documentation path is required" }, { status: 400 });
try {
return Response.json(await documentation.files.read(req.params.id, path));
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 404 });
}
},
"/api/workspaces/:id/docs/reindex": {
POST: async (req) => {
if (!(await workspaceStore.get(req.params.id))) return Response.json({ error: "Workspace not found" }, { status: 404 });
try {
return Response.json(await documentation.indexWorkspace(req.params.id));
} catch (error) {
appLogger().warn("documentation.index.workspace.failed", { workspaceId: req.params.id, error });
return Response.json({ error: "Unable to index documentation" }, { status: 503 });
}
},
},
"/api/workspaces/:id/docs/index-status": async (req) => {
if (!(await workspaceStore.get(req.params.id))) return Response.json({ error: "Workspace not found" }, { status: 404 });
return Response.json(await documentation.status(req.params.id));
},
"/api/workspaces/:id": {
PATCH: async (req) => {
const input = workspaceInputSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) {
return Response.json({ error: "Invalid workspace", issues: input.error.issues }, { status: 400 });
}
try {
const workspace = await workspaceStore.update(req.params.id, input.data);
return workspace
? Response.json(workspace)
: Response.json({ error: "Workspace not found" }, { status: 404 });
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
DELETE: async (req) => {
if (req.params.id === DEFAULT_WORKSPACE_ID) {
return Response.json({ error: "The default workspace cannot be deleted" }, { status: 409 });
}
if (!(await workspaceStore.get(req.params.id))) {
return Response.json({ error: "Workspace not found" }, { status: 404 });
}
const [active, archived, workspaceTasks, schedules] = await Promise.all([
sessionStore.list(false, req.params.id),
sessionStore.list(true, req.params.id),
tasks.listTasks(req.params.id),
tasks.listSchedules(req.params.id),
]);
if (active.length || archived.length || workspaceTasks.length || schedules.length) {
return Response.json({ error: "Workspace still has sessions, tasks, or schedules" }, { status: 409 });
}
await documentation.removeWorkspace(req.params.id);
await workspaceStore.delete(req.params.id);
return new Response(null, { status: 204 });
},
},
"/api/agents": {
GET: async () => Response.json({ agents: await agentStore.list() }),
},
"/api/agents/:id": {
PATCH: async (req) => {
const input = agentSettingsInputSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) {
return Response.json({ error: "Invalid agent settings", issues: input.error.issues }, { status: 400 });
}
try {
const settings = await agentStore.update(req.params.id, input.data);
return settings
? Response.json(settings)
: Response.json({ error: "Agent not found" }, { status: 404 });
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
},
"/api/settings/browser": {
GET: async () => Response.json(await browserStore.get()),
PATCH: async (req) => {
const input = browserSettingsSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) {
return Response.json({ error: "Invalid browser settings", issues: input.error.issues }, { status: 400 });
}
const settings = await browserStore.update(input.data);
await activeBrowser.apply(agent, settings);
return Response.json(settings);
},
},
"/api/settings/memory": {
GET: async () => Response.json(await memoryConfigStore.get()),
PATCH: async (req) => {
const input = memorySettingsSchema.safeParse(await req.json().catch(() => ({})));
if (!input.success) {
return Response.json({ error: "Invalid memory settings", issues: input.error.issues }, { status: 400 });
}
return Response.json(await memoryConfigStore.update(input.data));
},
},
"/api/settings/agent-runtime": {
GET: async () => Response.json(await runtimeConfigStore.get()),
PATCH: async (req) => {
const input = agentRuntimeSettingsSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) {
return Response.json({ error: "Invalid agent runtime settings", issues: input.error.issues }, { status: 400 });
}
if (!(await isKnownModel(input.data.defaultModel))) {
return Response.json({ error: `Unknown model: ${input.data.defaultModel}` }, { status: 400 });
}
try {
const settings = await runtimeConfigStore.update(input.data);
taskRunner.configure(settings);
return Response.json(settings);
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
},
"/api/agents/:id/skills": {
GET: async (req) => Response.json({ skills: await skillStore.list(req.params.id) }),
POST: async (req) => {
const input = skillInputSchema.safeParse(await req.json().catch(() => ({})));
if (!input.success) return Response.json({ error: "Invalid skill", issues: input.error.issues }, { status: 400 });
return Response.json(await skillStore.create(req.params.id, input.data), { status: 201 });
},
},
"/api/agents/:id/skills/:skillId": {
PATCH: async (req) => {
const input = skillInputSchema.safeParse(await req.json().catch(() => ({})));
if (!input.success) return Response.json({ error: "Invalid skill", issues: input.error.issues }, { status: 400 });
const skill = await skillStore.update(req.params.id, req.params.skillId, input.data);
return skill ? Response.json(skill) : Response.json({ error: "Skill not found" }, { status: 404 });
},
DELETE: async (req) => await skillStore.delete(req.params.id, req.params.skillId)
? new Response(null, { status: 204 })
: Response.json({ error: "Skill not found" }, { status: 404 }),
},
"/api/sessions": {
GET: async (req) => {
const params = new URL(req.url).searchParams;
const archived = params.get("archived") === "true";
const workspaceId = params.get("workspaceId") || undefined;
return Response.json({ sessions: await sessionStore.list(archived, workspaceId) });
},
POST: async (req) => {
const input = sessionPostSchema.safeParse(await readJson(req).catch(() => ({})));
if (!input.success) {
return Response.json({ error: "Invalid session", issues: input.error.issues }, { status: 400 });
}
const model = input.data.model ?? (await runtimeConfigStore.get()).defaultModel;
if (!(await isKnownModel(model))) {
return Response.json({ error: `Unknown model: ${model}` }, { status: 400 });
}
if (!(await workspaceStore.get(input.data.workspaceId))) {
return Response.json({ error: "Workspace not found" }, { status: 404 });
}
const session = await sessionStore.create(model, input.data.workspaceId);
try {
await hookRuntime.dispatch(createHookEvent("SessionStart", {
sessionId: session.id,
model,
detail: {
source: "create",
session: { title: session.title, model, workspaceId: session.workspaceId },
},
}));
startedSessions.add(session.id);
} catch (error) {
appLogger().warn("hook.session_start.failed", { error: messageOf(error) });
}
return Response.json(session, { status: 201 });
},
},
"/api/sessions/:id": {
GET: async (req) => {
const session = await sessionStore.get(req.params.id);
return session
? Response.json(session)
: Response.json({ error: "Session not found" }, { status: 404 });
},
PUT: async (req) => {
const input = sessionPutSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) {
return Response.json({ error: "Invalid session", issues: input.error.issues }, { status: 400 });
}
if (!(await isKnownModel(input.data.model))) {
return Response.json({ error: `Unknown model: ${input.data.model}` }, { status: 400 });
}
const session = await sessionStore.save(req.params.id, {
model: input.data.model,
messages: input.data.messages as ChatMessage[],
expectedRevision: input.data.revision,
});
if (session === "conflict") {
const current = await sessionStore.get(req.params.id);
return Response.json({ error: "revision conflict", session: current }, { status: 409 });
}
return session
? Response.json(session)
: Response.json({ error: "Session not found" }, { status: 404 });
},
PATCH: async (req) => {
const body = (await req.json().catch(() => ({}))) as {
title?: string;
archived?: boolean;
};
if (typeof body.title === "string") {
const title = body.title.trim();
if (!title) return Response.json({ error: "title is required" }, { status: 400 });
const session = await sessionStore.rename(req.params.id, title);
return session
? Response.json(session)
: Response.json({ error: "Session not found" }, { status: 404 });
}
if (typeof body.archived === "boolean") {
const session = await sessionStore.setArchived(req.params.id, body.archived);
return session
? Response.json(session)
: Response.json({ error: "Session not found" }, { status: 404 });
}
return Response.json({ error: "title or archived is required" }, { status: 400 });
},
DELETE: async (req) => {
const session = await sessionStore.get(req.params.id);
if (!session) return Response.json({ error: "Session not found" }, { status: 404 });
try {
await hookRuntime.dispatch(createHookEvent("SessionEnd", {
sessionId: session.id,
model: session.model,
detail: { source: "delete" },
}));
} catch (error) {
appLogger().warn("hook.session_end.failed", { error: messageOf(error) });
}
await hookAudit.deleteForSession(session.id);
await sessionStore.delete(session.id);
startedSessions.delete(session.id);
return new Response(null, { status: 204 });
},
},
"/api/sessions/:id/hooks": {
GET: async (req) => Response.json({ runs: await hookAudit.listForSession(req.params.id) }),
},
"/api/memories": {
GET: async (req) => {
const params = new URL(req.url).searchParams;
const kind = params.get("kind");
if (kind !== null && kind !== "fact" && kind !== "episode") {
return Response.json({ error: "kind must be fact or episode" }, { status: 400 });
}
return Response.json({
memories: await memoryStore.list({
resourceId: RESOURCE_ID,
query: params.get("query") ?? undefined,
kind: kind ?? undefined,
}),
});
},
POST: async (req) => {
const input = memoryCreateSchema.safeParse(await req.json().catch(() => ({})));
if (!input.success) {
return Response.json({ error: "Invalid memory", issues: input.error.issues }, { status: 400 });
}
await memoryStore.rememberFact({
resourceId: RESOURCE_ID,
sessionId: input.data.sessionId,
key: input.data.key,
content: input.data.content,
importance: input.data.importance,
});
const memories = await memoryStore.list({
resourceId: RESOURCE_ID,
query: input.data.content,
kind: "fact",
limit: 1,
});
return Response.json(memories[0], { status: 201 });
},
},
"/api/memories/:id": {
PATCH: async (req) => {
const input = memoryUpdateSchema.safeParse(await req.json().catch(() => ({})));
if (!input.success) {
return Response.json({ error: "Invalid memory", issues: input.error.issues }, { status: 400 });
}
const memory = await memoryStore.updateFact({
id: req.params.id,
resourceId: RESOURCE_ID,
key: input.data.key,
content: input.data.content,
importance: input.data.importance,
});
return memory
? Response.json(memory)
: Response.json({ error: "Editable fact not found" }, { status: 404 });
},
DELETE: async (req) => {
const deleted = await memoryStore.delete(req.params.id, RESOURCE_ID);
return deleted
? new Response(null, { status: 204 })
: Response.json({ error: "Memory not found" }, { status: 404 });
},
},
"/api/tasks": {
GET: async (req) => {
const workspaceId = new URL(req.url).searchParams.get("workspaceId") || undefined;
return Response.json({ tasks: await tasks.listTasks(workspaceId) });
},
POST: async (req) => {
const input = taskPostSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) return Response.json({ error: "Invalid task", issues: input.error.issues }, { status: 400 });
const model = input.data.model ?? (await runtimeConfigStore.get()).defaultModel;
if (!(await isKnownModel(model))) return Response.json({ error: `Unknown model: ${model}` }, { status: 400 });
if (!(await workspaceStore.get(input.data.workspaceId))) return Response.json({ error: "Workspace not found" }, { status: 404 });
const task = await tasks.createTask({
sessionId: input.data.sessionId,
workspaceId: input.data.workspaceId,
prompt: input.data.prompt,
model,
});
taskRunner.enqueue(task);
return Response.json(task, { status: 201 });
},
},
"/api/tasks/:id/cancel": {
POST: async (req) => await taskRunner.cancel(req.params.id)
? new Response(null, { status: 204 })
: Response.json({ error: "Task cannot be cancelled" }, { status: 409 }),
},
"/api/schedules": {
GET: async (req) => {
const workspaceId = new URL(req.url).searchParams.get("workspaceId") || undefined;
return Response.json({ schedules: await tasks.listSchedules(workspaceId) });
},
POST: async (req) => {
const input = scheduleSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) return Response.json({ error: "Invalid schedule", issues: input.error.issues }, { status: 400 });
const model = input.data.model ?? (await runtimeConfigStore.get()).defaultModel;
if (!(await isKnownModel(model))) return Response.json({ error: `Unknown model: ${model}` }, { status: 400 });
if (!(await workspaceStore.get(input.data.workspaceId))) return Response.json({ error: "Workspace not found" }, { status: 404 });
try {
return Response.json(await tasks.createSchedule({
sessionId: input.data.sessionId,
workspaceId: input.data.workspaceId,
name: input.data.name,
prompt: input.data.prompt,
model,
cron: input.data.cron,
enabled: input.data.enabled ?? true,
}), { status: 201 });
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
},
"/api/schedules/:id": {
PATCH: async (req) => {
const input = scheduleSchema.extend({ enabled: z.boolean() }).safeParse(await readJson(req).catch(() => undefined));
if (!input.success) return Response.json({ error: "Invalid schedule", issues: input.error.issues }, { status: 400 });
const model = input.data.model ?? (await runtimeConfigStore.get()).defaultModel;
if (!(await isKnownModel(model))) return Response.json({ error: `Unknown model: ${model}` }, { status: 400 });
if (!(await workspaceStore.get(input.data.workspaceId))) return Response.json({ error: "Workspace not found" }, { status: 404 });
try {
const schedule = await tasks.updateSchedule(req.params.id, {
workspaceId: input.data.workspaceId,
name: input.data.name,
prompt: input.data.prompt,
model,
cron: input.data.cron,
enabled: input.data.enabled,
});
return schedule ? Response.json(schedule) : Response.json({ error: "Schedule not found" }, { status: 404 });
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
DELETE: async (req) => await tasks.deleteSchedule(req.params.id)
? new Response(null, { status: 204 })
: Response.json({ error: "Schedule not found" }, { status: 404 }),
},
"/api/observability/overview": async (req) => {
try {
return Response.json(await getObservabilityOverview(parseObservabilityQuery(req.url)));
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
"/api/observability/traces": async (req) => {
try {
return Response.json(await listObservabilityTraces(parseObservabilityQuery(req.url)));
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
"/api/observability/traces/:id": async (req) => {
if (!/^[a-f0-9]{32}$/.test(req.params.id)) {
return Response.json({ error: "Invalid trace ID" }, { status: 400 });
}
const trace = await getObservabilityTrace(req.params.id);
return trace ? Response.json(trace) : Response.json({ error: "Trace not found" }, { status: 404 });
},
"/api/observability/logs": async (req) => {
try {
return Response.json(await listObservabilityLogs(parseObservabilityQuery(req.url)));
} catch (error) {
return Response.json({ error: messageOf(error) }, { status: 400 });
}
},
"/api/observability/health": async () => {
const health = await observabilityHealth();
return Response.json(health, { status: health.status === "ok" ? 200 : 503 });
},
"/api/observability/feedback": {
POST: async (req) => {
const input = observabilityFeedbackSchema.safeParse(await readJson(req).catch(() => undefined));
if (!input.success) {
return Response.json({ error: "Invalid feedback", issues: input.error.issues }, { status: 400 });
}
if (!await getObservabilityTrace(input.data.traceId)) {
return Response.json({ error: "Trace not found" }, { status: 404 });
}
await observability.addFeedback({
traceId: input.data.traceId,
feedback: {
feedbackSource: "user",
feedbackType: "thumbs",
value: input.data.value,
comment: input.data.comment,
},
});
return Response.json({ recorded: true });
},
},
"/api/chat": {
POST: async (req) => {
try {
const parsed = chatPostSchema.safeParse(await readJson(req).catch(() => undefined));
if (!parsed.success) return Response.json({ error: "Invalid chat request", issues: parsed.error.issues }, { status: 400 });
const body = parsed.data;
const model = body.model ?? (await runtimeConfigStore.get()).defaultModel;
if (!(await isKnownModel(model))) return Response.json({ error: `Unknown model: ${model}` }, { status: 400 });
const sessionId = body.id ?? crypto.randomUUID();
const turnId = crypto.randomUUID();
const traceId = createTraceId();
const messages = body.messages as ChatMessage[];
const userText = latestUserText(messages);
if (!userText) return Response.json({ error: "A user message is required" }, { status: 400 });
const existing = body.id ? await sessionStore.get(body.id) : undefined;
const workspaceId = existing?.workspaceId ?? body.workspaceId ?? DEFAULT_WORKSPACE_ID;
if (!(await workspaceStore.get(workspaceId))) {
return Response.json({ error: "Workspace not found" }, { status: 404 });
}
if (!startedSessions.has(sessionId)) {
await hookRuntime.dispatch(createHookEvent("SessionStart", {
sessionId,
model,
detail: { source: existing ? "resume" : "create", workspaceId },
}));
startedSessions.add(sessionId);
}
let activityWriter: UIMessageStreamWriter<AgentActivityMessage> | undefined;
const pendingActivity: Array<Parameters<UIMessageStreamWriter<AgentActivityMessage>["write"]>[0]> = [];
const lifecycle = { sessionId, turnId, model };
const prepared = await executionRuntime.prepare({
...lifecycle,
traceId,
workspaceId,
executionSource: "chat",
prompt: userText,
onIterationComplete: async (iteration) => {
if (iteration.agentId === "popagent") return;
const chunk = {
type: "data-agentActivity" as const,
id: iteration.runId,
data: agentActivity(turnId, iteration),
};
if (activityWriter) activityWriter.write(chunk);
else pendingActivity.push(chunk);
},
});
if (prepared.promptWasReplaced) replaceLatestUserText(messages, prepared.prompt);
const agentStream = await handleChatStream({
mastra,
agentId: "popagent",
version: "v6",
params: {
id: sessionId,
messages: [messages.at(-1)!],
memory: { thread: sessionId, resource: RESOURCE_ID },
} as Parameters<typeof handleChatStream>[0]["params"],
defaultOptions: prepared.options as Parameters<typeof handleChatStream>[0]["defaultOptions"],
onError: (error) => {
void hookRuntime.dispatch(createHookEvent("StopFailure", {
...lifecycle,
detail: { error: { message: messageOf(error) } },
})).catch(() => undefined);
return "The agent could not complete this turn.";
},
messageMetadata: ({ part }) =>
part.type === "finish" && "totalUsage" in part
? { custom: { usage: part.totalUsage, turnId, traceId } }
: undefined,
});
const stream = createUIMessageStream<AgentActivityMessage>({
execute: ({ writer }) => {
activityWriter = writer;
for (const chunk of pendingActivity) writer.write(chunk);
pendingActivity.length = 0;
writer.merge(agentStream as unknown as Parameters<typeof writer.merge>[0]);
},
});
return createUIMessageStreamResponse({
stream: stream as unknown as Parameters<typeof createUIMessageStreamResponse>[0]["stream"],
});
} catch (error) {
if (error instanceof HookBlockedError) return Response.json({ error: error.reason }, { status: 403 });
appLogger().error("chat.turn.failed", { error });
return Response.json({ error: "Unable to start the agent turn" }, { status: 500 });
}
},
},
"/api/*": () => Response.json({ error: "Not found" }, { status: 404 }),
"/*": index,
};
return Bun.serve({
port,
routes: guardApiRoutes(routes) as Bun.Serve.Routes<undefined, string>,
});
}
const messageOf = (error: unknown) => error instanceof Error ? error.message : String(error);
if (import.meta.main) {
const server = startServer(Number(process.env.PORT ?? 3000));
await taskRuntime.start();
const maintenance = startMaintenance();
appLogger().info("server.started", { url: String(server.url) });
let stopping = false;
const shutdown = async (signal: string) => {
if (stopping) return;
stopping = true;
appLogger().info("server.stopping", { signal });
maintenance.stop();
server.stop(false);
await taskRuntime.stop();
await mastra.observability.flush();
await mastra.observability.shutdown();
await storage.close();
process.exit(0);
};
process.once("SIGINT", () => void shutdown("SIGINT"));
process.once("SIGTERM", () => void shutdown("SIGTERM"));
}