@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,99 @@
1
+ import { z } from "zod";
2
+ import { routeWrap, jsonOk } from "../http";
3
+ import { createVault, getVault, deleteVault, listVaults, listEntries, getEntry, setEntry, deleteEntry } from "../db/vaults";
4
+ import { getAgent } from "../db/agents";
5
+ import { badRequest, notFound } from "../errors";
6
+
7
+ const CreateVaultSchema = z.object({
8
+ agent_id: z.string().min(1),
9
+ name: z.string().min(1),
10
+ });
11
+
12
+ const PutEntrySchema = z.object({
13
+ value: z.string(),
14
+ });
15
+
16
+ export function handleCreateVault(request: Request): Promise<Response> {
17
+ return routeWrap(request, async () => {
18
+ const body = await request.json();
19
+ const parsed = CreateVaultSchema.safeParse(body);
20
+ if (!parsed.success) throw badRequest(parsed.error.message);
21
+
22
+ const agent = getAgent(parsed.data.agent_id);
23
+ if (!agent) throw notFound(`agent not found: ${parsed.data.agent_id}`);
24
+
25
+ const vault = createVault({
26
+ agent_id: parsed.data.agent_id,
27
+ name: parsed.data.name,
28
+ });
29
+ return jsonOk(vault, 201);
30
+ });
31
+ }
32
+
33
+ export function handleListVaults(request: Request): Promise<Response> {
34
+ return routeWrap(request, async ({ request: req }) => {
35
+ const url = new URL(req.url);
36
+ const agentId = url.searchParams.get("agent_id") ?? undefined;
37
+ const data = listVaults({ agent_id: agentId });
38
+ return jsonOk({ data });
39
+ });
40
+ }
41
+
42
+ export function handleGetVault(request: Request, id: string): Promise<Response> {
43
+ return routeWrap(request, async () => {
44
+ const vault = getVault(id);
45
+ if (!vault) throw notFound(`vault not found: ${id}`);
46
+ return jsonOk(vault);
47
+ });
48
+ }
49
+
50
+ export function handleDeleteVault(request: Request, id: string): Promise<Response> {
51
+ return routeWrap(request, async () => {
52
+ const deleted = deleteVault(id);
53
+ if (!deleted) throw notFound(`vault not found: ${id}`);
54
+ return jsonOk({ id, type: "vault_deleted" });
55
+ });
56
+ }
57
+
58
+ export function handleListEntries(request: Request, vaultId: string): Promise<Response> {
59
+ return routeWrap(request, async () => {
60
+ const vault = getVault(vaultId);
61
+ if (!vault) throw notFound(`vault not found: ${vaultId}`);
62
+ const data = listEntries(vaultId);
63
+ return jsonOk({ data });
64
+ });
65
+ }
66
+
67
+ export function handleGetEntry(request: Request, vaultId: string, key: string): Promise<Response> {
68
+ return routeWrap(request, async () => {
69
+ const vault = getVault(vaultId);
70
+ if (!vault) throw notFound(`vault not found: ${vaultId}`);
71
+ const entry = getEntry(vaultId, key);
72
+ if (!entry) throw notFound(`entry not found: ${key}`);
73
+ return jsonOk(entry);
74
+ });
75
+ }
76
+
77
+ export function handlePutEntry(request: Request, vaultId: string, key: string): Promise<Response> {
78
+ return routeWrap(request, async () => {
79
+ const vault = getVault(vaultId);
80
+ if (!vault) throw notFound(`vault not found: ${vaultId}`);
81
+
82
+ const body = await request.json();
83
+ const parsed = PutEntrySchema.safeParse(body);
84
+ if (!parsed.success) throw badRequest(parsed.error.message);
85
+
86
+ setEntry(vaultId, key, parsed.data.value);
87
+ return jsonOk({ key, value: parsed.data.value });
88
+ });
89
+ }
90
+
91
+ export function handleDeleteEntry(request: Request, vaultId: string, key: string): Promise<Response> {
92
+ return routeWrap(request, async () => {
93
+ const vault = getVault(vaultId);
94
+ if (!vault) throw notFound(`vault not found: ${vaultId}`);
95
+ const deleted = deleteEntry(vaultId, key);
96
+ if (!deleted) throw notFound(`entry not found: ${key}`);
97
+ return jsonOk({ key, type: "entry_deleted" });
98
+ });
99
+ }
package/src/http.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Route helpers: common boilerplate for every /v1 handler.
3
+ *
4
+ * - `ensureInitialized()` runs on first request
5
+ * - `authenticate(request)` extracts + validates the API key
6
+ * - wraps errors into the Managed Agents envelope
7
+ *
8
+ * Framework-agnostic — uses Web Standard Response only.
9
+ */
10
+ import { ensureInitialized } from "./init";
11
+ import { authenticate } from "./auth/middleware";
12
+ import { toResponse } from "./errors";
13
+ import type { AuthContext } from "./types";
14
+
15
+ export interface RouteContext {
16
+ auth: AuthContext;
17
+ request: Request;
18
+ }
19
+
20
+ export async function routeWrap(
21
+ request: Request,
22
+ handler: (ctx: RouteContext) => Promise<Response>,
23
+ ): Promise<Response> {
24
+ try {
25
+ await ensureInitialized();
26
+ const auth = await authenticate(request);
27
+ return await handler({ auth, request });
28
+ } catch (err) {
29
+ return toResponse(err);
30
+ }
31
+ }
32
+
33
+ export function jsonOk<T>(body: T, status = 200): Response {
34
+ return Response.json(body, { status });
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @agentstep/agent-sdk — framework-agnostic Managed Agents engine.
3
+ *
4
+ * This is the public API surface. Adapters (Next.js, Hono, etc.) import
5
+ * from here or from subpath exports like `@agentstep/agent-sdk/db/agents`.
6
+ */
7
+
8
+ // HTTP helpers
9
+ export { routeWrap, jsonOk, type RouteContext } from "./http";
10
+
11
+ // Errors
12
+ export {
13
+ ApiError,
14
+ envelope,
15
+ toResponse,
16
+ badRequest,
17
+ unauthorized,
18
+ forbidden,
19
+ notFound,
20
+ conflict,
21
+ tooManyRequests,
22
+ serverBusy,
23
+ type ErrorType,
24
+ } from "./errors";
25
+
26
+ // Init + shutdown
27
+ export { ensureInitialized } from "./init";
28
+ export { installShutdownHandlers } from "./shutdown";
29
+
30
+ // Auth
31
+ export { authenticate } from "./auth/middleware";
32
+
33
+ // Types
34
+ export type {
35
+ Agent,
36
+ AuthContext,
37
+ EventRow,
38
+ ManagedEvent,
39
+ McpServerConfig,
40
+ SessionStatus,
41
+ } from "./types";
42
+
43
+ // State
44
+ export { pushPendingUserInput, type TurnInput } from "./state";
45
+
46
+ // DB
47
+ export { getDb } from "./db/client";
48
+ export { createAgent, getAgent, updateAgent, archiveAgent, listAgents } from "./db/agents";
49
+ export {
50
+ createSession,
51
+ getSession,
52
+ getSessionRow,
53
+ listSessions,
54
+ updateSessionMutable,
55
+ archiveSession,
56
+ setOutcomeCriteria,
57
+ } from "./db/sessions";
58
+ export {
59
+ createEnvironment,
60
+ getEnvironment,
61
+ listEnvironments,
62
+ archiveEnvironment,
63
+ deleteEnvironment,
64
+ hasSessionsAttached,
65
+ } from "./db/environments";
66
+ export { appendEventsBatch, listEvents, rowToManagedEvent } from "./db/events";
67
+ export { createVault, getVault, deleteVault, listVaults, listEntries, getEntry, setEntry, deleteEntry } from "./db/vaults";
68
+ export {
69
+ createMemoryStore, getMemoryStore, listMemoryStores, deleteMemoryStore,
70
+ createOrUpsertMemory, getMemory, getMemoryByPath, listMemories, updateMemory, deleteMemory,
71
+ } from "./db/memory";
72
+ export { executeBatch, BatchError } from "./db/batch";
73
+ export { isProxied, markProxied, unmarkProxied } from "./db/proxy";
74
+
75
+ // Sessions
76
+ export { appendEvent, subscribe, dropEmitter } from "./sessions/bus";
77
+ export { getActor, dropActor } from "./sessions/actor";
78
+ export { interruptSession } from "./sessions/interrupt";
79
+ export { runTurn, writePermissionResponse } from "./sessions/driver";
80
+
81
+ // Queue
82
+ export { enqueueTurn } from "./queue";
83
+
84
+ // Backends
85
+ export { resolveBackend } from "./backends/registry";
86
+
87
+ // Providers
88
+ export { resolveContainerProvider } from "./providers/registry";
89
+
90
+ // Proxy
91
+ export { forwardToAnthropic, validateAnthropicProxy } from "./proxy/forward";
92
+
93
+ // Sprite/lifecycle
94
+ export { releaseSession } from "./sprite/lifecycle";
95
+ export { kickoffEnvironmentSetup } from "./sprite/setup";
96
+
97
+ // OpenAPI
98
+ export { buildOpenApiDocument } from "./openapi/spec";
99
+
100
+ // Config
101
+ export { getConfig } from "./config";
102
+
103
+ // Utils
104
+ export { nowMs } from "./util/clock";
package/src/init.ts ADDED
@@ -0,0 +1,227 @@
1
+ /**
2
+ * One-time service initialization.
3
+ *
4
+ * Runs on first request (any route calls `await ensureInitialized()`):
5
+ * 1. Boot the DB (which runs migrations)
6
+ * 2. Recover stale sessions: any row with status='running' gets a
7
+ * `session.error{type:"server_restart"}` event + flipped to idle.
8
+ * (We do NOT implement true session.status_rescheduled semantics — see
9
+ * plan §I8.)
10
+ * 3. Sprite orphan reconciler: best-effort pruning of old sprites whose
11
+ * sessions no longer exist.
12
+ *
13
+ * Pattern from
14
+ */
15
+ import { getDb } from "./db/client";
16
+ import { createApiKey, listApiKeys } from "./db/api_keys";
17
+ import { getConfig } from "./config";
18
+ import { appendEvent } from "./sessions/bus";
19
+ import { getLastUnprocessedUserMessage } from "./db/events";
20
+ import { runSweep } from "./sessions/sweeper";
21
+ import { getRuntime } from "./state";
22
+ import { runTurn } from "./sessions/driver";
23
+ import { enqueueTurn } from "./queue";
24
+ import { reconcileOrphans, reconcileDockerOrphans } from "./sprite/lifecycle";
25
+ import { installShutdownHandlers } from "./shutdown";
26
+ import { nowMs } from "./util/clock";
27
+ import { resolveContainerProvider } from "./providers/registry";
28
+ import { getEnvironment } from "./db/environments";
29
+ import { setSessionSprite } from "./db/sessions";
30
+ import * as pool from "./sprite/pool";
31
+ import type { SessionRow } from "./types";
32
+
33
+ type GlobalInit = typeof globalThis & {
34
+ __caInitPromise?: Promise<void>;
35
+ __caSweeperHandle?: NodeJS.Timeout;
36
+ };
37
+ const g = globalThis as GlobalInit;
38
+
39
+ export async function ensureInitialized(): Promise<void> {
40
+ if (g.__caInitPromise) return g.__caInitPromise;
41
+ g.__caInitPromise = doInit();
42
+ return g.__caInitPromise;
43
+ }
44
+
45
+ async function doInit(): Promise<void> {
46
+ // 1. Bootstrap DB + migrations
47
+ getDb();
48
+
49
+ // 1b. Auto-seed a default API key if none exist
50
+ seedDefaultApiKey();
51
+
52
+ // 1c. Shutdown handlers
53
+ installShutdownHandlers();
54
+
55
+ // 2. Stale-session recovery
56
+ try {
57
+ await recoverStaleSessions();
58
+ } catch (err) {
59
+ console.error("[init] stale session recovery failed:", err);
60
+ }
61
+
62
+ // 3. Sprite orphan reconcile (best-effort, non-blocking)
63
+ const cfg = getConfig();
64
+ if (cfg.spriteToken) {
65
+ reconcileOrphans()
66
+ .then((r) => {
67
+ if (r.deleted > 0) {
68
+ console.log(`[init] reconciled ${r.deleted} orphan sprites, kept ${r.kept}`);
69
+ }
70
+ })
71
+ .catch((err) => {
72
+ console.warn("[init] orphan reconcile (sprites) failed:", err);
73
+ });
74
+ }
75
+
76
+ // 3b. Docker orphan reconcile (best-effort, non-blocking)
77
+ reconcileDockerOrphans()
78
+ .then((r) => {
79
+ if (r.deleted > 0) {
80
+ console.log(`[init] reconciled ${r.deleted} orphan docker containers, kept ${r.kept}`);
81
+ }
82
+ })
83
+ .catch((err) => {
84
+ console.warn("[init] orphan reconcile (docker) failed:", err);
85
+ });
86
+
87
+ // 4. Install the periodic sweeper (idle eviction + orphan reconcile).
88
+ // HMR caveat: the globalThis guard prevents duplicate timers across dev
89
+ // reloads, but when `next dev` hot-reloads the sweeper module the existing
90
+ // timer keeps firing into the *old* module's closure. Sweeper logic changes
91
+ // in dev require a full server restart to pick up.
92
+ if (!g.__caSweeperHandle) {
93
+ const intervalMs = getConfig().sweeperIntervalMs;
94
+ g.__caSweeperHandle = setInterval(() => {
95
+ void runSweep();
96
+ }, intervalMs);
97
+ }
98
+ }
99
+
100
+ function seedDefaultApiKey(): void {
101
+ try {
102
+ const keys = listApiKeys();
103
+ if (keys.length > 0) return;
104
+
105
+ // If SEED_API_KEY is set (e.g. via Secret Manager in Cloud Run),
106
+ // use it instead of generating a random key.
107
+ const seedKey = process.env.SEED_API_KEY;
108
+ if (seedKey) {
109
+ const { id } = createApiKey({ name: "default", permissions: ["*"], rawKey: seedKey });
110
+ console.log(`[init] created API key from SEED_API_KEY (id: ${id})`);
111
+ return;
112
+ }
113
+
114
+ const { key, id } = createApiKey({ name: "default", permissions: ["*"] });
115
+ console.log("[init] created default API key (save this — shown once):");
116
+ console.log(` id: ${id}`);
117
+ console.log(` key: ${key}`);
118
+ } catch (err) {
119
+ console.error("[init] failed to seed default API key:", err);
120
+ }
121
+ }
122
+
123
+ export async function recoverStaleSessions(): Promise<void> {
124
+ const db = getDb();
125
+ const rows = db
126
+ .prepare(
127
+ `SELECT * FROM sessions WHERE status = 'running' AND archived_at IS NULL`,
128
+ )
129
+ .all() as SessionRow[];
130
+
131
+ if (rows.length === 0) return;
132
+ console.log(`[init] recovering ${rows.length} stale running session(s)`);
133
+
134
+ const rt = getRuntime();
135
+ for (const row of rows) {
136
+ try {
137
+ // Try to reschedule: find the last unprocessed user.message
138
+ const lastMsg = getLastUnprocessedUserMessage(row.id);
139
+ if (lastMsg) {
140
+ // If the session had a sprite, verify the container still exists
141
+ if (row.sprite_name) {
142
+ const envObj = getEnvironment(row.environment_id);
143
+ const provider = await resolveContainerProvider(envObj?.config?.provider);
144
+ try {
145
+ const containers = await provider.list({ prefix: row.sprite_name });
146
+ const alive = containers.some((c) => c.name === row.sprite_name);
147
+ if (!alive) {
148
+ console.warn(`[init] sprite ${row.sprite_name} for session ${row.id} no longer exists, clearing`);
149
+ setSessionSprite(row.id, null);
150
+ } else {
151
+ // Re-register in the in-memory pool so lifecycle/sweeper can see it
152
+ pool.register({
153
+ spriteName: row.sprite_name,
154
+ envId: row.environment_id,
155
+ sessionId: row.id,
156
+ createdAt: nowMs(),
157
+ });
158
+ }
159
+ } catch (err) {
160
+ console.warn(`[init] container health check failed for ${row.sprite_name}, clearing:`, err);
161
+ setSessionSprite(row.id, null);
162
+ }
163
+ }
164
+
165
+ // Emit rescheduled event
166
+ appendEvent(row.id, {
167
+ type: "session.status_rescheduled",
168
+ payload: {},
169
+ origin: "server",
170
+ processedAt: nowMs(),
171
+ });
172
+
173
+ // Flip status to idle so the turn can restart
174
+ db.prepare(
175
+ `UPDATE sessions SET status = 'idle', stop_reason = 'rescheduled', updated_at = ? WHERE id = ?`,
176
+ ).run(nowMs(), row.id);
177
+ rt.inFlightRuns.delete(row.id);
178
+
179
+ // Extract the text from the user.message payload
180
+ const payload = JSON.parse(lastMsg.payload_json) as { content?: Array<{ type: string; text?: string }> };
181
+ const text = (payload.content ?? [])
182
+ .filter((b) => b.type === "text" && b.text)
183
+ .map((b) => b.text!)
184
+ .join("");
185
+
186
+ // Fire-and-forget: re-enqueue the turn
187
+ void enqueueTurn(row.environment_id, () =>
188
+ runTurn(row.id, [{ kind: "text", eventId: lastMsg.id, text }]),
189
+ ).catch((err: unknown) => {
190
+ console.error(`[init] reschedule turn failed for ${row.id}:`, err);
191
+ });
192
+
193
+ console.log(`[init] rescheduled session ${row.id}`);
194
+ continue;
195
+ }
196
+ } catch (err) {
197
+ console.warn(`[init] reschedule attempt failed for ${row.id}, falling back to error:`, err);
198
+ }
199
+
200
+ // Fallback: emit error + idle
201
+ try {
202
+ appendEvent(row.id, {
203
+ type: "session.error",
204
+ payload: {
205
+ error: {
206
+ type: "server_restart",
207
+ message: "server restarted while turn was running",
208
+ },
209
+ },
210
+ origin: "server",
211
+ processedAt: nowMs(),
212
+ });
213
+ appendEvent(row.id, {
214
+ type: "session.status_idle",
215
+ payload: { stop_reason: "error" },
216
+ origin: "server",
217
+ processedAt: nowMs(),
218
+ });
219
+ db.prepare(
220
+ `UPDATE sessions SET status = 'idle', stop_reason = 'error', updated_at = ? WHERE id = ?`,
221
+ ).run(nowMs(), row.id);
222
+ rt.inFlightRuns.delete(row.id);
223
+ } catch (err) {
224
+ console.error(`[init] failed to recover session ${row.id}:`, err);
225
+ }
226
+ }
227
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Singleton OpenAPIRegistry shared across the schema module and the spec
3
+ * builder. Kept in its own file so `schemas.ts` and `spec.ts` can both
4
+ * import it without a circular dependency.
5
+ */
6
+ import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
7
+
8
+ export const registry = new OpenAPIRegistry();