@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,295 @@
|
|
|
1
|
+
import { getDb } from "./client";
|
|
2
|
+
import { newId } from "../util/ids";
|
|
3
|
+
import { nowMs, toIso } from "../util/clock";
|
|
4
|
+
import type { Session, SessionResource, SessionRow, SessionStatus } from "../types";
|
|
5
|
+
|
|
6
|
+
export function hydrateSession(row: SessionRow): Session {
|
|
7
|
+
return {
|
|
8
|
+
id: row.id,
|
|
9
|
+
agent: { id: row.agent_id, version: row.agent_version },
|
|
10
|
+
environment_id: row.environment_id,
|
|
11
|
+
status: row.status,
|
|
12
|
+
stop_reason: row.stop_reason,
|
|
13
|
+
title: row.title,
|
|
14
|
+
metadata: JSON.parse(row.metadata_json) as Record<string, unknown>,
|
|
15
|
+
max_budget_usd: row.max_budget_usd ?? null,
|
|
16
|
+
outcome: row.outcome_criteria_json ? (JSON.parse(row.outcome_criteria_json) as Record<string, unknown>) : null,
|
|
17
|
+
resources: row.resources_json ? (JSON.parse(row.resources_json) as SessionResource[]) : null,
|
|
18
|
+
vault_ids: row.vault_ids_json ? (JSON.parse(row.vault_ids_json) as string[]) : null,
|
|
19
|
+
parent_session_id: row.parent_session_id ?? null,
|
|
20
|
+
thread_depth: row.thread_depth ?? 0,
|
|
21
|
+
stats: {
|
|
22
|
+
turn_count: row.turn_count,
|
|
23
|
+
tool_calls_count: row.tool_calls_count,
|
|
24
|
+
active_seconds: row.active_seconds,
|
|
25
|
+
duration_seconds: row.duration_seconds,
|
|
26
|
+
},
|
|
27
|
+
usage: {
|
|
28
|
+
input_tokens: row.usage_input_tokens,
|
|
29
|
+
output_tokens: row.usage_output_tokens,
|
|
30
|
+
cache_read_input_tokens: row.usage_cache_read_input_tokens,
|
|
31
|
+
cache_creation_input_tokens: row.usage_cache_creation_input_tokens,
|
|
32
|
+
cost_usd: row.usage_cost_usd,
|
|
33
|
+
},
|
|
34
|
+
created_at: toIso(row.created_at),
|
|
35
|
+
updated_at: toIso(row.updated_at),
|
|
36
|
+
archived_at: row.archived_at ? toIso(row.archived_at) : null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createSession(input: {
|
|
41
|
+
agent_id: string;
|
|
42
|
+
agent_version: number;
|
|
43
|
+
environment_id: string;
|
|
44
|
+
title?: string | null;
|
|
45
|
+
metadata?: Record<string, unknown>;
|
|
46
|
+
max_budget_usd?: number | null;
|
|
47
|
+
resources?: SessionResource[] | null;
|
|
48
|
+
vault_ids?: string[] | null;
|
|
49
|
+
parent_session_id?: string | null;
|
|
50
|
+
thread_depth?: number;
|
|
51
|
+
}): Session {
|
|
52
|
+
const db = getDb();
|
|
53
|
+
const id = newId("sess");
|
|
54
|
+
const now = nowMs();
|
|
55
|
+
|
|
56
|
+
db.prepare(
|
|
57
|
+
`INSERT INTO sessions (
|
|
58
|
+
id, agent_id, agent_version, environment_id, status,
|
|
59
|
+
title, metadata_json, max_budget_usd, resources_json,
|
|
60
|
+
vault_ids_json, parent_session_id, thread_depth,
|
|
61
|
+
created_at, updated_at
|
|
62
|
+
) VALUES (?, ?, ?, ?, 'idle', ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
63
|
+
).run(
|
|
64
|
+
id,
|
|
65
|
+
input.agent_id,
|
|
66
|
+
input.agent_version,
|
|
67
|
+
input.environment_id,
|
|
68
|
+
input.title ?? null,
|
|
69
|
+
JSON.stringify(input.metadata ?? {}),
|
|
70
|
+
input.max_budget_usd ?? null,
|
|
71
|
+
input.resources ? JSON.stringify(input.resources) : null,
|
|
72
|
+
input.vault_ids ? JSON.stringify(input.vault_ids) : null,
|
|
73
|
+
input.parent_session_id ?? null,
|
|
74
|
+
input.thread_depth ?? 0,
|
|
75
|
+
now,
|
|
76
|
+
now,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return getSession(id)!;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getSession(id: string): Session | null {
|
|
83
|
+
const row = getSessionRow(id);
|
|
84
|
+
return row ? hydrateSession(row) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getSessionRow(id: string): SessionRow | null {
|
|
88
|
+
const db = getDb();
|
|
89
|
+
return (
|
|
90
|
+
(db
|
|
91
|
+
.prepare(`SELECT * FROM sessions WHERE id = ?`)
|
|
92
|
+
.get(id) as SessionRow | undefined) ?? null
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function updateSessionStatus(
|
|
97
|
+
id: string,
|
|
98
|
+
status: SessionStatus,
|
|
99
|
+
stopReason?: string | null,
|
|
100
|
+
): void {
|
|
101
|
+
const db = getDb();
|
|
102
|
+
db.prepare(
|
|
103
|
+
`UPDATE sessions SET status = ?, stop_reason = ?, updated_at = ? WHERE id = ?`,
|
|
104
|
+
).run(status, stopReason ?? null, nowMs(), id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function setSessionSprite(id: string, spriteName: string | null): void {
|
|
108
|
+
const db = getDb();
|
|
109
|
+
db.prepare(
|
|
110
|
+
`UPDATE sessions SET sprite_name = ?, updated_at = ? WHERE id = ?`,
|
|
111
|
+
).run(spriteName, nowMs(), id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Store the backend's session id for subsequent resume turns. The DB column
|
|
116
|
+
* is named `claude_session_id` for historical reasons but holds any
|
|
117
|
+
* backend's session id (claude's `session_id`, opencode's `sessionID`).
|
|
118
|
+
*/
|
|
119
|
+
export function setBackendSessionId(id: string, backendSessionId: string): void {
|
|
120
|
+
const db = getDb();
|
|
121
|
+
db.prepare(
|
|
122
|
+
`UPDATE sessions SET claude_session_id = ?, updated_at = ? WHERE id = ?`,
|
|
123
|
+
).run(backendSessionId, nowMs(), id);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function setSessionProvider(id: string, providerName: string): void {
|
|
127
|
+
const db = getDb();
|
|
128
|
+
db.prepare(
|
|
129
|
+
`UPDATE sessions SET provider_name = ?, updated_at = ? WHERE id = ?`,
|
|
130
|
+
).run(providerName, nowMs(), id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function setIdleSince(id: string, idleSince: number | null): void {
|
|
134
|
+
const db = getDb();
|
|
135
|
+
db.prepare(`UPDATE sessions SET idle_since = ? WHERE id = ?`).run(idleSince, id);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function updateSessionMutable(
|
|
139
|
+
id: string,
|
|
140
|
+
input: { title?: string | null; metadata?: Record<string, unknown> },
|
|
141
|
+
): Session | null {
|
|
142
|
+
const db = getDb();
|
|
143
|
+
const existing = getSession(id);
|
|
144
|
+
if (!existing) return null;
|
|
145
|
+
|
|
146
|
+
db.prepare(
|
|
147
|
+
`UPDATE sessions SET title = ?, metadata_json = ?, updated_at = ? WHERE id = ?`,
|
|
148
|
+
).run(
|
|
149
|
+
input.title ?? existing.title,
|
|
150
|
+
JSON.stringify(input.metadata ?? existing.metadata),
|
|
151
|
+
nowMs(),
|
|
152
|
+
id,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return getSession(id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface UsageDelta {
|
|
159
|
+
input_tokens?: number;
|
|
160
|
+
output_tokens?: number;
|
|
161
|
+
cache_read_input_tokens?: number;
|
|
162
|
+
cache_creation_input_tokens?: number;
|
|
163
|
+
cost_usd?: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function bumpSessionStats(
|
|
167
|
+
id: string,
|
|
168
|
+
delta: { turn_count?: number; tool_calls_count?: number; duration_seconds?: number; active_seconds?: number },
|
|
169
|
+
usage?: UsageDelta,
|
|
170
|
+
): void {
|
|
171
|
+
const db = getDb();
|
|
172
|
+
db.prepare(
|
|
173
|
+
`UPDATE sessions SET
|
|
174
|
+
turn_count = turn_count + ?,
|
|
175
|
+
tool_calls_count = tool_calls_count + ?,
|
|
176
|
+
active_seconds = active_seconds + ?,
|
|
177
|
+
duration_seconds = duration_seconds + ?,
|
|
178
|
+
usage_input_tokens = usage_input_tokens + ?,
|
|
179
|
+
usage_output_tokens = usage_output_tokens + ?,
|
|
180
|
+
usage_cache_read_input_tokens = usage_cache_read_input_tokens + ?,
|
|
181
|
+
usage_cache_creation_input_tokens = usage_cache_creation_input_tokens + ?,
|
|
182
|
+
usage_cost_usd = usage_cost_usd + ?,
|
|
183
|
+
updated_at = ?
|
|
184
|
+
WHERE id = ?`,
|
|
185
|
+
).run(
|
|
186
|
+
delta.turn_count ?? 0,
|
|
187
|
+
delta.tool_calls_count ?? 0,
|
|
188
|
+
delta.active_seconds ?? 0,
|
|
189
|
+
delta.duration_seconds ?? 0,
|
|
190
|
+
usage?.input_tokens ?? 0,
|
|
191
|
+
usage?.output_tokens ?? 0,
|
|
192
|
+
usage?.cache_read_input_tokens ?? 0,
|
|
193
|
+
usage?.cache_creation_input_tokens ?? 0,
|
|
194
|
+
usage?.cost_usd ?? 0,
|
|
195
|
+
nowMs(),
|
|
196
|
+
id,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function archiveSession(id: string): boolean {
|
|
201
|
+
const db = getDb();
|
|
202
|
+
const res = db
|
|
203
|
+
.prepare(`UPDATE sessions SET archived_at = ?, updated_at = ? WHERE id = ? AND archived_at IS NULL`)
|
|
204
|
+
.run(nowMs(), nowMs(), id);
|
|
205
|
+
return res.changes > 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function listSessions(opts: {
|
|
209
|
+
agent_id?: string;
|
|
210
|
+
agent_version?: number;
|
|
211
|
+
environmentId?: string;
|
|
212
|
+
parent_session_id?: string;
|
|
213
|
+
status?: SessionStatus;
|
|
214
|
+
limit?: number;
|
|
215
|
+
order?: "asc" | "desc";
|
|
216
|
+
includeArchived?: boolean;
|
|
217
|
+
cursor?: string;
|
|
218
|
+
createdGt?: number;
|
|
219
|
+
createdGte?: number;
|
|
220
|
+
createdLt?: number;
|
|
221
|
+
createdLte?: number;
|
|
222
|
+
}): Session[] {
|
|
223
|
+
const db = getDb();
|
|
224
|
+
const limit = Math.min(Math.max(opts.limit ?? 20, 1), 100);
|
|
225
|
+
const order = opts.order === "asc" ? "ASC" : "DESC";
|
|
226
|
+
const includeArchived = opts.includeArchived ?? false;
|
|
227
|
+
|
|
228
|
+
const clauses: string[] = [];
|
|
229
|
+
const params: unknown[] = [];
|
|
230
|
+
|
|
231
|
+
if (opts.agent_id) {
|
|
232
|
+
clauses.push("agent_id = ?");
|
|
233
|
+
params.push(opts.agent_id);
|
|
234
|
+
}
|
|
235
|
+
if (opts.agent_version != null) {
|
|
236
|
+
clauses.push("agent_version = ?");
|
|
237
|
+
params.push(opts.agent_version);
|
|
238
|
+
}
|
|
239
|
+
if (opts.environmentId) {
|
|
240
|
+
clauses.push("environment_id = ?");
|
|
241
|
+
params.push(opts.environmentId);
|
|
242
|
+
}
|
|
243
|
+
if (opts.parent_session_id) {
|
|
244
|
+
clauses.push("parent_session_id = ?");
|
|
245
|
+
params.push(opts.parent_session_id);
|
|
246
|
+
}
|
|
247
|
+
if (opts.status) {
|
|
248
|
+
clauses.push("status = ?");
|
|
249
|
+
params.push(opts.status);
|
|
250
|
+
}
|
|
251
|
+
if (!includeArchived) clauses.push("archived_at IS NULL");
|
|
252
|
+
if (opts.createdGt != null) {
|
|
253
|
+
clauses.push("created_at > ?");
|
|
254
|
+
params.push(opts.createdGt);
|
|
255
|
+
}
|
|
256
|
+
if (opts.createdGte != null) {
|
|
257
|
+
clauses.push("created_at >= ?");
|
|
258
|
+
params.push(opts.createdGte);
|
|
259
|
+
}
|
|
260
|
+
if (opts.createdLt != null) {
|
|
261
|
+
clauses.push("created_at < ?");
|
|
262
|
+
params.push(opts.createdLt);
|
|
263
|
+
}
|
|
264
|
+
if (opts.createdLte != null) {
|
|
265
|
+
clauses.push("created_at <= ?");
|
|
266
|
+
params.push(opts.createdLte);
|
|
267
|
+
}
|
|
268
|
+
if (opts.cursor) {
|
|
269
|
+
clauses.push(order === "DESC" ? "id < ?" : "id > ?");
|
|
270
|
+
params.push(opts.cursor);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
274
|
+
const rows = db
|
|
275
|
+
.prepare(
|
|
276
|
+
`SELECT * FROM sessions ${where} ORDER BY id ${order} LIMIT ?`,
|
|
277
|
+
)
|
|
278
|
+
.all(...params, limit) as SessionRow[];
|
|
279
|
+
|
|
280
|
+
return rows.map(hydrateSession);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function setOutcomeCriteria(id: string, criteria: Record<string, unknown>): void {
|
|
284
|
+
const db = getDb();
|
|
285
|
+
db.prepare(`UPDATE sessions SET outcome_criteria_json = ?, updated_at = ? WHERE id = ?`).run(
|
|
286
|
+
JSON.stringify(criteria),
|
|
287
|
+
nowMs(),
|
|
288
|
+
id,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function getOutcomeCriteria(id: string): Record<string, unknown> | null {
|
|
293
|
+
const row = getSessionRow(id);
|
|
294
|
+
return row?.outcome_criteria_json ? (JSON.parse(row.outcome_criteria_json) as Record<string, unknown>) : null;
|
|
295
|
+
}
|
package/src/db/vaults.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault persistence: SQLite-backed key-value stores scoped to agents.
|
|
3
|
+
*
|
|
4
|
+
* Data persists across sessions. When `vault_ids` is set on a session,
|
|
5
|
+
* the driver provisions vault data into the container at turn start.
|
|
6
|
+
*/
|
|
7
|
+
import { getDb } from "./client";
|
|
8
|
+
import { newId } from "../util/ids";
|
|
9
|
+
import { nowMs, toIso } from "../util/clock";
|
|
10
|
+
import type { Vault, VaultEntry, VaultEntryRow, VaultRow } from "../types";
|
|
11
|
+
|
|
12
|
+
function hydrateVault(row: VaultRow): Vault {
|
|
13
|
+
return {
|
|
14
|
+
id: row.id,
|
|
15
|
+
agent_id: row.agent_id,
|
|
16
|
+
name: row.name,
|
|
17
|
+
created_at: toIso(row.created_at),
|
|
18
|
+
updated_at: toIso(row.updated_at),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createVault(input: {
|
|
23
|
+
agent_id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
}): Vault {
|
|
26
|
+
const db = getDb();
|
|
27
|
+
const id = newId("vault");
|
|
28
|
+
const now = nowMs();
|
|
29
|
+
|
|
30
|
+
db.prepare(
|
|
31
|
+
`INSERT INTO vaults (id, agent_id, name, created_at, updated_at)
|
|
32
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
33
|
+
).run(id, input.agent_id, input.name, now, now);
|
|
34
|
+
|
|
35
|
+
return getVault(id)!;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getVault(id: string): Vault | null {
|
|
39
|
+
const db = getDb();
|
|
40
|
+
const row = db
|
|
41
|
+
.prepare(`SELECT * FROM vaults WHERE id = ?`)
|
|
42
|
+
.get(id) as VaultRow | undefined;
|
|
43
|
+
return row ? hydrateVault(row) : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function listVaults(opts: { agent_id?: string }): Vault[] {
|
|
47
|
+
const db = getDb();
|
|
48
|
+
if (opts.agent_id) {
|
|
49
|
+
const rows = db
|
|
50
|
+
.prepare(
|
|
51
|
+
`SELECT * FROM vaults WHERE agent_id = ? ORDER BY created_at DESC`,
|
|
52
|
+
)
|
|
53
|
+
.all(opts.agent_id) as VaultRow[];
|
|
54
|
+
return rows.map(hydrateVault);
|
|
55
|
+
}
|
|
56
|
+
const rows = db
|
|
57
|
+
.prepare(`SELECT * FROM vaults ORDER BY created_at DESC`)
|
|
58
|
+
.all() as VaultRow[];
|
|
59
|
+
return rows.map(hydrateVault);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function deleteVault(id: string): boolean {
|
|
63
|
+
const db = getDb();
|
|
64
|
+
const res = db.prepare(`DELETE FROM vaults WHERE id = ?`).run(id);
|
|
65
|
+
return res.changes > 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function setEntry(vaultId: string, key: string, value: string): void {
|
|
69
|
+
const db = getDb();
|
|
70
|
+
const now = nowMs();
|
|
71
|
+
db.prepare(
|
|
72
|
+
`INSERT INTO vault_entries (vault_id, key, value, updated_at)
|
|
73
|
+
VALUES (?, ?, ?, ?)
|
|
74
|
+
ON CONFLICT(vault_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
|
75
|
+
).run(vaultId, key, value, now);
|
|
76
|
+
|
|
77
|
+
// Update vault's updated_at timestamp
|
|
78
|
+
db.prepare(`UPDATE vaults SET updated_at = ? WHERE id = ?`).run(now, vaultId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getEntry(
|
|
82
|
+
vaultId: string,
|
|
83
|
+
key: string,
|
|
84
|
+
): VaultEntry | null {
|
|
85
|
+
const db = getDb();
|
|
86
|
+
const row = db
|
|
87
|
+
.prepare(
|
|
88
|
+
`SELECT * FROM vault_entries WHERE vault_id = ? AND key = ?`,
|
|
89
|
+
)
|
|
90
|
+
.get(vaultId, key) as VaultEntryRow | undefined;
|
|
91
|
+
return row ? { key: row.key, value: row.value } : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function listEntries(vaultId: string): VaultEntry[] {
|
|
95
|
+
const db = getDb();
|
|
96
|
+
const rows = db
|
|
97
|
+
.prepare(
|
|
98
|
+
`SELECT * FROM vault_entries WHERE vault_id = ? ORDER BY key ASC`,
|
|
99
|
+
)
|
|
100
|
+
.all(vaultId) as VaultEntryRow[];
|
|
101
|
+
return rows.map((r) => ({ key: r.key, value: r.value }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function deleteEntry(vaultId: string, key: string): boolean {
|
|
105
|
+
const db = getDb();
|
|
106
|
+
const res = db
|
|
107
|
+
.prepare(`DELETE FROM vault_entries WHERE vault_id = ? AND key = ?`)
|
|
108
|
+
.run(vaultId, key);
|
|
109
|
+
return res.changes > 0;
|
|
110
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed Agents error envelope + typed HTTP error responses.
|
|
3
|
+
*
|
|
4
|
+
* Shape:
|
|
5
|
+
* { "type": "error", "error": { "type": "...", "message": "..." } }
|
|
6
|
+
*
|
|
7
|
+
* Uses Web Standard Response — no framework dependency.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ErrorType =
|
|
11
|
+
| "invalid_request_error"
|
|
12
|
+
| "authentication_error"
|
|
13
|
+
| "permission_error"
|
|
14
|
+
| "not_found_error"
|
|
15
|
+
| "rate_limit_error"
|
|
16
|
+
| "server_busy"
|
|
17
|
+
| "server_error";
|
|
18
|
+
|
|
19
|
+
export class ApiError extends Error {
|
|
20
|
+
constructor(
|
|
21
|
+
public status: number,
|
|
22
|
+
public type: ErrorType,
|
|
23
|
+
message: string,
|
|
24
|
+
) {
|
|
25
|
+
super(message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function envelope(type: ErrorType, message: string) {
|
|
30
|
+
return { type: "error", error: { type, message } };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function toResponse(err: unknown): Response {
|
|
34
|
+
if (err instanceof ApiError) {
|
|
35
|
+
return Response.json(envelope(err.type, err.message), { status: err.status });
|
|
36
|
+
}
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38
|
+
console.error("[error] unhandled:", msg);
|
|
39
|
+
return Response.json(envelope("server_error", "internal server error"), { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Convenience constructors
|
|
43
|
+
|
|
44
|
+
export const badRequest = (msg: string) => new ApiError(400, "invalid_request_error", msg);
|
|
45
|
+
export const unauthorized = (msg = "missing or invalid API key") =>
|
|
46
|
+
new ApiError(401, "authentication_error", msg);
|
|
47
|
+
export const forbidden = (msg: string) => new ApiError(403, "permission_error", msg);
|
|
48
|
+
export const notFound = (msg: string) => new ApiError(404, "not_found_error", msg);
|
|
49
|
+
export const conflict = (msg: string) => new ApiError(409, "invalid_request_error", msg);
|
|
50
|
+
export const tooManyRequests = (msg = "rate limit exceeded") =>
|
|
51
|
+
new ApiError(429, "rate_limit_error", msg);
|
|
52
|
+
export const serverBusy = (msg = "server is at capacity") =>
|
|
53
|
+
new ApiError(503, "server_busy", msg);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { routeWrap, jsonOk } from "../http";
|
|
3
|
+
import { createAgent, getAgent, updateAgent, archiveAgent, listAgents } from "../db/agents";
|
|
4
|
+
import { resolveBackend } from "../backends/registry";
|
|
5
|
+
import { isProxied, markProxied, unmarkProxied } from "../db/proxy";
|
|
6
|
+
import { forwardToAnthropic, validateAnthropicProxy } from "../proxy/forward";
|
|
7
|
+
import { badRequest, notFound } from "../errors";
|
|
8
|
+
|
|
9
|
+
const ToolSchema = z.union([
|
|
10
|
+
z.object({
|
|
11
|
+
type: z.literal("agent_toolset_20260401"),
|
|
12
|
+
configs: z
|
|
13
|
+
.array(z.object({ name: z.string(), enabled: z.boolean().optional() }))
|
|
14
|
+
.optional(),
|
|
15
|
+
default_config: z.object({ enabled: z.boolean().optional() }).optional(),
|
|
16
|
+
}),
|
|
17
|
+
z.object({
|
|
18
|
+
type: z.literal("custom"),
|
|
19
|
+
name: z.string().min(1),
|
|
20
|
+
description: z.string(),
|
|
21
|
+
input_schema: z.record(z.unknown()),
|
|
22
|
+
}),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const McpServerSchema = z.record(
|
|
26
|
+
z.object({
|
|
27
|
+
type: z.enum(["stdio", "http", "sse"]).optional(),
|
|
28
|
+
url: z.string().optional(),
|
|
29
|
+
command: z.union([z.string(), z.array(z.string())]).optional(),
|
|
30
|
+
args: z.array(z.string()).optional(),
|
|
31
|
+
headers: z.record(z.string()).optional(),
|
|
32
|
+
env: z.record(z.string()).optional(),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const CreateSchema = z.object({
|
|
37
|
+
name: z.string().min(1),
|
|
38
|
+
model: z.string().min(1),
|
|
39
|
+
system: z.string().nullish(),
|
|
40
|
+
tools: z.array(ToolSchema).optional(),
|
|
41
|
+
mcp_servers: McpServerSchema.optional(),
|
|
42
|
+
backend: z.enum(["claude", "opencode", "codex", "anthropic", "gemini", "factory"]).optional(),
|
|
43
|
+
webhook_url: z.string().url().optional(),
|
|
44
|
+
webhook_events: z.array(z.string()).optional(),
|
|
45
|
+
threads_enabled: z.boolean().optional(),
|
|
46
|
+
confirmation_mode: z.boolean().optional(),
|
|
47
|
+
callable_agents: z.array(z.object({
|
|
48
|
+
type: z.literal("agent"),
|
|
49
|
+
id: z.string(),
|
|
50
|
+
version: z.number().int().optional(),
|
|
51
|
+
})).optional(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const UpdateSchema = z.object({
|
|
55
|
+
name: z.string().min(1).optional(),
|
|
56
|
+
model: z.string().min(1).optional(),
|
|
57
|
+
system: z.string().nullish(),
|
|
58
|
+
tools: z.array(z.unknown()).optional(),
|
|
59
|
+
mcp_servers: z.record(z.unknown()).optional(),
|
|
60
|
+
webhook_url: z.string().url().nullish(),
|
|
61
|
+
webhook_events: z.array(z.string()).optional(),
|
|
62
|
+
confirmation_mode: z.boolean().optional(),
|
|
63
|
+
callable_agents: z.array(z.object({
|
|
64
|
+
type: z.literal("agent"),
|
|
65
|
+
id: z.string(),
|
|
66
|
+
version: z.number().int().optional(),
|
|
67
|
+
})).optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export function handleCreateAgent(request: Request): Promise<Response> {
|
|
71
|
+
return routeWrap(request, async () => {
|
|
72
|
+
const rawBody = await request.text();
|
|
73
|
+
const body = rawBody ? JSON.parse(rawBody) : null;
|
|
74
|
+
const parsed = CreateSchema.safeParse(body);
|
|
75
|
+
if (!parsed.success) {
|
|
76
|
+
throw badRequest(`invalid body: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const backendName = parsed.data.backend ?? "claude";
|
|
80
|
+
|
|
81
|
+
if (backendName === "anthropic") {
|
|
82
|
+
const proxyErr = validateAnthropicProxy();
|
|
83
|
+
if (proxyErr) throw badRequest(proxyErr);
|
|
84
|
+
const proxyRes = await forwardToAnthropic(request, "/v1/agents", { body: rawBody });
|
|
85
|
+
if (proxyRes.ok) {
|
|
86
|
+
try {
|
|
87
|
+
const data = (await proxyRes.clone().json()) as { id: string };
|
|
88
|
+
markProxied(data.id, "agent");
|
|
89
|
+
} catch { /* best-effort */ }
|
|
90
|
+
}
|
|
91
|
+
return proxyRes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const backend = resolveBackend(backendName);
|
|
95
|
+
|
|
96
|
+
if (backendName !== "claude" && parsed.data.tools && parsed.data.tools.length > 0) {
|
|
97
|
+
throw badRequest(
|
|
98
|
+
`${backendName} backend does not use agent tool configs; tools are managed by the backend's internal permission system. Omit the tools field for ${backendName} agents.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const createErr = backend.validateAgentCreation?.();
|
|
103
|
+
if (createErr) throw badRequest(createErr);
|
|
104
|
+
|
|
105
|
+
const agent = createAgent({
|
|
106
|
+
name: parsed.data.name,
|
|
107
|
+
model: parsed.data.model,
|
|
108
|
+
system: parsed.data.system ?? null,
|
|
109
|
+
tools: parsed.data.tools ?? [],
|
|
110
|
+
mcp_servers: parsed.data.mcp_servers ?? {},
|
|
111
|
+
backend: backendName,
|
|
112
|
+
webhook_url: parsed.data.webhook_url ?? null,
|
|
113
|
+
webhook_events: parsed.data.webhook_events,
|
|
114
|
+
threads_enabled: parsed.data.threads_enabled ?? false,
|
|
115
|
+
confirmation_mode: parsed.data.confirmation_mode ?? false,
|
|
116
|
+
callable_agents: parsed.data.callable_agents,
|
|
117
|
+
});
|
|
118
|
+
return jsonOk(agent, 201);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function handleListAgents(request: Request): Promise<Response> {
|
|
123
|
+
return routeWrap(request, async ({ request: req }) => {
|
|
124
|
+
const url = new URL(req.url);
|
|
125
|
+
const limit = url.searchParams.get("limit");
|
|
126
|
+
const order = url.searchParams.get("order") as "asc" | "desc" | null;
|
|
127
|
+
const includeArchived = url.searchParams.get("include_archived") === "true";
|
|
128
|
+
const cursor = url.searchParams.get("page") ?? undefined;
|
|
129
|
+
|
|
130
|
+
const data = listAgents({
|
|
131
|
+
limit: limit ? Number(limit) : undefined,
|
|
132
|
+
order: order ?? undefined,
|
|
133
|
+
includeArchived,
|
|
134
|
+
cursor,
|
|
135
|
+
});
|
|
136
|
+
return jsonOk({
|
|
137
|
+
data,
|
|
138
|
+
next_page: data.length > 0 ? data[data.length - 1].id : null,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function handleGetAgent(request: Request, id: string): Promise<Response> {
|
|
144
|
+
return routeWrap(request, async () => {
|
|
145
|
+
if (isProxied(id)) return forwardToAnthropic(request, `/v1/agents/${id}`);
|
|
146
|
+
const url = new URL(request.url);
|
|
147
|
+
const versionParam = url.searchParams.get("version");
|
|
148
|
+
const version = versionParam ? Number(versionParam) : undefined;
|
|
149
|
+
const agent = getAgent(id, version);
|
|
150
|
+
if (!agent) throw notFound(`agent ${id} not found`);
|
|
151
|
+
return jsonOk(agent);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function handleUpdateAgent(request: Request, id: string): Promise<Response> {
|
|
156
|
+
return routeWrap(request, async () => {
|
|
157
|
+
if (isProxied(id)) return forwardToAnthropic(request, `/v1/agents/${id}`);
|
|
158
|
+
const body = (await request.json().catch(() => null)) as Record<string, unknown> | null;
|
|
159
|
+
|
|
160
|
+
if (body && typeof body === "object" && "backend" in body) {
|
|
161
|
+
throw badRequest("backend cannot be changed after agent creation");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const parsed = UpdateSchema.safeParse(body);
|
|
165
|
+
if (!parsed.success) throw badRequest(parsed.error.message);
|
|
166
|
+
|
|
167
|
+
const updated = updateAgent(id, {
|
|
168
|
+
name: parsed.data.name,
|
|
169
|
+
model: parsed.data.model,
|
|
170
|
+
system: parsed.data.system,
|
|
171
|
+
tools: parsed.data.tools as never,
|
|
172
|
+
mcp_servers: parsed.data.mcp_servers as never,
|
|
173
|
+
webhook_url: parsed.data.webhook_url,
|
|
174
|
+
webhook_events: parsed.data.webhook_events,
|
|
175
|
+
confirmation_mode: parsed.data.confirmation_mode,
|
|
176
|
+
callable_agents: parsed.data.callable_agents,
|
|
177
|
+
});
|
|
178
|
+
if (!updated) throw notFound(`agent ${id} not found`);
|
|
179
|
+
return jsonOk(updated);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function handleDeleteAgent(request: Request, id: string): Promise<Response> {
|
|
184
|
+
return routeWrap(request, async () => {
|
|
185
|
+
if (isProxied(id)) {
|
|
186
|
+
const res = await forwardToAnthropic(request, `/v1/agents/${id}`);
|
|
187
|
+
if (res.ok) unmarkProxied(id);
|
|
188
|
+
return res;
|
|
189
|
+
}
|
|
190
|
+
const ok = archiveAgent(id);
|
|
191
|
+
if (!ok) throw notFound(`agent ${id} not found`);
|
|
192
|
+
return jsonOk({ id, type: "agent_deleted" });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { routeWrap, jsonOk } from "../http";
|
|
3
|
+
import { executeBatch, BatchError } from "../db/batch";
|
|
4
|
+
import { badRequest } from "../errors";
|
|
5
|
+
|
|
6
|
+
const OperationSchema = z.object({
|
|
7
|
+
method: z.string().min(1),
|
|
8
|
+
path: z.string().min(1),
|
|
9
|
+
body: z.record(z.unknown()).optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const BatchSchema = z.object({
|
|
13
|
+
operations: z.array(OperationSchema).min(1).max(50),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function handleBatch(request: Request): Promise<Response> {
|
|
17
|
+
return routeWrap(request, async () => {
|
|
18
|
+
const body = await request.json();
|
|
19
|
+
const parsed = BatchSchema.safeParse(body);
|
|
20
|
+
if (!parsed.success) throw badRequest(parsed.error.message);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const results = executeBatch(parsed.data.operations);
|
|
24
|
+
return jsonOk({ results });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err instanceof BatchError) {
|
|
27
|
+
return jsonOk(
|
|
28
|
+
{
|
|
29
|
+
error: {
|
|
30
|
+
type: "batch_error",
|
|
31
|
+
message: err.message,
|
|
32
|
+
failed_operation_index: err.failedIndex,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
400,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|