@agentstep/agent-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/package.json +45 -0
  2. package/src/auth/middleware.ts +38 -0
  3. package/src/backends/claude/args.ts +88 -0
  4. package/src/backends/claude/index.ts +193 -0
  5. package/src/backends/claude/permission-hook.ts +152 -0
  6. package/src/backends/claude/tool-bridge.ts +211 -0
  7. package/src/backends/claude/translator.ts +209 -0
  8. package/src/backends/claude/wrapper-script.ts +45 -0
  9. package/src/backends/codex/args.ts +69 -0
  10. package/src/backends/codex/auth.ts +35 -0
  11. package/src/backends/codex/index.ts +57 -0
  12. package/src/backends/codex/setup.ts +37 -0
  13. package/src/backends/codex/translator.ts +223 -0
  14. package/src/backends/codex/wrapper-script.ts +26 -0
  15. package/src/backends/factory/args.ts +45 -0
  16. package/src/backends/factory/auth.ts +30 -0
  17. package/src/backends/factory/index.ts +56 -0
  18. package/src/backends/factory/setup.ts +34 -0
  19. package/src/backends/factory/translator.ts +139 -0
  20. package/src/backends/factory/wrapper-script.ts +33 -0
  21. package/src/backends/gemini/args.ts +44 -0
  22. package/src/backends/gemini/auth.ts +30 -0
  23. package/src/backends/gemini/index.ts +53 -0
  24. package/src/backends/gemini/setup.ts +34 -0
  25. package/src/backends/gemini/translator.ts +139 -0
  26. package/src/backends/gemini/wrapper-script.ts +26 -0
  27. package/src/backends/opencode/args.ts +53 -0
  28. package/src/backends/opencode/auth.ts +53 -0
  29. package/src/backends/opencode/index.ts +70 -0
  30. package/src/backends/opencode/mcp.ts +67 -0
  31. package/src/backends/opencode/setup.ts +54 -0
  32. package/src/backends/opencode/translator.ts +168 -0
  33. package/src/backends/opencode/wrapper-script.ts +46 -0
  34. package/src/backends/registry.ts +38 -0
  35. package/src/backends/shared/ndjson.ts +29 -0
  36. package/src/backends/shared/translator-types.ts +69 -0
  37. package/src/backends/shared/wrap-prompt.ts +17 -0
  38. package/src/backends/types.ts +85 -0
  39. package/src/config/index.ts +95 -0
  40. package/src/db/agents.ts +185 -0
  41. package/src/db/api_keys.ts +78 -0
  42. package/src/db/batch.ts +142 -0
  43. package/src/db/client.ts +81 -0
  44. package/src/db/environments.ts +127 -0
  45. package/src/db/events.ts +208 -0
  46. package/src/db/memory.ts +143 -0
  47. package/src/db/migrations.ts +295 -0
  48. package/src/db/proxy.ts +37 -0
  49. package/src/db/sessions.ts +295 -0
  50. package/src/db/vaults.ts +110 -0
  51. package/src/errors.ts +53 -0
  52. package/src/handlers/agents.ts +194 -0
  53. package/src/handlers/batch.ts +41 -0
  54. package/src/handlers/docs.ts +87 -0
  55. package/src/handlers/environments.ts +154 -0
  56. package/src/handlers/events.ts +234 -0
  57. package/src/handlers/index.ts +12 -0
  58. package/src/handlers/memory.ts +141 -0
  59. package/src/handlers/openapi.ts +14 -0
  60. package/src/handlers/sessions.ts +223 -0
  61. package/src/handlers/stream.ts +76 -0
  62. package/src/handlers/threads.ts +26 -0
  63. package/src/handlers/ui/app.js +984 -0
  64. package/src/handlers/ui/index.html +112 -0
  65. package/src/handlers/ui/style.css +164 -0
  66. package/src/handlers/ui.ts +1281 -0
  67. package/src/handlers/vaults.ts +99 -0
  68. package/src/http.ts +35 -0
  69. package/src/index.ts +104 -0
  70. package/src/init.ts +227 -0
  71. package/src/openapi/registry.ts +8 -0
  72. package/src/openapi/schemas.ts +625 -0
  73. package/src/openapi/spec.ts +691 -0
  74. package/src/providers/apple.ts +220 -0
  75. package/src/providers/daytona.ts +217 -0
  76. package/src/providers/docker.ts +264 -0
  77. package/src/providers/e2b.ts +203 -0
  78. package/src/providers/fly.ts +276 -0
  79. package/src/providers/modal.ts +222 -0
  80. package/src/providers/podman.ts +206 -0
  81. package/src/providers/registry.ts +28 -0
  82. package/src/providers/shared.ts +11 -0
  83. package/src/providers/sprites.ts +55 -0
  84. package/src/providers/types.ts +73 -0
  85. package/src/providers/vercel.ts +208 -0
  86. package/src/proxy/forward.ts +111 -0
  87. package/src/queue/index.ts +111 -0
  88. package/src/sessions/actor.ts +53 -0
  89. package/src/sessions/bus.ts +155 -0
  90. package/src/sessions/driver.ts +818 -0
  91. package/src/sessions/grader.ts +120 -0
  92. package/src/sessions/interrupt.ts +14 -0
  93. package/src/sessions/sweeper.ts +136 -0
  94. package/src/sessions/threads.ts +126 -0
  95. package/src/sessions/tools.ts +50 -0
  96. package/src/shutdown.ts +78 -0
  97. package/src/sprite/client.ts +294 -0
  98. package/src/sprite/exec.ts +161 -0
  99. package/src/sprite/lifecycle.ts +339 -0
  100. package/src/sprite/pool.ts +65 -0
  101. package/src/sprite/setup.ts +159 -0
  102. package/src/state.ts +61 -0
  103. package/src/types.ts +339 -0
  104. package/src/util/clock.ts +7 -0
  105. package/src/util/ids.ts +11 -0
