@springbrand/agent-runtime 0.1.3-alpha.4 → 0.1.3-alpha.5
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 +3 -1
- package/src/adapter/cloudflare/index.ts +60 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +86 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +1509 -0
- package/src/adapter/cloudflare/sandbox/id.ts +23 -0
- package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +574 -0
- package/src/adapter/cloudflare/subagent/runner.ts +175 -0
- package/src/adapter/cloudflare/subagent/tools.ts +256 -0
- package/src/adapter/cloudflare/universal-agent/definition.ts +71 -0
- package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
- package/src/adapter/cloudflare/universal-agent/preparation.ts +273 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +74 -0
- package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
- package/src/agent-tool-runtime.ts +152 -0
- package/src/index.ts +49 -7
- package/src/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +4 -2
- package/src/pi/runtime-adapter/index.ts +6 -2
- package/src/pi/tool/base.ts +17 -0
- package/src/pi/tool/core.ts +13 -0
- package/src/pi/tool/schedule.ts +11 -0
- package/src/pi/tool/subagent.ts +14 -0
- package/src/pi/tool/workspace-sandbox.ts +15 -0
- package/src/runtime-agent-context.ts +112 -0
- package/src/runtime-agent.ts +429 -315
- package/src/runtime-assembler.ts +249 -99
- package/src/runtime-definition.ts +173 -0
- package/src/runtime.ts +139 -12
- package/src/tool-registry.ts +143 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
|
|
3
|
+
function hex(bytes: ArrayBuffer): string {
|
|
4
|
+
return [...new Uint8Array(bytes)]
|
|
5
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
6
|
+
.join("");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Stable, non-reversible Sandbox identifier for one owned Chat Session. */
|
|
10
|
+
export async function sandboxIdForSession(
|
|
11
|
+
userId: string,
|
|
12
|
+
sessionId: string,
|
|
13
|
+
): Promise<string> {
|
|
14
|
+
const digest = await crypto.subtle.digest(
|
|
15
|
+
"SHA-256",
|
|
16
|
+
// JSON string framing keeps the tuple unambiguous even when either ID
|
|
17
|
+
// contains the delimiter characters used by a human-readable format.
|
|
18
|
+
encoder.encode(
|
|
19
|
+
JSON.stringify(["universal-agent", userId, sessionId]),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
return `ua-${hex(digest).slice(0, 40)}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const SANDBOX_HOST_POLICY = Object.freeze({
|
|
2
|
+
defaultExecTimeoutMs: 30_000,
|
|
3
|
+
maxExecTimeoutMs: 60_000,
|
|
4
|
+
maxOutputBytes: 64 * 1024,
|
|
5
|
+
maxBackgroundProcesses: 4,
|
|
6
|
+
maxBackgroundProcessAgeMs: 2 * 60 * 60_000,
|
|
7
|
+
maxPublishFiles: 100,
|
|
8
|
+
maxPublishEntries: 400,
|
|
9
|
+
maxPublishBytes: 25 * 1024 * 1024,
|
|
10
|
+
maxPublishFileBytes: 25 * 1024 * 1024,
|
|
11
|
+
maxHydrationFiles: 10_000,
|
|
12
|
+
maxHydrationEntries: 20_000,
|
|
13
|
+
maxHydrationBytes: 512 * 1024 * 1024,
|
|
14
|
+
maxHydrationManifestBytes: 1024 * 1024,
|
|
15
|
+
} as const);
|
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Agent,
|
|
3
|
+
type AgentContext,
|
|
4
|
+
type AgentToolProgressSnapshot,
|
|
5
|
+
type AgentToolRunInspection,
|
|
6
|
+
type AgentToolStoredChunk,
|
|
7
|
+
type FiberRecoveryContext,
|
|
8
|
+
type FiberRecoveryResult,
|
|
9
|
+
} from "agents";
|
|
10
|
+
import type { AgentEvent } from "@earendil-works/pi-agent-core";
|
|
11
|
+
import {
|
|
12
|
+
AGENT_TYPES,
|
|
13
|
+
} from "../../../layers/orchestration/subagents/agent-types/registry";
|
|
14
|
+
import type { AgentType } from "../../../layers/orchestration/subagents/agent-types/contract";
|
|
15
|
+
import type {
|
|
16
|
+
RuntimeProviderPort,
|
|
17
|
+
WorkspacePort,
|
|
18
|
+
} from "../../../kernel/bindings";
|
|
19
|
+
import { PiChunkEncoder } from "../../../pi/message";
|
|
20
|
+
import {
|
|
21
|
+
createPiModels,
|
|
22
|
+
resolvePiApiKey,
|
|
23
|
+
resolvePiModel,
|
|
24
|
+
withProviderRetry,
|
|
25
|
+
} from "../../../pi/runtime-adapter/models";
|
|
26
|
+
import { runCloudflareSubAgent } from "./runner";
|
|
27
|
+
import { createCloudflareSubAgentTools } from "./tools";
|
|
28
|
+
|
|
29
|
+
type RunStatus =
|
|
30
|
+
| "starting"
|
|
31
|
+
| "running"
|
|
32
|
+
| "completed"
|
|
33
|
+
| "error"
|
|
34
|
+
| "aborted";
|
|
35
|
+
|
|
36
|
+
interface RunRow {
|
|
37
|
+
run_id: string;
|
|
38
|
+
agent_type: string;
|
|
39
|
+
status: RunStatus;
|
|
40
|
+
input_json: string;
|
|
41
|
+
output_json: string | null;
|
|
42
|
+
summary: string | null;
|
|
43
|
+
error_message: string | null;
|
|
44
|
+
started_at: number;
|
|
45
|
+
completed_at: number | null;
|
|
46
|
+
progress_json: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ChunkRow {
|
|
50
|
+
sequence: number;
|
|
51
|
+
body: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface TailSubscriber {
|
|
55
|
+
push(chunk: AgentToolStoredChunk): void;
|
|
56
|
+
close(): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const FIBER_NAME = "pi-subagent";
|
|
60
|
+
const TERMINAL = new Set<RunStatus>([
|
|
61
|
+
"completed",
|
|
62
|
+
"error",
|
|
63
|
+
"aborted",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
function errorMessage(error: unknown): string {
|
|
67
|
+
return error instanceof Error ? error.message : String(error);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseJson(value: string | null): unknown {
|
|
71
|
+
if (value === null) return undefined;
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(value);
|
|
74
|
+
} catch {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stringifyJson(value: unknown): string {
|
|
80
|
+
const json = JSON.stringify(value);
|
|
81
|
+
if (json === undefined) {
|
|
82
|
+
throw new Error("SubAgent input is not JSON serializable");
|
|
83
|
+
}
|
|
84
|
+
return json;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function progressFor(
|
|
88
|
+
event: AgentEvent,
|
|
89
|
+
): Omit<AgentToolProgressSnapshot, "at"> | null {
|
|
90
|
+
if (event.type === "agent_start") {
|
|
91
|
+
return { phase: "running", message: "SubAgent started" };
|
|
92
|
+
}
|
|
93
|
+
if (event.type === "tool_execution_start") {
|
|
94
|
+
return {
|
|
95
|
+
phase: "tool",
|
|
96
|
+
message: `Running ${event.toolName}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (event.type === "tool_execution_update") {
|
|
100
|
+
return {
|
|
101
|
+
phase: "tool",
|
|
102
|
+
message: `${event.toolName} is still running`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (event.type === "tool_execution_end") {
|
|
106
|
+
return {
|
|
107
|
+
phase: event.isError ? "tool-error" : "tool",
|
|
108
|
+
message: event.isError
|
|
109
|
+
? `${event.toolName} failed`
|
|
110
|
+
: `${event.toolName} completed`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Pi-native child Durable Object used by the Agents SDK Agent Tool protocol.
|
|
118
|
+
* The base Agent owns routing/facets; this class owns only bounded Pi
|
|
119
|
+
* inference plus its durable run/chunk adapter.
|
|
120
|
+
*/
|
|
121
|
+
export interface CloudflareSubAgentExecution {
|
|
122
|
+
provider: RuntimeProviderPort;
|
|
123
|
+
workspace: WorkspacePort;
|
|
124
|
+
loader: WorkerLoader;
|
|
125
|
+
outbound: Fetcher;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export abstract class CloudflareSubAgent<
|
|
129
|
+
Env extends Cloudflare.Env,
|
|
130
|
+
> extends Agent<Env, Record<string, never>> {
|
|
131
|
+
initialState: Record<string, never> = {};
|
|
132
|
+
|
|
133
|
+
private readonly subscribers = new Map<
|
|
134
|
+
string,
|
|
135
|
+
Set<TailSubscriber>
|
|
136
|
+
>();
|
|
137
|
+
|
|
138
|
+
constructor(ctx: AgentContext, env: Env) {
|
|
139
|
+
super(ctx, env);
|
|
140
|
+
this.sql`
|
|
141
|
+
CREATE TABLE IF NOT EXISTS pi_subagent_runs (
|
|
142
|
+
run_id TEXT PRIMARY KEY,
|
|
143
|
+
agent_type TEXT NOT NULL,
|
|
144
|
+
status TEXT NOT NULL,
|
|
145
|
+
input_json TEXT NOT NULL,
|
|
146
|
+
output_json TEXT,
|
|
147
|
+
summary TEXT,
|
|
148
|
+
error_message TEXT,
|
|
149
|
+
started_at INTEGER NOT NULL,
|
|
150
|
+
completed_at INTEGER,
|
|
151
|
+
progress_json TEXT
|
|
152
|
+
)
|
|
153
|
+
`;
|
|
154
|
+
this.sql`
|
|
155
|
+
CREATE TABLE IF NOT EXISTS pi_subagent_chunks (
|
|
156
|
+
run_id TEXT NOT NULL,
|
|
157
|
+
sequence INTEGER NOT NULL,
|
|
158
|
+
body TEXT NOT NULL,
|
|
159
|
+
PRIMARY KEY (run_id, sequence)
|
|
160
|
+
)
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
protected abstract prepareExecution(): Promise<CloudflareSubAgentExecution>;
|
|
165
|
+
|
|
166
|
+
private resolveType(name: string): AgentType {
|
|
167
|
+
const type = AGENT_TYPES.byName[name];
|
|
168
|
+
if (!type) throw new Error(`unknown SubAgent type: ${name}`);
|
|
169
|
+
return type;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private readRun(runId: string): RunRow | null {
|
|
173
|
+
return (
|
|
174
|
+
this.sql<RunRow>`
|
|
175
|
+
SELECT
|
|
176
|
+
run_id,
|
|
177
|
+
agent_type,
|
|
178
|
+
status,
|
|
179
|
+
input_json,
|
|
180
|
+
output_json,
|
|
181
|
+
summary,
|
|
182
|
+
error_message,
|
|
183
|
+
started_at,
|
|
184
|
+
completed_at,
|
|
185
|
+
progress_json
|
|
186
|
+
FROM pi_subagent_runs
|
|
187
|
+
WHERE run_id = ${runId}
|
|
188
|
+
`[0] ?? null
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private readChunks(
|
|
193
|
+
runId: string,
|
|
194
|
+
afterSequence = -1,
|
|
195
|
+
): AgentToolStoredChunk[] {
|
|
196
|
+
return this.sql<ChunkRow>`
|
|
197
|
+
SELECT sequence, body
|
|
198
|
+
FROM pi_subagent_chunks
|
|
199
|
+
WHERE run_id = ${runId} AND sequence > ${afterSequence}
|
|
200
|
+
ORDER BY sequence ASC
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private inspection(row: RunRow): AgentToolRunInspection {
|
|
205
|
+
const progress = parseJson(row.progress_json);
|
|
206
|
+
return {
|
|
207
|
+
runId: row.run_id,
|
|
208
|
+
status: row.status,
|
|
209
|
+
...(row.output_json === null
|
|
210
|
+
? {}
|
|
211
|
+
: { output: parseJson(row.output_json) }),
|
|
212
|
+
...(row.summary === null ? {} : { summary: row.summary }),
|
|
213
|
+
...(row.error_message === null
|
|
214
|
+
? {}
|
|
215
|
+
: { error: row.error_message }),
|
|
216
|
+
startedAt: row.started_at,
|
|
217
|
+
...(row.completed_at === null
|
|
218
|
+
? {}
|
|
219
|
+
: { completedAt: row.completed_at }),
|
|
220
|
+
...(typeof progress === "object" && progress !== null
|
|
221
|
+
? { progress: progress as AgentToolProgressSnapshot }
|
|
222
|
+
: {}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private appendChunk(runId: string, body: string): void {
|
|
227
|
+
const next =
|
|
228
|
+
this.sql<{ sequence: number }>`
|
|
229
|
+
SELECT COALESCE(MAX(sequence), -1) + 1 AS sequence
|
|
230
|
+
FROM pi_subagent_chunks
|
|
231
|
+
WHERE run_id = ${runId}
|
|
232
|
+
`[0]?.sequence ?? 0;
|
|
233
|
+
const chunk = { sequence: next, body };
|
|
234
|
+
this.sql`
|
|
235
|
+
INSERT INTO pi_subagent_chunks (run_id, sequence, body)
|
|
236
|
+
VALUES (${runId}, ${chunk.sequence}, ${chunk.body})
|
|
237
|
+
`;
|
|
238
|
+
for (const subscriber of this.subscribers.get(runId) ?? []) {
|
|
239
|
+
subscriber.push(chunk);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private persistProgress(
|
|
244
|
+
runId: string,
|
|
245
|
+
progress: Omit<AgentToolProgressSnapshot, "at">,
|
|
246
|
+
): void {
|
|
247
|
+
const snapshot: AgentToolProgressSnapshot = {
|
|
248
|
+
...progress,
|
|
249
|
+
at: Date.now(),
|
|
250
|
+
};
|
|
251
|
+
this.sql`
|
|
252
|
+
UPDATE pi_subagent_runs
|
|
253
|
+
SET progress_json = ${stringifyJson(snapshot)}
|
|
254
|
+
WHERE run_id = ${runId}
|
|
255
|
+
`;
|
|
256
|
+
this.appendChunk(
|
|
257
|
+
runId,
|
|
258
|
+
stringifyJson({
|
|
259
|
+
type: "data-agent-progress",
|
|
260
|
+
data: progress,
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private closeSubscribers(runId: string): void {
|
|
266
|
+
const subscribers = this.subscribers.get(runId);
|
|
267
|
+
if (!subscribers) return;
|
|
268
|
+
this.subscribers.delete(runId);
|
|
269
|
+
for (const subscriber of subscribers) subscriber.close();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private completeRun(runId: string, output: unknown): void {
|
|
273
|
+
const outputJson = stringifyJson(output);
|
|
274
|
+
this.sql`
|
|
275
|
+
UPDATE pi_subagent_runs
|
|
276
|
+
SET
|
|
277
|
+
status = 'completed',
|
|
278
|
+
output_json = ${outputJson},
|
|
279
|
+
summary = ${outputJson},
|
|
280
|
+
error_message = NULL,
|
|
281
|
+
completed_at = ${Date.now()}
|
|
282
|
+
WHERE run_id = ${runId}
|
|
283
|
+
AND status IN ('starting', 'running')
|
|
284
|
+
`;
|
|
285
|
+
this.closeSubscribers(runId);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private failRun(
|
|
289
|
+
runId: string,
|
|
290
|
+
status: "error" | "aborted",
|
|
291
|
+
error: string,
|
|
292
|
+
): void {
|
|
293
|
+
this.sql`
|
|
294
|
+
UPDATE pi_subagent_runs
|
|
295
|
+
SET
|
|
296
|
+
status = ${status},
|
|
297
|
+
error_message = ${error},
|
|
298
|
+
completed_at = ${Date.now()}
|
|
299
|
+
WHERE run_id = ${runId}
|
|
300
|
+
AND status IN ('starting', 'running')
|
|
301
|
+
`;
|
|
302
|
+
this.closeSubscribers(runId);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async executeRun(
|
|
306
|
+
runId: string,
|
|
307
|
+
type: AgentType,
|
|
308
|
+
input: unknown,
|
|
309
|
+
signal: AbortSignal,
|
|
310
|
+
): Promise<void> {
|
|
311
|
+
if (TERMINAL.has(this.readRun(runId)?.status ?? "error")) return;
|
|
312
|
+
this.sql`
|
|
313
|
+
UPDATE pi_subagent_runs
|
|
314
|
+
SET status = 'running'
|
|
315
|
+
WHERE run_id = ${runId} AND status = 'starting'
|
|
316
|
+
`;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const { provider, workspace, loader, outbound } =
|
|
320
|
+
await this.prepareExecution();
|
|
321
|
+
const models = createPiModels(provider);
|
|
322
|
+
const model = resolvePiModel(provider, provider.defaultModel);
|
|
323
|
+
const tools = createCloudflareSubAgentTools({
|
|
324
|
+
profile: type.tools,
|
|
325
|
+
workspace,
|
|
326
|
+
loader,
|
|
327
|
+
outbound,
|
|
328
|
+
settle: (call) => {
|
|
329
|
+
this.appendChunk(
|
|
330
|
+
runId,
|
|
331
|
+
stringifyJson({
|
|
332
|
+
type: "data-agent-progress",
|
|
333
|
+
data: {
|
|
334
|
+
phase: call.isError ? "tool-error" : "tool",
|
|
335
|
+
message: call.isError
|
|
336
|
+
? `${call.toolName} failed`
|
|
337
|
+
: `${call.toolName} completed`,
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
let streamSequence = 0;
|
|
344
|
+
const encoder = new PiChunkEncoder({
|
|
345
|
+
messageId: runId,
|
|
346
|
+
startedAt: this.readRun(runId)?.started_at ?? Date.now(),
|
|
347
|
+
});
|
|
348
|
+
const output = await runCloudflareSubAgent({
|
|
349
|
+
type,
|
|
350
|
+
input,
|
|
351
|
+
model,
|
|
352
|
+
streamFn: withProviderRetry(models.streamSimple.bind(models)),
|
|
353
|
+
getApiKey: () =>
|
|
354
|
+
resolvePiApiKey(provider, provider.defaultModel),
|
|
355
|
+
tools,
|
|
356
|
+
signal,
|
|
357
|
+
onEvent: (event) => {
|
|
358
|
+
for (const chunk of encoder.encode(event)) {
|
|
359
|
+
this.appendChunk(runId, stringifyJson({
|
|
360
|
+
v: 1,
|
|
361
|
+
messageId: runId,
|
|
362
|
+
sequence: streamSequence++,
|
|
363
|
+
chunk,
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
const progress = progressFor(event);
|
|
367
|
+
if (progress) this.persistProgress(runId, progress);
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
this.completeRun(runId, output);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
this.failRun(
|
|
373
|
+
runId,
|
|
374
|
+
signal.aborted ? "aborted" : "error",
|
|
375
|
+
signal.aborted ? "SubAgent run aborted" : errorMessage(error),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async startAgentToolRun(
|
|
381
|
+
input: unknown,
|
|
382
|
+
options: { runId: string; signal?: AbortSignal },
|
|
383
|
+
): Promise<AgentToolRunInspection> {
|
|
384
|
+
const existing = this.readRun(options.runId);
|
|
385
|
+
if (existing) {
|
|
386
|
+
return (
|
|
387
|
+
(await this.inspectAgentToolRun(options.runId)) ??
|
|
388
|
+
this.inspection(existing)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const record =
|
|
393
|
+
typeof input === "object" && input !== null
|
|
394
|
+
? (input as Record<string, unknown>)
|
|
395
|
+
: {};
|
|
396
|
+
const name = record.agentType;
|
|
397
|
+
if (typeof name !== "string") {
|
|
398
|
+
throw new Error(`unknown SubAgent type: ${String(name ?? "")}`);
|
|
399
|
+
}
|
|
400
|
+
const type = this.resolveType(name);
|
|
401
|
+
const { agentType: _agentType, ...rawInput } = record;
|
|
402
|
+
const parsed = type.inputSchema.safeParse(rawInput);
|
|
403
|
+
if (!parsed.success) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`SubAgent input failed schema validation: ${parsed.error.message}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const inputJson = stringifyJson(parsed.data);
|
|
409
|
+
const startedAt = Date.now();
|
|
410
|
+
this.sql`
|
|
411
|
+
INSERT INTO pi_subagent_runs (
|
|
412
|
+
run_id,
|
|
413
|
+
agent_type,
|
|
414
|
+
status,
|
|
415
|
+
input_json,
|
|
416
|
+
started_at
|
|
417
|
+
)
|
|
418
|
+
VALUES (
|
|
419
|
+
${options.runId},
|
|
420
|
+
${type.name},
|
|
421
|
+
'starting',
|
|
422
|
+
${inputJson},
|
|
423
|
+
${startedAt}
|
|
424
|
+
)
|
|
425
|
+
`;
|
|
426
|
+
|
|
427
|
+
const abort = () => {
|
|
428
|
+
void this.cancelAgentToolRun(options.runId).catch(() => {});
|
|
429
|
+
};
|
|
430
|
+
if (options.signal?.aborted) abort();
|
|
431
|
+
else options.signal?.addEventListener("abort", abort, { once: true });
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
await this.startFiber(
|
|
435
|
+
FIBER_NAME,
|
|
436
|
+
(fiber) =>
|
|
437
|
+
this.executeRun(
|
|
438
|
+
options.runId,
|
|
439
|
+
type,
|
|
440
|
+
parsed.data,
|
|
441
|
+
fiber.signal,
|
|
442
|
+
),
|
|
443
|
+
{
|
|
444
|
+
idempotencyKey: options.runId,
|
|
445
|
+
metadata: { runId: options.runId },
|
|
446
|
+
waitForCompletion: false,
|
|
447
|
+
},
|
|
448
|
+
);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
this.failRun(options.runId, "error", errorMessage(error));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const row = this.readRun(options.runId);
|
|
454
|
+
if (!row) throw new Error("SubAgent run was not persisted");
|
|
455
|
+
return this.inspection(row);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async cancelAgentToolRun(
|
|
459
|
+
runId: string,
|
|
460
|
+
_reason?: unknown,
|
|
461
|
+
): Promise<void> {
|
|
462
|
+
this.failRun(runId, "aborted", "SubAgent run aborted");
|
|
463
|
+
await this.cancelFiberByKey(runId, "SubAgent run aborted");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async inspectAgentToolRun(
|
|
467
|
+
runId: string,
|
|
468
|
+
): Promise<AgentToolRunInspection | null> {
|
|
469
|
+
let row = this.readRun(runId);
|
|
470
|
+
if (!row) return null;
|
|
471
|
+
if (!TERMINAL.has(row.status)) {
|
|
472
|
+
const fiber = await this.inspectFiberByKey(runId);
|
|
473
|
+
if (
|
|
474
|
+
!fiber ||
|
|
475
|
+
fiber.status === "interrupted" ||
|
|
476
|
+
fiber.status === "error" ||
|
|
477
|
+
fiber.status === "completed"
|
|
478
|
+
) {
|
|
479
|
+
this.failRun(
|
|
480
|
+
runId,
|
|
481
|
+
"error",
|
|
482
|
+
fiber?.error ?? "SubAgent run was interrupted before completion",
|
|
483
|
+
);
|
|
484
|
+
} else if (fiber.status === "aborted") {
|
|
485
|
+
this.failRun(runId, "aborted", "SubAgent run aborted");
|
|
486
|
+
}
|
|
487
|
+
row = this.readRun(runId);
|
|
488
|
+
}
|
|
489
|
+
return row ? this.inspection(row) : null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async getAgentToolChunks(
|
|
493
|
+
runId: string,
|
|
494
|
+
options?: { afterSequence?: number },
|
|
495
|
+
): Promise<AgentToolStoredChunk[]> {
|
|
496
|
+
return this.readChunks(runId, options?.afterSequence);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async tailAgentToolRun(
|
|
500
|
+
runId: string,
|
|
501
|
+
options?: { afterSequence?: number; signal?: AbortSignal },
|
|
502
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
503
|
+
// Keep the replay read and live subscription in one synchronous section;
|
|
504
|
+
// otherwise a chunk emitted across an `await` could fall into the gap.
|
|
505
|
+
const chunks = this.readChunks(runId, options?.afterSequence);
|
|
506
|
+
const terminal = TERMINAL.has(
|
|
507
|
+
this.readRun(runId)?.status ?? "error",
|
|
508
|
+
);
|
|
509
|
+
let detach = () => {};
|
|
510
|
+
|
|
511
|
+
// Facet RPC only transports byte streams; runAgentTool decodes JSONL bytes.
|
|
512
|
+
const encode = (chunk: AgentToolStoredChunk) =>
|
|
513
|
+
new TextEncoder().encode(`${stringifyJson(chunk)}\n`);
|
|
514
|
+
|
|
515
|
+
return new ReadableStream({
|
|
516
|
+
type: "bytes",
|
|
517
|
+
start: (controller) => {
|
|
518
|
+
for (const chunk of chunks) controller.enqueue(encode(chunk));
|
|
519
|
+
if (terminal || options?.signal?.aborted) {
|
|
520
|
+
controller.close();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const subscriber: TailSubscriber = {
|
|
525
|
+
push: (chunk) => controller.enqueue(encode(chunk)),
|
|
526
|
+
close: () => {
|
|
527
|
+
detach();
|
|
528
|
+
controller.close();
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
const set =
|
|
532
|
+
this.subscribers.get(runId) ?? new Set<TailSubscriber>();
|
|
533
|
+
set.add(subscriber);
|
|
534
|
+
this.subscribers.set(runId, set);
|
|
535
|
+
|
|
536
|
+
const abort = () => {
|
|
537
|
+
detach();
|
|
538
|
+
controller.close();
|
|
539
|
+
};
|
|
540
|
+
detach = () => {
|
|
541
|
+
options?.signal?.removeEventListener("abort", abort);
|
|
542
|
+
const current = this.subscribers.get(runId);
|
|
543
|
+
current?.delete(subscriber);
|
|
544
|
+
if (current?.size === 0) this.subscribers.delete(runId);
|
|
545
|
+
};
|
|
546
|
+
options?.signal?.addEventListener("abort", abort, {
|
|
547
|
+
once: true,
|
|
548
|
+
});
|
|
549
|
+
},
|
|
550
|
+
cancel: () => detach(),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
override async onFiberRecovered(
|
|
555
|
+
context: FiberRecoveryContext,
|
|
556
|
+
): Promise<void | FiberRecoveryResult> {
|
|
557
|
+
if (context.name !== FIBER_NAME) return;
|
|
558
|
+
const runId =
|
|
559
|
+
typeof context.metadata?.runId === "string"
|
|
560
|
+
? context.metadata.runId
|
|
561
|
+
: context.idempotencyKey;
|
|
562
|
+
if (runId) {
|
|
563
|
+
this.failRun(
|
|
564
|
+
runId,
|
|
565
|
+
"error",
|
|
566
|
+
"SubAgent run was interrupted before completion",
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
status: "interrupted",
|
|
571
|
+
reason: "SubAgent run was interrupted before completion",
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|