@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
package/src/db/events.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { getDb } from "./client";
|
|
2
|
+
import { newId } from "../util/ids";
|
|
3
|
+
import { nowMs, toIso } from "../util/clock";
|
|
4
|
+
import type { EventOrigin, EventRow, ManagedEvent } from "../types";
|
|
5
|
+
|
|
6
|
+
export interface AppendInput {
|
|
7
|
+
type: string;
|
|
8
|
+
payload: Record<string, unknown>;
|
|
9
|
+
origin: EventOrigin;
|
|
10
|
+
idempotencyKey?: string | null;
|
|
11
|
+
processedAt?: number | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Append an event to a session's log inside an IMMEDIATE transaction.
|
|
16
|
+
*
|
|
17
|
+
* Reserves the next sequence number by reading `sessions.last_seq` and
|
|
18
|
+
* updating it atomically. Honors the partial-unique idempotency_key index —
|
|
19
|
+
* if a matching key already exists for this session, the existing row is
|
|
20
|
+
* returned and no new row is inserted.
|
|
21
|
+
*
|
|
22
|
+
* NOTE: this function does NOT emit on the bus. The caller (typically
|
|
23
|
+
* `lib/sessions/bus.ts`) is responsible for post-commit fan-out, so that
|
|
24
|
+
* the order "commit first, then emit" is guaranteed.
|
|
25
|
+
*/
|
|
26
|
+
export function appendEvent(sessionId: string, input: AppendInput): EventRow {
|
|
27
|
+
const db = getDb();
|
|
28
|
+
|
|
29
|
+
return db.transaction(() => {
|
|
30
|
+
if (input.idempotencyKey) {
|
|
31
|
+
const dupe = db
|
|
32
|
+
.prepare(
|
|
33
|
+
`SELECT * FROM events WHERE session_id = ? AND idempotency_key = ?`,
|
|
34
|
+
)
|
|
35
|
+
.get(sessionId, input.idempotencyKey) as EventRow | undefined;
|
|
36
|
+
if (dupe) return dupe;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const row = db
|
|
40
|
+
.prepare(
|
|
41
|
+
`SELECT last_seq FROM sessions WHERE id = ?`,
|
|
42
|
+
)
|
|
43
|
+
.get(sessionId) as { last_seq: number } | undefined;
|
|
44
|
+
if (!row) throw new Error(`session not found: ${sessionId}`);
|
|
45
|
+
|
|
46
|
+
const seq = row.last_seq + 1;
|
|
47
|
+
const id = newId("evt");
|
|
48
|
+
const receivedAt = nowMs();
|
|
49
|
+
|
|
50
|
+
db.prepare(
|
|
51
|
+
`INSERT INTO events (
|
|
52
|
+
id, session_id, seq, type, payload_json,
|
|
53
|
+
processed_at, received_at, origin, idempotency_key
|
|
54
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
55
|
+
).run(
|
|
56
|
+
id,
|
|
57
|
+
sessionId,
|
|
58
|
+
seq,
|
|
59
|
+
input.type,
|
|
60
|
+
JSON.stringify(input.payload),
|
|
61
|
+
input.processedAt ?? null,
|
|
62
|
+
receivedAt,
|
|
63
|
+
input.origin,
|
|
64
|
+
input.idempotencyKey ?? null,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
db.prepare(`UPDATE sessions SET last_seq = ?, updated_at = ? WHERE id = ?`).run(
|
|
68
|
+
seq,
|
|
69
|
+
receivedAt,
|
|
70
|
+
sessionId,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
id,
|
|
75
|
+
session_id: sessionId,
|
|
76
|
+
seq,
|
|
77
|
+
type: input.type,
|
|
78
|
+
payload_json: JSON.stringify(input.payload),
|
|
79
|
+
processed_at: input.processedAt ?? null,
|
|
80
|
+
received_at: receivedAt,
|
|
81
|
+
origin: input.origin,
|
|
82
|
+
idempotency_key: input.idempotencyKey ?? null,
|
|
83
|
+
};
|
|
84
|
+
})();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Append multiple events in a single transaction. Returns the inserted rows
|
|
89
|
+
* in input order. Same idempotency semantics per row.
|
|
90
|
+
*/
|
|
91
|
+
export function appendEventsBatch(sessionId: string, inputs: AppendInput[]): EventRow[] {
|
|
92
|
+
const db = getDb();
|
|
93
|
+
|
|
94
|
+
return db.transaction(() => {
|
|
95
|
+
const rows: EventRow[] = [];
|
|
96
|
+
for (const input of inputs) {
|
|
97
|
+
if (input.idempotencyKey) {
|
|
98
|
+
const dupe = db
|
|
99
|
+
.prepare(
|
|
100
|
+
`SELECT * FROM events WHERE session_id = ? AND idempotency_key = ?`,
|
|
101
|
+
)
|
|
102
|
+
.get(sessionId, input.idempotencyKey) as EventRow | undefined;
|
|
103
|
+
if (dupe) {
|
|
104
|
+
rows.push(dupe);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cur = db
|
|
110
|
+
.prepare(
|
|
111
|
+
`SELECT last_seq FROM sessions WHERE id = ?`,
|
|
112
|
+
)
|
|
113
|
+
.get(sessionId) as { last_seq: number } | undefined;
|
|
114
|
+
if (!cur) throw new Error(`session not found: ${sessionId}`);
|
|
115
|
+
|
|
116
|
+
const seq = cur.last_seq + 1;
|
|
117
|
+
const id = newId("evt");
|
|
118
|
+
const receivedAt = nowMs();
|
|
119
|
+
|
|
120
|
+
db.prepare(
|
|
121
|
+
`INSERT INTO events (
|
|
122
|
+
id, session_id, seq, type, payload_json,
|
|
123
|
+
processed_at, received_at, origin, idempotency_key
|
|
124
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
125
|
+
).run(
|
|
126
|
+
id,
|
|
127
|
+
sessionId,
|
|
128
|
+
seq,
|
|
129
|
+
input.type,
|
|
130
|
+
JSON.stringify(input.payload),
|
|
131
|
+
input.processedAt ?? null,
|
|
132
|
+
receivedAt,
|
|
133
|
+
input.origin,
|
|
134
|
+
input.idempotencyKey ?? null,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
db.prepare(`UPDATE sessions SET last_seq = ?, updated_at = ? WHERE id = ?`).run(
|
|
138
|
+
seq,
|
|
139
|
+
receivedAt,
|
|
140
|
+
sessionId,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
rows.push({
|
|
144
|
+
id,
|
|
145
|
+
session_id: sessionId,
|
|
146
|
+
seq,
|
|
147
|
+
type: input.type,
|
|
148
|
+
payload_json: JSON.stringify(input.payload),
|
|
149
|
+
processed_at: input.processedAt ?? null,
|
|
150
|
+
received_at: receivedAt,
|
|
151
|
+
origin: input.origin,
|
|
152
|
+
idempotency_key: input.idempotencyKey ?? null,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return rows;
|
|
156
|
+
})();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function markUserEventProcessed(eventId: string, when: number): void {
|
|
160
|
+
const db = getDb();
|
|
161
|
+
db.prepare(`UPDATE events SET processed_at = ? WHERE id = ?`).run(when, eventId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function listEvents(
|
|
165
|
+
sessionId: string,
|
|
166
|
+
opts: {
|
|
167
|
+
limit?: number;
|
|
168
|
+
order?: "asc" | "desc";
|
|
169
|
+
afterSeq?: number;
|
|
170
|
+
},
|
|
171
|
+
): EventRow[] {
|
|
172
|
+
const db = getDb();
|
|
173
|
+
const limit = Math.min(Math.max(opts.limit ?? 50, 1), 500);
|
|
174
|
+
const order = opts.order === "desc" ? "DESC" : "ASC";
|
|
175
|
+
const afterSeq = opts.afterSeq ?? 0;
|
|
176
|
+
|
|
177
|
+
return db
|
|
178
|
+
.prepare(
|
|
179
|
+
`SELECT * FROM events
|
|
180
|
+
WHERE session_id = ? AND seq > ?
|
|
181
|
+
ORDER BY seq ${order}
|
|
182
|
+
LIMIT ?`,
|
|
183
|
+
)
|
|
184
|
+
.all(sessionId, afterSeq, limit) as EventRow[];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function getLastUnprocessedUserMessage(sessionId: string): EventRow | null {
|
|
188
|
+
const db = getDb();
|
|
189
|
+
return (
|
|
190
|
+
(db
|
|
191
|
+
.prepare(
|
|
192
|
+
`SELECT * FROM events WHERE session_id = ? AND type = 'user.message' AND processed_at IS NULL ORDER BY seq DESC LIMIT 1`,
|
|
193
|
+
)
|
|
194
|
+
.get(sessionId) as EventRow | undefined) ?? null
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function rowToManagedEvent(row: EventRow): ManagedEvent {
|
|
199
|
+
const payload = JSON.parse(row.payload_json) as Record<string, unknown>;
|
|
200
|
+
return {
|
|
201
|
+
id: row.id,
|
|
202
|
+
seq: row.seq,
|
|
203
|
+
session_id: row.session_id,
|
|
204
|
+
type: row.type,
|
|
205
|
+
processed_at: row.processed_at != null ? toIso(row.processed_at) : null,
|
|
206
|
+
...payload,
|
|
207
|
+
};
|
|
208
|
+
}
|
package/src/db/memory.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { getDb } from "./client";
|
|
3
|
+
import { newId } from "../util/ids";
|
|
4
|
+
import { nowMs, toIso } from "../util/clock";
|
|
5
|
+
import type { MemoryStore, MemoryStoreRow, Memory, MemoryRow } from "../types";
|
|
6
|
+
|
|
7
|
+
function hydrateStore(row: MemoryStoreRow): MemoryStore {
|
|
8
|
+
return {
|
|
9
|
+
id: row.id,
|
|
10
|
+
name: row.name,
|
|
11
|
+
description: row.description,
|
|
12
|
+
created_at: toIso(row.created_at),
|
|
13
|
+
updated_at: toIso(row.updated_at),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hydrateMemory(row: MemoryRow): Memory {
|
|
18
|
+
return {
|
|
19
|
+
id: row.id,
|
|
20
|
+
store_id: row.store_id,
|
|
21
|
+
path: row.path,
|
|
22
|
+
content: row.content,
|
|
23
|
+
content_sha256: row.content_sha256,
|
|
24
|
+
created_at: toIso(row.created_at),
|
|
25
|
+
updated_at: toIso(row.updated_at),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sha256(content: string): string {
|
|
30
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Memory Stores ────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export function createMemoryStore(input: { name: string; description?: string | null }): MemoryStore {
|
|
36
|
+
const db = getDb();
|
|
37
|
+
const id = newId("ms");
|
|
38
|
+
const now = nowMs();
|
|
39
|
+
db.prepare(
|
|
40
|
+
`INSERT INTO memory_stores (id, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`,
|
|
41
|
+
).run(id, input.name, input.description ?? null, now, now);
|
|
42
|
+
return getMemoryStore(id)!;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getMemoryStore(id: string): MemoryStore | null {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
const row = db.prepare(`SELECT * FROM memory_stores WHERE id = ?`).get(id) as MemoryStoreRow | undefined;
|
|
48
|
+
return row ? hydrateStore(row) : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function listMemoryStores(): MemoryStore[] {
|
|
52
|
+
const db = getDb();
|
|
53
|
+
const rows = db.prepare(`SELECT * FROM memory_stores ORDER BY created_at DESC`).all() as MemoryStoreRow[];
|
|
54
|
+
return rows.map(hydrateStore);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function deleteMemoryStore(id: string): boolean {
|
|
58
|
+
const db = getDb();
|
|
59
|
+
const res = db.prepare(`DELETE FROM memory_stores WHERE id = ?`).run(id);
|
|
60
|
+
return res.changes > 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Memories ─────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export function createOrUpsertMemory(storeId: string, path: string, content: string): Memory {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
const hash = sha256(content);
|
|
68
|
+
const now = nowMs();
|
|
69
|
+
const id = newId("mem");
|
|
70
|
+
|
|
71
|
+
db.prepare(
|
|
72
|
+
`INSERT INTO memories (id, store_id, path, content, content_sha256, created_at, updated_at)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
74
|
+
ON CONFLICT(store_id, path) DO UPDATE SET content = excluded.content, content_sha256 = excluded.content_sha256, updated_at = excluded.updated_at`,
|
|
75
|
+
).run(id, storeId, path, content, hash, now, now);
|
|
76
|
+
|
|
77
|
+
return getMemoryByPath(storeId, path)!;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getMemory(id: string): Memory | null {
|
|
81
|
+
const db = getDb();
|
|
82
|
+
const row = db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id) as MemoryRow | undefined;
|
|
83
|
+
return row ? hydrateMemory(row) : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getMemoryByPath(storeId: string, path: string): Memory | null {
|
|
87
|
+
const db = getDb();
|
|
88
|
+
const row = db
|
|
89
|
+
.prepare(
|
|
90
|
+
`SELECT * FROM memories WHERE store_id = ? AND path = ?`,
|
|
91
|
+
)
|
|
92
|
+
.get(storeId, path) as MemoryRow | undefined;
|
|
93
|
+
return row ? hydrateMemory(row) : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function listMemories(storeId: string): Memory[] {
|
|
97
|
+
const db = getDb();
|
|
98
|
+
const rows = db
|
|
99
|
+
.prepare(
|
|
100
|
+
`SELECT * FROM memories WHERE store_id = ? ORDER BY path ASC`,
|
|
101
|
+
)
|
|
102
|
+
.all(storeId) as MemoryRow[];
|
|
103
|
+
return rows.map(hydrateMemory);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function searchMemories(storeId: string, query: string): Memory[] {
|
|
107
|
+
const db = getDb();
|
|
108
|
+
const escaped = query.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
109
|
+
const pattern = `%${escaped}%`;
|
|
110
|
+
const rows = db
|
|
111
|
+
.prepare(
|
|
112
|
+
`SELECT * FROM memories WHERE store_id = ? AND (path LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\') ORDER BY path ASC`,
|
|
113
|
+
)
|
|
114
|
+
.all(storeId, pattern, pattern) as MemoryRow[];
|
|
115
|
+
return rows.map(hydrateMemory);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function updateMemory(
|
|
119
|
+
id: string,
|
|
120
|
+
content: string,
|
|
121
|
+
preconditionSha256?: string,
|
|
122
|
+
): { memory: Memory | null; conflict: boolean } {
|
|
123
|
+
const db = getDb();
|
|
124
|
+
const existing = getMemory(id);
|
|
125
|
+
if (!existing) return { memory: null, conflict: false };
|
|
126
|
+
|
|
127
|
+
if (preconditionSha256 && existing.content_sha256 !== preconditionSha256) {
|
|
128
|
+
return { memory: null, conflict: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hash = sha256(content);
|
|
132
|
+
const now = nowMs();
|
|
133
|
+
db.prepare(
|
|
134
|
+
`UPDATE memories SET content = ?, content_sha256 = ?, updated_at = ? WHERE id = ?`,
|
|
135
|
+
).run(content, hash, now, id);
|
|
136
|
+
return { memory: getMemory(id), conflict: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function deleteMemory(id: string): boolean {
|
|
140
|
+
const db = getDb();
|
|
141
|
+
const res = db.prepare(`DELETE FROM memories WHERE id = ?`).run(id);
|
|
142
|
+
return res.changes > 0;
|
|
143
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type Database from "libsql";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Idempotent schema. All `CREATE TABLE IF NOT EXISTS`. Safe to re-run on boot.
|
|
5
|
+
*
|
|
6
|
+
* Schema locked per plan §Data model. Key decisions:
|
|
7
|
+
* - events is append-only with (session_id, seq) unique
|
|
8
|
+
* - processed_at is NULLABLE (set on dispatch, not on insert)
|
|
9
|
+
* - idempotency_key partial unique index dedupes client retries
|
|
10
|
+
* - stats / usage are columnar on sessions (not JSON blobs)
|
|
11
|
+
* - environments have async state machine (preparing → ready | failed)
|
|
12
|
+
* - sessions.sprite_name is NULL until first user.message (lazy reservation)
|
|
13
|
+
*/
|
|
14
|
+
export function runMigrations(db: InstanceType<typeof Database>): void {
|
|
15
|
+
db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
name TEXT NOT NULL,
|
|
19
|
+
hash TEXT NOT NULL UNIQUE,
|
|
20
|
+
prefix TEXT NOT NULL,
|
|
21
|
+
permissions_json TEXT NOT NULL DEFAULT '[]',
|
|
22
|
+
created_at INTEGER NOT NULL,
|
|
23
|
+
revoked_at INTEGER
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
27
|
+
key TEXT PRIMARY KEY,
|
|
28
|
+
value TEXT,
|
|
29
|
+
type TEXT NOT NULL DEFAULT 'text',
|
|
30
|
+
updated_at INTEGER NOT NULL
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
current_version INTEGER NOT NULL,
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
created_at INTEGER NOT NULL,
|
|
38
|
+
updated_at INTEGER NOT NULL,
|
|
39
|
+
archived_at INTEGER
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS agent_versions (
|
|
43
|
+
agent_id TEXT NOT NULL,
|
|
44
|
+
version INTEGER NOT NULL,
|
|
45
|
+
model TEXT NOT NULL,
|
|
46
|
+
system TEXT,
|
|
47
|
+
tools_json TEXT NOT NULL DEFAULT '[]',
|
|
48
|
+
mcp_servers_json TEXT NOT NULL DEFAULT '{}',
|
|
49
|
+
created_at INTEGER NOT NULL,
|
|
50
|
+
-- backend column added in migration below (opencode adapter); default
|
|
51
|
+
-- 'claude' so existing rows and new inserts that omit it work the same
|
|
52
|
+
PRIMARY KEY (agent_id, version),
|
|
53
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
name TEXT NOT NULL,
|
|
59
|
+
config_json TEXT NOT NULL,
|
|
60
|
+
state TEXT NOT NULL DEFAULT 'preparing',
|
|
61
|
+
state_message TEXT,
|
|
62
|
+
template_sprite TEXT,
|
|
63
|
+
checkpoint_id TEXT,
|
|
64
|
+
created_at INTEGER NOT NULL,
|
|
65
|
+
archived_at INTEGER
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
agent_id TEXT NOT NULL,
|
|
71
|
+
agent_version INTEGER NOT NULL,
|
|
72
|
+
environment_id TEXT NOT NULL,
|
|
73
|
+
sprite_name TEXT,
|
|
74
|
+
-- Legacy name: holds any backend's session id (claude's session_id or
|
|
75
|
+
-- opencode's sessionID). Kept as claude_session_id to avoid schema
|
|
76
|
+
-- churn; see lib/backends/types.ts for the abstraction.
|
|
77
|
+
claude_session_id TEXT,
|
|
78
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
79
|
+
stop_reason TEXT,
|
|
80
|
+
title TEXT,
|
|
81
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
82
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
83
|
+
tool_calls_count INTEGER NOT NULL DEFAULT 0,
|
|
84
|
+
active_seconds REAL NOT NULL DEFAULT 0,
|
|
85
|
+
duration_seconds REAL NOT NULL DEFAULT 0,
|
|
86
|
+
usage_input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
87
|
+
usage_output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
88
|
+
usage_cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
89
|
+
usage_cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
usage_cost_usd REAL NOT NULL DEFAULT 0,
|
|
91
|
+
last_seq INTEGER NOT NULL DEFAULT 0,
|
|
92
|
+
idle_since INTEGER,
|
|
93
|
+
parked_checkpoint_id TEXT,
|
|
94
|
+
created_at INTEGER NOT NULL,
|
|
95
|
+
updated_at INTEGER NOT NULL,
|
|
96
|
+
archived_at INTEGER,
|
|
97
|
+
FOREIGN KEY (agent_id, agent_version) REFERENCES agent_versions(agent_id, version),
|
|
98
|
+
FOREIGN KEY (environment_id) REFERENCES environments(id)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_env ON sessions(environment_id);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_created_id ON sessions(created_at DESC, id);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
105
|
+
|
|
106
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
107
|
+
id TEXT PRIMARY KEY,
|
|
108
|
+
session_id TEXT NOT NULL,
|
|
109
|
+
seq INTEGER NOT NULL,
|
|
110
|
+
type TEXT NOT NULL,
|
|
111
|
+
payload_json TEXT NOT NULL,
|
|
112
|
+
processed_at INTEGER,
|
|
113
|
+
received_at INTEGER NOT NULL,
|
|
114
|
+
origin TEXT NOT NULL,
|
|
115
|
+
idempotency_key TEXT,
|
|
116
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_events_session_seq ON events(session_id, seq);
|
|
120
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_events_idem
|
|
121
|
+
ON events(session_id, idempotency_key)
|
|
122
|
+
WHERE idempotency_key IS NOT NULL;
|
|
123
|
+
|
|
124
|
+
-- Proxy routing table: tracks which resource IDs belong to Anthropic's
|
|
125
|
+
-- hosted MA API. When isProxied(id) returns true, the route handler
|
|
126
|
+
-- forwards the request to api.anthropic.com instead of handling locally.
|
|
127
|
+
CREATE TABLE IF NOT EXISTS proxy_resources (
|
|
128
|
+
resource_id TEXT PRIMARY KEY,
|
|
129
|
+
resource_type TEXT NOT NULL CHECK(resource_type IN ('agent','environment','session')),
|
|
130
|
+
created_at INTEGER NOT NULL
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
-- Vault persistence: per-agent key-value stores that persist across sessions.
|
|
134
|
+
CREATE TABLE IF NOT EXISTS vaults (
|
|
135
|
+
id TEXT PRIMARY KEY,
|
|
136
|
+
agent_id TEXT NOT NULL,
|
|
137
|
+
name TEXT NOT NULL,
|
|
138
|
+
created_at INTEGER NOT NULL,
|
|
139
|
+
updated_at INTEGER NOT NULL,
|
|
140
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS vault_entries (
|
|
144
|
+
vault_id TEXT NOT NULL,
|
|
145
|
+
key TEXT NOT NULL,
|
|
146
|
+
value TEXT NOT NULL,
|
|
147
|
+
updated_at INTEGER NOT NULL,
|
|
148
|
+
PRIMARY KEY (vault_id, key),
|
|
149
|
+
FOREIGN KEY (vault_id) REFERENCES vaults(id) ON DELETE CASCADE
|
|
150
|
+
);
|
|
151
|
+
`);
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Incremental migrations (add via PRAGMA table_info guards so re-boot is a
|
|
155
|
+
// no-op on already-migrated DBs). No schema_version table exists yet; when
|
|
156
|
+
// the migration count grows, introduce one and fold these into it.
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
// opencode adapter: backend discriminator on agent_versions
|
|
160
|
+
const agentVersionCols = db
|
|
161
|
+
.prepare(`PRAGMA table_info(agent_versions)`)
|
|
162
|
+
.all() as Array<{ name: string }>;
|
|
163
|
+
if (!agentVersionCols.some((c) => c.name === "backend")) {
|
|
164
|
+
db.exec(
|
|
165
|
+
`ALTER TABLE agent_versions ADD COLUMN backend TEXT NOT NULL DEFAULT 'claude'`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Docker provider: provider_name on sessions (defaults to 'sprites' for existing rows)
|
|
170
|
+
const sessionCols = db
|
|
171
|
+
.prepare(`PRAGMA table_info(sessions)`)
|
|
172
|
+
.all() as Array<{ name: string }>;
|
|
173
|
+
if (!sessionCols.some((c) => c.name === "provider_name")) {
|
|
174
|
+
db.exec(
|
|
175
|
+
`ALTER TABLE sessions ADD COLUMN provider_name TEXT NOT NULL DEFAULT 'sprites'`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Spec gap 3: max_budget_usd on sessions
|
|
180
|
+
const sessionCols2 = db
|
|
181
|
+
.prepare(`PRAGMA table_info(sessions)`)
|
|
182
|
+
.all() as Array<{ name: string }>;
|
|
183
|
+
if (!sessionCols2.some((c) => c.name === "max_budget_usd")) {
|
|
184
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN max_budget_usd REAL`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Spec gap 4: webhook_url + webhook_events on agent_versions
|
|
188
|
+
const avCols2 = db
|
|
189
|
+
.prepare(`PRAGMA table_info(agent_versions)`)
|
|
190
|
+
.all() as Array<{ name: string }>;
|
|
191
|
+
if (!avCols2.some((c) => c.name === "webhook_url")) {
|
|
192
|
+
db.exec(`ALTER TABLE agent_versions ADD COLUMN webhook_url TEXT`);
|
|
193
|
+
}
|
|
194
|
+
if (!avCols2.some((c) => c.name === "webhook_events_json")) {
|
|
195
|
+
db.exec(
|
|
196
|
+
`ALTER TABLE agent_versions ADD COLUMN webhook_events_json TEXT NOT NULL DEFAULT '["session.status_idle","session.status_running","session.error"]'`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Spec gap 6: outcome_criteria_json on sessions
|
|
201
|
+
const sessionCols3 = db
|
|
202
|
+
.prepare(`PRAGMA table_info(sessions)`)
|
|
203
|
+
.all() as Array<{ name: string }>;
|
|
204
|
+
if (!sessionCols3.some((c) => c.name === "outcome_criteria_json")) {
|
|
205
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN outcome_criteria_json TEXT`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Spec gap 7: resources_json on sessions
|
|
209
|
+
const sessionCols4 = db
|
|
210
|
+
.prepare(`PRAGMA table_info(sessions)`)
|
|
211
|
+
.all() as Array<{ name: string }>;
|
|
212
|
+
if (!sessionCols4.some((c) => c.name === "resources_json")) {
|
|
213
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN resources_json TEXT`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Vault persistence: vault_ids_json on sessions
|
|
217
|
+
const sessionCols5 = db
|
|
218
|
+
.prepare(`PRAGMA table_info(sessions)`)
|
|
219
|
+
.all() as Array<{ name: string }>;
|
|
220
|
+
if (!sessionCols5.some((c) => c.name === "vault_ids_json")) {
|
|
221
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN vault_ids_json TEXT`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Multi-agent threads: parent_session_id + thread_depth on sessions
|
|
225
|
+
const sessionCols6 = db
|
|
226
|
+
.prepare(`PRAGMA table_info(sessions)`)
|
|
227
|
+
.all() as Array<{ name: string }>;
|
|
228
|
+
if (!sessionCols6.some((c) => c.name === "parent_session_id")) {
|
|
229
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN parent_session_id TEXT`);
|
|
230
|
+
}
|
|
231
|
+
if (!sessionCols6.some((c) => c.name === "thread_depth")) {
|
|
232
|
+
db.exec(
|
|
233
|
+
`ALTER TABLE sessions ADD COLUMN thread_depth INTEGER NOT NULL DEFAULT 0`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Multi-agent threads: threads_enabled on agent_versions
|
|
238
|
+
const avCols3 = db
|
|
239
|
+
.prepare(`PRAGMA table_info(agent_versions)`)
|
|
240
|
+
.all() as Array<{ name: string }>;
|
|
241
|
+
if (!avCols3.some((c) => c.name === "threads_enabled")) {
|
|
242
|
+
db.exec(
|
|
243
|
+
`ALTER TABLE agent_versions ADD COLUMN threads_enabled INTEGER NOT NULL DEFAULT 0`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Tool confirmation: confirmation_mode on agent_versions
|
|
248
|
+
const avCols4 = db
|
|
249
|
+
.prepare(`PRAGMA table_info(agent_versions)`)
|
|
250
|
+
.all() as Array<{ name: string }>;
|
|
251
|
+
if (!avCols4.some((c) => c.name === "confirmation_mode")) {
|
|
252
|
+
db.exec(
|
|
253
|
+
`ALTER TABLE agent_versions ADD COLUMN confirmation_mode INTEGER NOT NULL DEFAULT 0`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Memory stores
|
|
258
|
+
db.exec(`
|
|
259
|
+
CREATE TABLE IF NOT EXISTS memory_stores (
|
|
260
|
+
id TEXT PRIMARY KEY,
|
|
261
|
+
name TEXT NOT NULL,
|
|
262
|
+
description TEXT,
|
|
263
|
+
created_at INTEGER NOT NULL,
|
|
264
|
+
updated_at INTEGER NOT NULL
|
|
265
|
+
)
|
|
266
|
+
`);
|
|
267
|
+
db.exec(`
|
|
268
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
269
|
+
id TEXT PRIMARY KEY,
|
|
270
|
+
store_id TEXT NOT NULL REFERENCES memory_stores(id) ON DELETE CASCADE,
|
|
271
|
+
path TEXT NOT NULL,
|
|
272
|
+
content TEXT NOT NULL,
|
|
273
|
+
content_sha256 TEXT NOT NULL,
|
|
274
|
+
created_at INTEGER NOT NULL,
|
|
275
|
+
updated_at INTEGER NOT NULL,
|
|
276
|
+
UNIQUE(store_id, path)
|
|
277
|
+
)
|
|
278
|
+
`);
|
|
279
|
+
|
|
280
|
+
// Index on parent_session_id for thread listing
|
|
281
|
+
db.exec(`
|
|
282
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_parent
|
|
283
|
+
ON sessions(parent_session_id) WHERE parent_session_id IS NOT NULL
|
|
284
|
+
`);
|
|
285
|
+
|
|
286
|
+
// callable_agents on agent_versions (for multi-agent thread config)
|
|
287
|
+
const avCols5 = db
|
|
288
|
+
.prepare(`PRAGMA table_info(agent_versions)`)
|
|
289
|
+
.all() as Array<{ name: string }>;
|
|
290
|
+
if (!avCols5.some((c) => c.name === "callable_agents_json")) {
|
|
291
|
+
db.exec(
|
|
292
|
+
`ALTER TABLE agent_versions ADD COLUMN callable_agents_json TEXT`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
package/src/db/proxy.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy routing table: tracks which resource IDs belong to Anthropic's
|
|
3
|
+
* hosted Managed Agents API. The route handlers check `isProxied(id)`
|
|
4
|
+
* before touching local state — if true, they forward to Anthropic.
|
|
5
|
+
*
|
|
6
|
+
* Anthropic owns the IDs (they're assigned by their API on create). We
|
|
7
|
+
* store them in this table after a successful proxy-create so subsequent
|
|
8
|
+
* requests for that ID auto-route without the client needing to specify
|
|
9
|
+
* anything.
|
|
10
|
+
*/
|
|
11
|
+
import { getDb } from "./client";
|
|
12
|
+
import { nowMs } from "../util/clock";
|
|
13
|
+
|
|
14
|
+
export type ProxiedResourceType = "agent" | "environment" | "session";
|
|
15
|
+
|
|
16
|
+
export function isProxied(id: string): boolean {
|
|
17
|
+
const db = getDb();
|
|
18
|
+
const row = db
|
|
19
|
+
.prepare(
|
|
20
|
+
`SELECT resource_id FROM proxy_resources WHERE resource_id = ?`,
|
|
21
|
+
)
|
|
22
|
+
.get(id) as { resource_id: string } | undefined;
|
|
23
|
+
return !!row;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function markProxied(id: string, type: ProxiedResourceType): void {
|
|
27
|
+
const db = getDb();
|
|
28
|
+
db.prepare(
|
|
29
|
+
`INSERT OR IGNORE INTO proxy_resources (resource_id, resource_type, created_at)
|
|
30
|
+
VALUES (?, ?, ?)`,
|
|
31
|
+
).run(id, type, nowMs());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function unmarkProxied(id: string): void {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
db.prepare(`DELETE FROM proxy_resources WHERE resource_id = ?`).run(id);
|
|
37
|
+
}
|