@@ -0,0 +1,87 @@
1
+ const CUSTOM_CSS = `
2
+ /* AgentStep Gateway Design System — Scalar Theme */
3
+ .light-mode {
4
+ --scalar-color-1: #171717;
5
+ --scalar-color-2: #525252;
6
+ --scalar-color-3: #737373;
7
+ --scalar-color-accent: #65a30d;
8
+ --scalar-color-green: #65a30d;
9
+ --scalar-background-1: #ffffff;
10
+ --scalar-background-2: #f5f5f5;
11
+ --scalar-background-3: #e5e5e5;
12
+ --scalar-background-accent: #ecfccb;
13
+ --scalar-border-color: #e5e5e5;
14
+ --scalar-sidebar-background-1: #f5f5f5;
15
+ --scalar-sidebar-color-1: #171717;
16
+ --scalar-sidebar-color-2: #525252;
17
+ --scalar-sidebar-color-active: #65a30d;
18
+ --scalar-sidebar-border-color: #e5e5e5;
19
+ --scalar-button-1: #171717;
20
+ --scalar-button-1-color: #ffffff;
21
+ --scalar-button-1-hover: #262626;
22
+ }
23
+ .dark-mode {
24
+ --scalar-color-1: #e5e5e5;
25
+ --scalar-color-2: #a3a3a3;
26
+ --scalar-color-3: #737373;
27
+ --scalar-color-accent: #a3e635;
28
+ --scalar-color-green: #a3e635;
29
+ --scalar-background-1: #0a0a0a;
30
+ --scalar-background-2: #171717;
31
+ --scalar-background-3: #262626;
32
+ --scalar-background-accent: rgba(163,230,53,0.1);
33
+ --scalar-border-color: rgba(255,255,255,0.1);
34
+ --scalar-sidebar-background-1: #171717;
35
+ --scalar-sidebar-color-1: #e5e5e5;
36
+ --scalar-sidebar-color-2: #a3a3a3;
37
+ --scalar-sidebar-color-active: #a3e635;
38
+ --scalar-sidebar-border-color: rgba(255,255,255,0.1);
39
+ --scalar-button-1: #a3e635;
40
+ --scalar-button-1-color: #0a0a0a;
41
+ --scalar-button-1-hover: #bef264;
42
+ }
43
+ `.trim();
44
+
45
+ const SCALAR_CONFIG_JSON = JSON.stringify({
46
+ spec: { url: "/v1/openapi.json" },
47
+ theme: "none",
48
+ layout: "modern",
49
+ documentDownloadType: "direct",
50
+ darkMode: true,
51
+ customCss: CUSTOM_CSS,
52
+ hiddenClients: true,
53
+ hideGenerateMcpServer: true,
54
+ metaData: {
55
+ title: "AgentStep Gateway — API Reference",
56
+ },
57
+ }).replace(/"/g, """);
58
+
59
+ const HTML = `<!doctype html>
60
+ <html lang="en">
61
+ <head>
62
+ <meta charset="utf-8" />
63
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
64
+ <title>AgentStep Gateway — API Reference</title>
65
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
66
+ <style>
67
+ body { margin: 0; }
68
+ </style>
69
+ </head>
70
+ <body>
71
+ <script
72
+ id="api-reference"
73
+ data-configuration="${SCALAR_CONFIG_JSON}"
74
+ ></script>
75
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
76
+ </body>
77
+ </html>
78
+ `;
79
+
80
+ export async function handleGetDocs(): Promise<Response> {
81
+ return new Response(HTML, {
82
+ headers: {
83
+ "content-type": "text/html; charset=utf-8",
84
+ "cache-control": "no-cache",
85
+ },
86
+ });
87
+ }
@@ -0,0 +1,154 @@
1
+ import { z } from "zod";
2
+ import { routeWrap, jsonOk } from "../http";
3
+ import {
4
+ createEnvironment,
5
+ getEnvironment,
6
+ listEnvironments,
7
+ archiveEnvironment,
8
+ deleteEnvironment,
9
+ hasSessionsAttached,
10
+ } from "../db/environments";
11
+ import { kickoffEnvironmentSetup } from "../sprite/setup";
12
+ import { resolveContainerProvider as resolveProvider } from "../providers/registry";
13
+ import { isProxied, markProxied, unmarkProxied } from "../db/proxy";
14
+ import { forwardToAnthropic } from "../proxy/forward";
15
+ import { badRequest, conflict, notFound } from "../errors";
16
+
17
+ const PackagesSchema = z
18
+ .object({
19
+ apt: z.array(z.string()).optional(),
20
+ cargo: z.array(z.string()).optional(),
21
+ gem: z.array(z.string()).optional(),
22
+ go: z.array(z.string()).optional(),
23
+ npm: z.array(z.string()).optional(),
24
+ pip: z.array(z.string()).optional(),
25
+ })
26
+ .optional();
27
+
28
+ const NetworkingSchema = z.union([
29
+ z.object({ type: z.literal("unrestricted") }),
30
+ z.object({
31
+ type: z.literal("limited"),
32
+ allowed_hosts: z.array(z.string()).optional(),
33
+ allow_mcp_servers: z.boolean().optional(),
34
+ allow_package_managers: z.boolean().optional(),
35
+ }),
36
+ ]);
37
+
38
+ const ConfigSchema = z.object({
39
+ type: z.literal("cloud"),
40
+ provider: z.enum(["sprites", "docker", "apple", "podman", "e2b", "vercel", "daytona", "fly", "modal"]).optional(),
41
+ packages: PackagesSchema,
42
+ networking: NetworkingSchema.optional(),
43
+ });
44
+
45
+ const CreateSchema = z.object({
46
+ name: z.string().min(1),
47
+ config: ConfigSchema,
48
+ backend: z.enum(["anthropic"]).optional(),
49
+ });
50
+
51
+ export function handleCreateEnvironment(request: Request): Promise<Response> {
52
+ return routeWrap(request, async () => {
53
+ const rawBody = await request.text();
54
+ const body = rawBody ? JSON.parse(rawBody) : null;
55
+ const parsed = CreateSchema.safeParse(body);
56
+ if (!parsed.success) throw badRequest(parsed.error.message);
57
+
58
+ if (parsed.data.backend === "anthropic") {
59
+ const { backend: _, ...rest } = body as Record<string, unknown>;
60
+ const forwardBody = JSON.stringify(rest);
61
+ const proxyRes = await forwardToAnthropic(request, "/v1/environments", { body: forwardBody });
62
+ if (proxyRes.ok) {
63
+ try {
64
+ const data = (await proxyRes.clone().json()) as { id: string };
65
+ markProxied(data.id, "environment");
66
+ } catch { /* best-effort */ }
67
+ }
68
+ return proxyRes;
69
+ }
70
+
71
+ // Pre-flight: check provider is available before creating the environment
72
+ const providerName = parsed.data.config.provider ?? "sprites";
73
+ const provider = await resolveProvider(providerName);
74
+ if (provider.checkAvailability) {
75
+ const result = await provider.checkAvailability();
76
+ if (!result.available) {
77
+ throw badRequest(`Provider "${providerName}" is not available: ${result.message}`);
78
+ }
79
+ }
80
+
81
+ const env = createEnvironment({
82
+ name: parsed.data.name,
83
+ config: parsed.data.config,
84
+ });
85
+
86
+ kickoffEnvironmentSetup(env.id);
87
+ return jsonOk(env, 201);
88
+ });
89
+ }
90
+
91
+ export function handleListEnvironments(request: Request): Promise<Response> {
92
+ return routeWrap(request, async ({ request: req }) => {
93
+ const url = new URL(req.url);
94
+ const limit = url.searchParams.get("limit");
95
+ const order = url.searchParams.get("order") as "asc" | "desc" | null;
96
+ const includeArchived = url.searchParams.get("include_archived") === "true";
97
+ const cursor = url.searchParams.get("page") ?? undefined;
98
+
99
+ const data = listEnvironments({
100
+ limit: limit ? Number(limit) : undefined,
101
+ order: order ?? undefined,
102
+ includeArchived,
103
+ cursor,
104
+ });
105
+ return jsonOk({
106
+ data,
107
+ next_page: data.length > 0 ? data[data.length - 1].id : null,
108
+ });
109
+ });
110
+ }
111
+
112
+ export function handleGetEnvironment(request: Request, id: string): Promise<Response> {
113
+ return routeWrap(request, async () => {
114
+ if (isProxied(id)) return forwardToAnthropic(request, `/v1/environments/${id}`);
115
+ const env = getEnvironment(id);
116
+ if (!env) throw notFound(`environment ${id} not found`);
117
+ return jsonOk(env);
118
+ });
119
+ }
120
+
121
+ export function handleDeleteEnvironment(request: Request, id: string): Promise<Response> {
122
+ return routeWrap(request, async () => {
123
+ if (isProxied(id)) {
124
+ const res = await forwardToAnthropic(request, `/v1/environments/${id}`);
125
+ if (res.ok) unmarkProxied(id);
126
+ return res;
127
+ }
128
+ const env = getEnvironment(id);
129
+ if (!env) throw notFound(`environment ${id} not found`);
130
+ if (hasSessionsAttached(id)) {
131
+ throw conflict(`environment ${id} still has active sessions attached`);
132
+ }
133
+ deleteEnvironment(id);
134
+ return jsonOk({ id, type: "environment_deleted" });
135
+ });
136
+ }
137
+
138
+ export function handleArchiveEnvironment(request: Request, id: string): Promise<Response> {
139
+ return routeWrap(request, async () => {
140
+ if (isProxied(id)) {
141
+ const res = await forwardToAnthropic(request, `/v1/environments/${id}/archive`);
142
+ if (res.ok) unmarkProxied(id);
143
+ return res;
144
+ }
145
+ const existed = getEnvironment(id);
146
+ if (!existed) throw notFound(`environment ${id} not found`);
147
+ if (hasSessionsAttached(id)) {
148
+ throw conflict(`environment ${id} still has active sessions attached`);
149
+ }
150
+ archiveEnvironment(id);
151
+ const env = getEnvironment(id)!;
152
+ return jsonOk(env);
153
+ });
154
+ }
@@ -0,0 +1,234 @@
1
+ import { z } from "zod";
2
+ import { routeWrap, jsonOk } from "../http";
3
+ import { getSession, getSessionRow, setOutcomeCriteria } from "../db/sessions";
4
+ import { listEvents, rowToManagedEvent } from "../db/events";
5
+ import { appendEvent } from "../sessions/bus";
6
+ import { getActor } from "../sessions/actor";
7
+ import { interruptSession } from "../sessions/interrupt";
8
+ import { runTurn, writePermissionResponse } from "../sessions/driver";
9
+ import { enqueueTurn } from "../queue";
10
+ import { pushPendingUserInput, type TurnInput } from "../state";
11
+ import { isProxied } from "../db/proxy";
12
+ import { forwardToAnthropic } from "../proxy/forward";
13
+ import { badRequest, notFound } from "../errors";
14
+ import { getAgent } from "../db/agents";
15
+ import { nowMs } from "../util/clock";
16
+ import type { EventRow } from "../types";
17
+
18
+ const TextBlock = z.object({ type: z.literal("text"), text: z.string() });
19
+
20
+ const UserMessage = z.object({
21
+ type: z.literal("user.message"),
22
+ content: z.array(TextBlock).min(1),
23
+ });
24
+
25
+ const UserInterrupt = z.object({ type: z.literal("user.interrupt") });
26
+
27
+ const UserToolConfirmation = z.object({
28
+ type: z.literal("user.tool_confirmation"),
29
+ tool_use_id: z.string().optional(),
30
+ result: z.enum(["allow", "deny"]).optional(),
31
+ deny_message: z.string().optional(),
32
+ });
33
+
34
+ const UserCustomToolResult = z.object({
35
+ type: z.literal("user.custom_tool_result"),
36
+ custom_tool_use_id: z.string(),
37
+ content: z.array(z.unknown()),
38
+ });
39
+
40
+ const UserDefineOutcome = z.object({
41
+ type: z.literal("user.define_outcome"),
42
+ description: z.string().min(1),
43
+ rubric: z.string().optional(),
44
+ max_iterations: z.number().int().min(1).max(20).optional(),
45
+ });
46
+
47
+ const UserEvent = z.union([
48
+ UserMessage,
49
+ UserInterrupt,
50
+ UserToolConfirmation,
51
+ UserCustomToolResult,
52
+ UserDefineOutcome,
53
+ ]);
54
+
55
+ const BatchSchema = z.object({
56
+ events: z.array(UserEvent).min(1),
57
+ });
58
+
59
+ export function handlePostEvents(request: Request, sessionId: string): Promise<Response> {
60
+ return routeWrap(request, async () => {
61
+ if (isProxied(sessionId)) return forwardToAnthropic(request, `/v1/sessions/${sessionId}/events`);
62
+ const session = getSession(sessionId);
63
+ if (!session) throw notFound(`session ${sessionId} not found`);
64
+
65
+ const body = await request.json().catch(() => null);
66
+ const parsed = BatchSchema.safeParse(body);
67
+ if (!parsed.success) throw badRequest(parsed.error.message);
68
+
69
+ for (const event of parsed.data.events) {
70
+ if (event.type === "user.tool_confirmation") {
71
+ const agent = getAgent(session.agent.id, session.agent.version);
72
+ if (!agent?.confirmation_mode) {
73
+ throw badRequest(
74
+ "user.tool_confirmation is not supported: this agent does not have confirmation_mode enabled. " +
75
+ "Set confirmation_mode: true on the agent to use tool confirmation.",
76
+ );
77
+ }
78
+ }
79
+ }
80
+
81
+ const idemHeader = request.headers.get("idempotency-key") || request.headers.get("Idempotency-Key");
82
+ const actor = getActor(sessionId);
83
+
84
+ const hasInterrupt = parsed.data.events.some((e) => e.type === "user.interrupt");
85
+ if (hasInterrupt) {
86
+ interruptSession(sessionId);
87
+ }
88
+
89
+ const appended: { rows: EventRow[]; pendingForTurn: TurnInput[] } = await actor.enqueue(async () => {
90
+ const rows: EventRow[] = [];
91
+ const pendingForTurn: TurnInput[] = [];
92
+ let sawInterrupt = false;
93
+
94
+ for (const [idx, event] of parsed.data.events.entries()) {
95
+ const ik = idemHeader ? `${idemHeader}:${idx}` : null;
96
+
97
+ if (event.type === "user.interrupt") {
98
+ const row = appendEvent(sessionId, {
99
+ type: "user.interrupt",
100
+ payload: {},
101
+ origin: "user",
102
+ idempotencyKey: ik,
103
+ processedAt: nowMs(),
104
+ });
105
+ rows.push(row);
106
+ sawInterrupt = true;
107
+ continue;
108
+ }
109
+
110
+ if (event.type === "user.message") {
111
+ const text = event.content.map((b) => b.text).join("");
112
+ const row = appendEvent(sessionId, {
113
+ type: "user.message",
114
+ payload: { content: event.content },
115
+ origin: "user",
116
+ idempotencyKey: ik,
117
+ processedAt: null,
118
+ });
119
+ rows.push(row);
120
+
121
+ const inp: TurnInput = { kind: "text", eventId: row.id, text };
122
+ const currentStatus = getSessionRow(sessionId)?.status ?? "idle";
123
+ if (currentStatus === "running" || sawInterrupt) {
124
+ pushPendingUserInput({ sessionId, input: inp });
125
+ } else {
126
+ pendingForTurn.push(inp);
127
+ }
128
+ continue;
129
+ }
130
+
131
+ if (event.type === "user.custom_tool_result") {
132
+ const row = appendEvent(sessionId, {
133
+ type: "user.custom_tool_result",
134
+ payload: {
135
+ custom_tool_use_id: event.custom_tool_use_id,
136
+ content: event.content,
137
+ },
138
+ origin: "user",
139
+ idempotencyKey: ik,
140
+ processedAt: nowMs(),
141
+ });
142
+ rows.push(row);
143
+
144
+ const inp: TurnInput = {
145
+ kind: "tool_result",
146
+ eventId: row.id,
147
+ custom_tool_use_id: event.custom_tool_use_id,
148
+ content: event.content as unknown[],
149
+ };
150
+ const currentStatus = getSessionRow(sessionId)?.status ?? "idle";
151
+ if (currentStatus === "running" || sawInterrupt) {
152
+ pushPendingUserInput({ sessionId, input: inp });
153
+ } else {
154
+ pendingForTurn.push(inp);
155
+ }
156
+ continue;
157
+ }
158
+
159
+ if (event.type === "user.tool_confirmation") {
160
+ const confirmResult = event.result ?? "allow";
161
+ const row = appendEvent(sessionId, {
162
+ type: "user.tool_confirmation",
163
+ payload: {
164
+ tool_use_id: event.tool_use_id ?? null,
165
+ result: confirmResult,
166
+ deny_message: event.deny_message ?? null,
167
+ },
168
+ origin: "user",
169
+ idempotencyKey: ik,
170
+ processedAt: nowMs(),
171
+ });
172
+ rows.push(row);
173
+
174
+ void writePermissionResponse(
175
+ sessionId,
176
+ confirmResult,
177
+ event.deny_message ?? undefined,
178
+ ).catch((err: unknown) => {
179
+ console.warn(`[events] writePermissionResponse failed:`, err);
180
+ });
181
+ continue;
182
+ }
183
+
184
+ if (event.type === "user.define_outcome") {
185
+ const { type: _type, ...criteria } = event;
186
+ const row = appendEvent(sessionId, {
187
+ type: "user.define_outcome",
188
+ payload: criteria,
189
+ origin: "user",
190
+ idempotencyKey: ik,
191
+ processedAt: nowMs(),
192
+ });
193
+ rows.push(row);
194
+ setOutcomeCriteria(sessionId, criteria);
195
+ continue;
196
+ }
197
+ }
198
+
199
+ return { rows, pendingForTurn };
200
+ });
201
+
202
+ if (appended.pendingForTurn.length > 0) {
203
+ const row = getSessionRow(sessionId);
204
+ if (row) {
205
+ void enqueueTurn(row.environment_id, () => runTurn(sessionId, appended.pendingForTurn)).catch(
206
+ (err: unknown) => {
207
+ console.error(`[events] enqueueTurn failed:`, err);
208
+ },
209
+ );
210
+ }
211
+ }
212
+
213
+ return jsonOk({ events: appended.rows.map(rowToManagedEvent) });
214
+ });
215
+ }
216
+
217
+ export function handleListEvents(request: Request, sessionId: string): Promise<Response> {
218
+ return routeWrap(request, async () => {
219
+ if (isProxied(sessionId)) return forwardToAnthropic(request, `/v1/sessions/${sessionId}/events`);
220
+ const session = getSession(sessionId);
221
+ if (!session) throw notFound(`session ${sessionId} not found`);
222
+
223
+ const url = new URL(request.url);
224
+ const limit = Number(url.searchParams.get("limit") ?? "50");
225
+ const order = (url.searchParams.get("order") === "desc" ? "desc" : "asc") as "asc" | "desc";
226
+ const afterSeq = Number(url.searchParams.get("after_seq") ?? "0");
227
+
228
+ const rows = listEvents(sessionId, { limit, order, afterSeq });
229
+ return jsonOk({
230
+ data: rows.map(rowToManagedEvent),
231
+ next_page: rows.length > 0 ? String(rows[rows.length - 1].seq) : null,
232
+ });
233
+ });
234
+ }
@@ -0,0 +1,12 @@
1
+ export { handleCreateAgent, handleListAgents, handleGetAgent, handleUpdateAgent, handleDeleteAgent } from "./agents";
2
+ export { handleCreateEnvironment, handleListEnvironments, handleGetEnvironment, handleDeleteEnvironment, handleArchiveEnvironment } from "./environments";
3
+ export { handleCreateSession, handleListSessions, handleGetSession, handleUpdateSession, handleDeleteSession, handleArchiveSession } from "./sessions";
4
+ export { handlePostEvents, handleListEvents } from "./events";
5
+ export { handleSessionStream } from "./stream";
6
+ export { handleCreateVault, handleListVaults, handleGetVault, handleDeleteVault, handleListEntries, handleGetEntry, handlePutEntry, handleDeleteEntry } from "./vaults";
7
+ export { handleBatch } from "./batch";
8
+ export { handleListThreads } from "./threads";
9
+ export { handleCreateMemoryStore, handleListMemoryStores, handleGetMemoryStore, handleDeleteMemoryStore, handleCreateMemory, handleListMemories, handleGetMemory, handleUpdateMemory, handleDeleteMemory } from "./memory";
10
+ export { handleGetOpenApiSpec } from "./openapi";
11
+ export { handleGetDocs } from "./docs";
12
+ export { handleGetUI } from "./ui";
@@ -0,0 +1,141 @@
1
+ import { z } from "zod";
2
+ import { routeWrap, jsonOk } from "../http";
3
+ import {
4
+ createMemoryStore,
5
+ getMemoryStore,
6
+ listMemoryStores,
7
+ deleteMemoryStore,
8
+ createOrUpsertMemory,
9
+ getMemory,
10
+ listMemories,
11
+ updateMemory,
12
+ deleteMemory,
13
+ } from "../db/memory";
14
+ import { badRequest, notFound, conflict } from "../errors";
15
+
16
+ // ── Memory Stores ─────────────────────────────────────────���──────────────
17
+
18
+ const CreateStoreSchema = z.object({
19
+ name: z.string().min(1),
20
+ description: z.string().optional(),
21
+ });
22
+
23
+ export function handleCreateMemoryStore(request: Request): Promise<Response> {
24
+ return routeWrap(request, async () => {
25
+ const body = await request.json();
26
+ const parsed = CreateStoreSchema.safeParse(body);
27
+ if (!parsed.success) throw badRequest(parsed.error.message);
28
+
29
+ const store = createMemoryStore({
30
+ name: parsed.data.name,
31
+ description: parsed.data.description,
32
+ });
33
+ return jsonOk(store, 201);
34
+ });
35
+ }
36
+
37
+ export function handleListMemoryStores(request: Request): Promise<Response> {
38
+ return routeWrap(request, async () => {
39
+ const data = listMemoryStores();
40
+ return jsonOk({ data });
41
+ });
42
+ }
43
+
44
+ export function handleGetMemoryStore(request: Request, id: string): Promise<Response> {
45
+ return routeWrap(request, async () => {
46
+ const store = getMemoryStore(id);
47
+ if (!store) throw notFound(`memory store not found: ${id}`);
48
+ return jsonOk(store);
49
+ });
50
+ }
51
+
52
+ export function handleDeleteMemoryStore(request: Request, id: string): Promise<Response> {
53
+ return routeWrap(request, async () => {
54
+ const deleted = deleteMemoryStore(id);
55
+ if (!deleted) throw notFound(`memory store not found: ${id}`);
56
+ return jsonOk({ id, type: "memory_store_deleted" });
57
+ });
58
+ }
59
+
60
+ // ── Memories ─────────────────────────────────────────────────────────────
61
+
62
+ const CreateMemorySchema = z.object({
63
+ path: z.string().min(1),
64
+ content: z.string(),
65
+ });
66
+
67
+ const UpdateMemorySchema = z.object({
68
+ content: z.string(),
69
+ content_sha256: z.string().optional(),
70
+ });
71
+
72
+ export function handleCreateMemory(request: Request, storeId: string): Promise<Response> {
73
+ return routeWrap(request, async () => {
74
+ const store = getMemoryStore(storeId);
75
+ if (!store) throw notFound(`memory store not found: ${storeId}`);
76
+
77
+ const body = await request.json();
78
+ const parsed = CreateMemorySchema.safeParse(body);
79
+ if (!parsed.success) throw badRequest(parsed.error.message);
80
+
81
+ const memory = createOrUpsertMemory(storeId, parsed.data.path, parsed.data.content);
82
+ return jsonOk(memory, 201);
83
+ });
84
+ }
85
+
86
+ export function handleListMemories(request: Request, storeId: string): Promise<Response> {
87
+ return routeWrap(request, async () => {
88
+ const store = getMemoryStore(storeId);
89
+ if (!store) throw notFound(`memory store not found: ${storeId}`);
90
+
91
+ const data = listMemories(storeId);
92
+ return jsonOk({ data });
93
+ });
94
+ }
95
+
96
+ export function handleGetMemory(request: Request, storeId: string, memId: string): Promise<Response> {
97
+ return routeWrap(request, async () => {
98
+ const store = getMemoryStore(storeId);
99
+ if (!store) throw notFound(`memory store not found: ${storeId}`);
100
+
101
+ const memory = getMemory(memId);
102
+ if (!memory || memory.store_id !== storeId) throw notFound(`memory not found: ${memId}`);
103
+ return jsonOk(memory);
104
+ });
105
+ }
106
+
107
+ export function handleUpdateMemory(request: Request, storeId: string, memId: string): Promise<Response> {
108
+ return routeWrap(request, async () => {
109
+ const store = getMemoryStore(storeId);
110
+ if (!store) throw notFound(`memory store not found: ${storeId}`);
111
+
112
+ const body = await request.json();
113
+ const parsed = UpdateMemorySchema.safeParse(body);
114
+ if (!parsed.success) throw badRequest(parsed.error.message);
115
+
116
+ const { memory, conflict: isConflict } = updateMemory(
117
+ memId,
118
+ parsed.data.content,
119
+ parsed.data.content_sha256,
120
+ );
121
+
122
+ if (isConflict) {
123
+ throw conflict(
124
+ `content_sha256 precondition failed: the memory has been modified since you last read it. Re-read and retry.`,
125
+ );
126
+ }
127
+ if (!memory) throw notFound(`memory not found: ${memId}`);
128
+ return jsonOk(memory);
129
+ });
130
+ }
131
+
132
+ export function handleDeleteMemory(request: Request, storeId: string, memId: string): Promise<Response> {
133
+ return routeWrap(request, async () => {
134
+ const store = getMemoryStore(storeId);
135
+ if (!store) throw notFound(`memory store not found: ${storeId}`);
136
+
137
+ const deleted = deleteMemory(memId);
138
+ if (!deleted) throw notFound(`memory not found: ${memId}`);
139
+ return jsonOk({ id: memId, type: "memory_deleted" });
140
+ });
141
+ }
@@ -0,0 +1,14 @@
1
+ import { buildOpenApiDocument } from "../openapi/spec";
2
+
3
+ function originFromRequest(request: Request): string {
4
+ const headers = request.headers;
5
+ const proto = headers.get("x-forwarded-proto") ?? new URL(request.url).protocol.replace(":", "");
6
+ const host = headers.get("x-forwarded-host") ?? headers.get("host") ?? new URL(request.url).host;
7
+ return `${proto}://${host}`;
8
+ }
9
+
10
+ export async function handleGetOpenApiSpec(request: Request): Promise<Response> {
11
+ const serverUrl = originFromRequest(request);
12
+ const doc = buildOpenApiDocument({ serverUrl });
13
+ return Response.json(doc);
14
+ }