@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +12 -3
  2. package/src/adapter/cloudflare/index.ts +56 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
  4. package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -0
  5. package/src/adapter/cloudflare/sandbox/id.ts +23 -0
  6. package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
  7. package/src/adapter/cloudflare/subagent/definition.ts +574 -0
  8. package/src/adapter/cloudflare/subagent/runner.ts +175 -0
  9. package/src/adapter/cloudflare/subagent/tools.ts +254 -0
  10. package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
  13. package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
  14. package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
  15. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
  16. package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
  17. package/src/agent-tool-runtime.ts +152 -0
  18. package/src/db/agent-tool.repo.ts +27 -0
  19. package/src/db/index.ts +33 -0
  20. package/src/db/interaction.repo.ts +185 -0
  21. package/src/db/schema.ts +25 -1
  22. package/src/db/submission.repo.ts +63 -1
  23. package/src/index.ts +57 -21
  24. package/src/kernel/approval-lifecycle.ts +41 -6
  25. package/src/kernel/bindings.ts +73 -9
  26. package/src/kernel/interaction-lifecycle.ts +395 -0
  27. package/src/kernel/public-contracts.ts +2 -0
  28. package/src/kernel/recoverable-chat-agent.ts +104 -6
  29. package/src/kernel/runtime-assembly-view.ts +37 -0
  30. package/src/kernel/runtime-assembly.ts +41 -0
  31. package/src/kernel/runtime-config.ts +4 -0
  32. package/src/kernel/runtime-load.ts +191 -0
  33. package/src/kernel/state.ts +12 -1
  34. package/src/kernel/submission-lifecycle.ts +33 -2
  35. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  36. package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
  37. package/src/lib/mcp.ts +7 -3
  38. package/src/lib/prompt.ts +4 -1
  39. package/src/lib/telemetry-dev.ts +7 -4
  40. package/src/pi/assembly/context.ts +3 -3
  41. package/src/pi/assembly/extensions.ts +11 -22
  42. package/src/pi/assembly/snapshot.ts +6 -3
  43. package/src/pi/message/contract.ts +7 -0
  44. package/src/pi/message/conversion.ts +9 -1
  45. package/src/pi/runtime-adapter/assembly.ts +26 -31
  46. package/src/pi/runtime-adapter/execution.ts +198 -15
  47. package/src/pi/runtime-adapter/index.ts +24 -8
  48. package/src/pi/runtime-adapter/models.ts +382 -35
  49. package/src/pi/runtime-adapter/recovery.ts +188 -1
  50. package/src/pi/runtime-adapter/transcript.ts +61 -3
  51. package/src/pi/tool/ai-adapter.ts +58 -1
  52. package/src/pi/tool/base.ts +190 -12
  53. package/src/pi/tool/compiler.ts +34 -1
  54. package/src/pi/tool/core-host.ts +19 -24
  55. package/src/pi/tool/core.ts +30 -120
  56. package/src/pi/tool/gateway.ts +54 -0
  57. package/src/pi/tool/index.ts +2 -0
  58. package/src/pi/tool/mcp.ts +96 -68
  59. package/src/pi/tool/schedule.ts +41 -19
  60. package/src/pi/tool/skill.ts +126 -420
  61. package/src/pi/tool/subagent.ts +14 -2
  62. package/src/pi/tool/web-fetch.ts +281 -0
  63. package/src/pi/tool/web-search/api.ts +34 -18
  64. package/src/pi/tool/web-search/web-search.ts +0 -1
  65. package/src/pi/tool/workspace-revision.ts +64 -0
  66. package/src/pi/tool/workspace-sandbox.ts +105 -263
  67. package/src/pi/turn/index.ts +20 -0
  68. package/src/pi/turn/interaction.ts +181 -0
  69. package/src/pi/turn/tool-recovery.ts +244 -1
  70. package/src/runtime-agent-context.ts +112 -0
  71. package/src/runtime-agent.ts +568 -321
  72. package/src/{plugins.ts → runtime-assembler.ts} +372 -398
  73. package/src/runtime-definition.ts +175 -0
  74. package/src/runtime.ts +835 -204
  75. package/src/tool-registry.ts +143 -0
  76. package/src/workspace-versioning.ts +46 -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
+ }