@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.
- package/package.json +45 -0
- package/src/auth/middleware.ts +38 -0
- package/src/backends/claude/args.ts +88 -0
- package/src/backends/claude/index.ts +193 -0
- package/src/backends/claude/permission-hook.ts +152 -0
- package/src/backends/claude/tool-bridge.ts +211 -0
- package/src/backends/claude/translator.ts +209 -0
- package/src/backends/claude/wrapper-script.ts +45 -0
- package/src/backends/codex/args.ts +69 -0
- package/src/backends/codex/auth.ts +35 -0
- package/src/backends/codex/index.ts +57 -0
- package/src/backends/codex/setup.ts +37 -0
- package/src/backends/codex/translator.ts +223 -0
- package/src/backends/codex/wrapper-script.ts +26 -0
- package/src/backends/factory/args.ts +45 -0
- package/src/backends/factory/auth.ts +30 -0
- package/src/backends/factory/index.ts +56 -0
- package/src/backends/factory/setup.ts +34 -0
- package/src/backends/factory/translator.ts +139 -0
- package/src/backends/factory/wrapper-script.ts +33 -0
- package/src/backends/gemini/args.ts +44 -0
- package/src/backends/gemini/auth.ts +30 -0
- package/src/backends/gemini/index.ts +53 -0
- package/src/backends/gemini/setup.ts +34 -0
- package/src/backends/gemini/translator.ts +139 -0
- package/src/backends/gemini/wrapper-script.ts +26 -0
- package/src/backends/opencode/args.ts +53 -0
- package/src/backends/opencode/auth.ts +53 -0
- package/src/backends/opencode/index.ts +70 -0
- package/src/backends/opencode/mcp.ts +67 -0
- package/src/backends/opencode/setup.ts +54 -0
- package/src/backends/opencode/translator.ts +168 -0
- package/src/backends/opencode/wrapper-script.ts +46 -0
- package/src/backends/registry.ts +38 -0
- package/src/backends/shared/ndjson.ts +29 -0
- package/src/backends/shared/translator-types.ts +69 -0
- package/src/backends/shared/wrap-prompt.ts +17 -0
- package/src/backends/types.ts +85 -0
- package/src/config/index.ts +95 -0
- package/src/db/agents.ts +185 -0
- package/src/db/api_keys.ts +78 -0
- package/src/db/batch.ts +142 -0
- package/src/db/client.ts +81 -0
- package/src/db/environments.ts +127 -0
- package/src/db/events.ts +208 -0
- package/src/db/memory.ts +143 -0
- package/src/db/migrations.ts +295 -0
- package/src/db/proxy.ts +37 -0
- package/src/db/sessions.ts +295 -0
- package/src/db/vaults.ts +110 -0
- package/src/errors.ts +53 -0
- package/src/handlers/agents.ts +194 -0
- package/src/handlers/batch.ts +41 -0
- package/src/handlers/docs.ts +87 -0
- package/src/handlers/environments.ts +154 -0
- package/src/handlers/events.ts +234 -0
- package/src/handlers/index.ts +12 -0
- package/src/handlers/memory.ts +141 -0
- package/src/handlers/openapi.ts +14 -0
- package/src/handlers/sessions.ts +223 -0
- package/src/handlers/stream.ts +76 -0
- package/src/handlers/threads.ts +26 -0
- package/src/handlers/ui/app.js +984 -0
- package/src/handlers/ui/index.html +112 -0
- package/src/handlers/ui/style.css +164 -0
- package/src/handlers/ui.ts +1281 -0
- package/src/handlers/vaults.ts +99 -0
- package/src/http.ts +35 -0
- package/src/index.ts +104 -0
- package/src/init.ts +227 -0
- package/src/openapi/registry.ts +8 -0
- package/src/openapi/schemas.ts +625 -0
- package/src/openapi/spec.ts +691 -0
- package/src/providers/apple.ts +220 -0
- package/src/providers/daytona.ts +217 -0
- package/src/providers/docker.ts +264 -0
- package/src/providers/e2b.ts +203 -0
- package/src/providers/fly.ts +276 -0
- package/src/providers/modal.ts +222 -0
- package/src/providers/podman.ts +206 -0
- package/src/providers/registry.ts +28 -0
- package/src/providers/shared.ts +11 -0
- package/src/providers/sprites.ts +55 -0
- package/src/providers/types.ts +73 -0
- package/src/providers/vercel.ts +208 -0
- package/src/proxy/forward.ts +111 -0
- package/src/queue/index.ts +111 -0
- package/src/sessions/actor.ts +53 -0
- package/src/sessions/bus.ts +155 -0
- package/src/sessions/driver.ts +818 -0
- package/src/sessions/grader.ts +120 -0
- package/src/sessions/interrupt.ts +14 -0
- package/src/sessions/sweeper.ts +136 -0
- package/src/sessions/threads.ts +126 -0
- package/src/sessions/tools.ts +50 -0
- package/src/shutdown.ts +78 -0
- package/src/sprite/client.ts +294 -0
- package/src/sprite/exec.ts +161 -0
- package/src/sprite/lifecycle.ts +339 -0
- package/src/sprite/pool.ts +65 -0
- package/src/sprite/setup.ts +159 -0
- package/src/state.ts +61 -0
- package/src/types.ts +339 -0
- package/src/util/clock.ts +7 -0
- 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();
|