pi-agent-replicate 0.0.0-stage → 1.4.1

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 (82) hide show
  1. package/AGENTS.md +27 -0
  2. package/CHANGELOG.md +152 -0
  3. package/CONTEXT.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +68 -2
  6. package/RELEASING.md +132 -0
  7. package/assets/banner.png +0 -0
  8. package/config.json.example +6 -0
  9. package/docs/README.md +23 -0
  10. package/docs/adr/0003-installable-role-packs.md +29 -0
  11. package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
  12. package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
  13. package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
  14. package/docs/configuration.md +14 -0
  15. package/docs/development.md +27 -0
  16. package/docs/public-subagent-tree-api.md +58 -0
  17. package/docs/retained-roles.md +71 -0
  18. package/docs/settled-lifecycle-contract.md +71 -0
  19. package/docs/worktree-subagents.md +58 -0
  20. package/examples/role-pack/extension.ts +18 -0
  21. package/examples/role-pack/package.json +16 -0
  22. package/examples/role-pack/roles/example-reviewer.md +12 -0
  23. package/index.d.ts +116 -0
  24. package/index.js +1064 -0
  25. package/index.ts +155 -0
  26. package/package.json +70 -4
  27. package/pi-extension/subagents/activity.js +575 -0
  28. package/pi-extension/subagents/activity.ts +980 -0
  29. package/pi-extension/subagents/completion.js +184 -0
  30. package/pi-extension/subagents/completion.ts +229 -0
  31. package/pi-extension/subagents/delivery-drain.js +15 -0
  32. package/pi-extension/subagents/delivery-drain.ts +20 -0
  33. package/pi-extension/subagents/delivery-files.js +86 -0
  34. package/pi-extension/subagents/delivery-files.ts +105 -0
  35. package/pi-extension/subagents/delivery-log.js +51 -0
  36. package/pi-extension/subagents/delivery-log.ts +51 -0
  37. package/pi-extension/subagents/delivery-payload.ts +55 -0
  38. package/pi-extension/subagents/delivery.ts +219 -0
  39. package/pi-extension/subagents/execution-owner.js +30 -0
  40. package/pi-extension/subagents/execution-owner.ts +38 -0
  41. package/pi-extension/subagents/herdr.js +645 -0
  42. package/pi-extension/subagents/herdr.ts +927 -0
  43. package/pi-extension/subagents/index.ts +3400 -0
  44. package/pi-extension/subagents/launch.js +774 -0
  45. package/pi-extension/subagents/launch.ts +1124 -0
  46. package/pi-extension/subagents/lifecycle.js +477 -0
  47. package/pi-extension/subagents/lifecycle.ts +582 -0
  48. package/pi-extension/subagents/lineage.js +806 -0
  49. package/pi-extension/subagents/lineage.ts +841 -0
  50. package/pi-extension/subagents/replica-background.js +32 -0
  51. package/pi-extension/subagents/replica-background.ts +38 -0
  52. package/pi-extension/subagents/replica-branch.ts +45 -0
  53. package/pi-extension/subagents/replica-directory-settings.ts +51 -0
  54. package/pi-extension/subagents/replica-history.js +22 -0
  55. package/pi-extension/subagents/replica-history.ts +21 -0
  56. package/pi-extension/subagents/replica-work.js +326 -0
  57. package/pi-extension/subagents/replica-work.ts +336 -0
  58. package/pi-extension/subagents/replica-worker.js +74 -0
  59. package/pi-extension/subagents/replica-worker.ts +71 -0
  60. package/pi-extension/subagents/replication.ts +99 -0
  61. package/pi-extension/subagents/result-delivery-queue.ts +33 -0
  62. package/pi-extension/subagents/role-catalog.js +123 -0
  63. package/pi-extension/subagents/role-catalog.ts +24 -0
  64. package/pi-extension/subagents/runtime-routing.js +154 -0
  65. package/pi-extension/subagents/runtime-routing.ts +239 -0
  66. package/pi-extension/subagents/session-binding.js +346 -0
  67. package/pi-extension/subagents/session-binding.ts +339 -0
  68. package/pi-extension/subagents/session-work.js +219 -0
  69. package/pi-extension/subagents/session-work.ts +167 -0
  70. package/pi-extension/subagents/session.js +318 -0
  71. package/pi-extension/subagents/session.ts +448 -0
  72. package/pi-extension/subagents/settled-contract.js +47 -0
  73. package/pi-extension/subagents/settled-contract.ts +175 -0
  74. package/pi-extension/subagents/source-state.js +180 -0
  75. package/pi-extension/subagents/source-state.ts +184 -0
  76. package/pi-extension/subagents/status.ts +493 -0
  77. package/pi-extension/subagents/subagent-done.js +805 -0
  78. package/pi-extension/subagents/subagent-done.ts +878 -0
  79. package/pi-extension/subagents/terminal.js +111 -0
  80. package/pi-extension/subagents/terminal.ts +208 -0
  81. package/pi-extension/subagents/watchdog.js +122 -0
  82. package/pi-extension/subagents/watchdog.ts +171 -0
@@ -0,0 +1,980 @@
1
+ import {
2
+ appendFileSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import type {
12
+ AssistantStopReason,
13
+ SettledOutcomeKind,
14
+ } from "./settled-contract.ts";
15
+
16
+ export type SubagentActivityPhase = "starting" | "active" | "waiting" | "done";
17
+ export type SubagentActivityScope =
18
+ "agent" | "turn" | "provider" | "streaming" | "tool" | "compaction";
19
+
20
+ /** Pi's reason for starting manual or automatic context compaction. */
21
+ export type SubagentCompactionReason = "manual" | "threshold" | "overflow";
22
+
23
+ export type SubagentActivityEvent =
24
+ | "session_start"
25
+ | "session_before_compact"
26
+ | "session_compact"
27
+ | "session_compact_failed"
28
+ | "input"
29
+ | "before_agent_start"
30
+ | "agent_start"
31
+ | "agent_end"
32
+ | "agent_settled"
33
+ | "turn_start"
34
+ | "turn_end"
35
+ | "before_provider_request"
36
+ | "after_provider_response"
37
+ | "message_update"
38
+ | "tool_execution_start"
39
+ | "tool_call"
40
+ | "tool_execution_update"
41
+ | "tool_result"
42
+ | "tool_execution_end"
43
+ | "caller_ping"
44
+ | "subagent_done"
45
+ | "session_shutdown";
46
+
47
+ export interface SubagentActivityState {
48
+ version: 1;
49
+ runningChildId: string;
50
+ createdAt: number;
51
+ updatedAt: number;
52
+ sequence: number;
53
+ latestEvent: SubagentActivityEvent;
54
+ phase: SubagentActivityPhase;
55
+ agentActive: boolean;
56
+ turnActive: boolean;
57
+ providerActive: boolean;
58
+ toolActive: boolean;
59
+
60
+ /** True from Pi's start hook until a success, failure, or abort hook arrives. */
61
+ compactionActive?: boolean;
62
+ /** Pi's latest manual, threshold, or overflow compaction reason. */
63
+ compactionReason?: SubagentCompactionReason;
64
+ /** Whether Pi will retry the interrupted run after this compaction. */
65
+ compactionWillRetry?: boolean;
66
+ /** Whether the latest failed compaction was cancelled or aborted. */
67
+ compactionAborted?: boolean;
68
+ /** Bounded diagnostic from the latest failed compaction. */
69
+ compactionErrorMessage?: string;
70
+ activeScope?: SubagentActivityScope;
71
+ activeSince?: number;
72
+ waitingSince?: number;
73
+ turnIndex?: number;
74
+ messageEventType?: string;
75
+ toolCallId?: string;
76
+ toolName?: string;
77
+ toolStartedAt?: number;
78
+ toolEndedAt?: number;
79
+ /** Outcome recorded at the fully-settled boundary. */
80
+ settledOutcome?: SettledOutcomeKind;
81
+ settledAssistantId?: string;
82
+ settledStopReason?: AssistantStopReason;
83
+ settledErrorMessage?: string;
84
+ settledEmpty?: boolean;
85
+ }
86
+
87
+ /** One durable child-to-parent settled boundary. It carries identity only; session JSONL holds response text. */
88
+ export interface SettledActivityEvent {
89
+ schema: 1;
90
+ childId: string;
91
+ sequence: number;
92
+ recordedAt: number;
93
+ turnIndex?: number;
94
+ assistantId?: string;
95
+ outcome: SettledOutcomeKind;
96
+ stopReason?: AssistantStopReason;
97
+ errorMessage?: string;
98
+ empty?: boolean;
99
+ }
100
+
101
+ export type ActivityReadResult =
102
+ | { ok: true; activity: SubagentActivityState }
103
+ | { ok: false; reason: "missing" | "invalid" | "wrong-id"; error?: string };
104
+
105
+ export type SubagentShutdownReason =
106
+ "quit" | "reload" | "new" | "resume" | "fork";
107
+
108
+ export interface SubagentActivityRecorder {
109
+ sessionStart(): void;
110
+ /** Mark Pi context compaction as active without starting an agent turn. */
111
+ compactionStart(details: {
112
+ reason: SubagentCompactionReason;
113
+ willRetry: boolean;
114
+ }): void;
115
+ /** End active compaction after Pi saves its summary. */
116
+ compactionSucceeded(details: {
117
+ reason: SubagentCompactionReason;
118
+ willRetry: boolean;
119
+ }): void;
120
+ /** End active compaction after Pi reports failure or abort. */
121
+ compactionFailed(details: {
122
+ reason: SubagentCompactionReason;
123
+ willRetry: boolean;
124
+ aborted: boolean;
125
+ errorMessage?: string;
126
+ }): void;
127
+ input(): void;
128
+ beforeAgentStart(): void;
129
+ agentStart(): void;
130
+ agentEndWaiting(): void;
131
+ agentEndDone(): void;
132
+ agentSettled(details: {
133
+ outcome: SettledOutcomeKind;
134
+ assistantId?: string;
135
+ stopReason?: AssistantStopReason;
136
+ errorMessage?: string;
137
+ empty?: boolean;
138
+ turnIndex?: number;
139
+ autoExit?: boolean;
140
+ /** Append this boundary to the parent-observed settled stream. */
141
+ publish?: boolean;
142
+ }): void;
143
+ turnStart(turnIndex?: number): void;
144
+ turnEnd(turnIndex?: number): void;
145
+ beforeProviderRequest(): void;
146
+ afterProviderResponse(): void;
147
+ messageUpdate(messageEventType?: string): void;
148
+ toolExecutionStart(toolCallId?: string, toolName?: string): void;
149
+ toolCall(toolCallId?: string, toolName?: string): void;
150
+ toolExecutionUpdate(toolCallId?: string, toolName?: string): void;
151
+ toolResult(toolCallId?: string, toolName?: string): void;
152
+ toolExecutionEnd(toolCallId?: string, toolName?: string): void;
153
+ callerPing(): void;
154
+ subagentDone(): void;
155
+ sessionShutdown(reason: SubagentShutdownReason): void;
156
+ }
157
+
158
+ const ACTIVITY_UPDATE_THROTTLE_MS = 500;
159
+ const MAX_WRITE_FAILURES = 3;
160
+ const KNOWN_PHASES = new Set<SubagentActivityPhase>([
161
+ "starting",
162
+ "active",
163
+ "waiting",
164
+ "done",
165
+ ]);
166
+ const KNOWN_SCOPES = new Set<SubagentActivityScope>([
167
+ "agent",
168
+ "turn",
169
+ "provider",
170
+ "streaming",
171
+ "tool",
172
+ "compaction",
173
+ ]);
174
+ const KNOWN_OUTCOMES = new Set<SettledOutcomeKind>([
175
+ "clean",
176
+ "empty",
177
+ "error",
178
+ "intentional-abort",
179
+ "unexpected-abort",
180
+ ]);
181
+ const KNOWN_COMPACTION_REASONS = new Set<SubagentCompactionReason>([
182
+ "manual",
183
+ "threshold",
184
+ "overflow",
185
+ ]);
186
+ const KNOWN_EVENTS = new Set<SubagentActivityEvent>([
187
+ "session_start",
188
+ "session_before_compact",
189
+ "session_compact",
190
+ "session_compact_failed",
191
+ "input",
192
+ "before_agent_start",
193
+ "agent_start",
194
+ "agent_end",
195
+ "agent_settled",
196
+ "turn_start",
197
+ "turn_end",
198
+ "before_provider_request",
199
+ "after_provider_response",
200
+ "message_update",
201
+ "tool_execution_start",
202
+ "tool_call",
203
+ "tool_execution_update",
204
+ "tool_result",
205
+ "tool_execution_end",
206
+ "caller_ping",
207
+ "subagent_done",
208
+ "session_shutdown",
209
+ ]);
210
+ const MAX_ACTIVITY_STRING_LENGTH = 200;
211
+
212
+ export function getSubagentActivityFile(
213
+ artifactDir: string,
214
+ runningChildId: string,
215
+ ): string {
216
+ return join(artifactDir, "subagent-activity", `${runningChildId}.json`);
217
+ }
218
+
219
+ /** Return the append-only settled-boundary log adjacent to a child activity file. */
220
+ export function getSubagentSettledEventsFile(
221
+ artifactDir: string,
222
+ runningChildId: string,
223
+ ): string {
224
+ return join(
225
+ artifactDir,
226
+ "subagent-activity",
227
+ `${runningChildId}.settled.jsonl`,
228
+ );
229
+ }
230
+
231
+ function isSettledActivityEvent(
232
+ value: unknown,
233
+ childId: string,
234
+ ): value is SettledActivityEvent {
235
+ const object = requireObject(value);
236
+ return (
237
+ !!object &&
238
+ object.schema === 1 &&
239
+ object.childId === childId &&
240
+ Number.isInteger(object.sequence) &&
241
+ Number.isFinite(object.recordedAt) &&
242
+ typeof object.outcome === "string" &&
243
+ KNOWN_OUTCOMES.has(object.outcome as SettledOutcomeKind) &&
244
+ (object.turnIndex == null || Number.isInteger(object.turnIndex)) &&
245
+ (object.assistantId == null || typeof object.assistantId === "string") &&
246
+ (object.stopReason == null || typeof object.stopReason === "string") &&
247
+ (object.errorMessage == null || typeof object.errorMessage === "string") &&
248
+ (object.empty == null || typeof object.empty === "boolean")
249
+ );
250
+ }
251
+
252
+ /**
253
+ * Return settled boundaries in their authoritative activity order. Publication
254
+ * order is not stable when observers race with the child writer.
255
+ */
256
+ export function orderSettledActivityEvents(
257
+ events: readonly SettledActivityEvent[],
258
+ ): SettledActivityEvent[] {
259
+ return [...events].sort(
260
+ (left, right) =>
261
+ left.sequence - right.sequence ||
262
+ (left.assistantId ?? "").localeCompare(right.assistantId ?? ""),
263
+ );
264
+ }
265
+
266
+ /** Read complete JSONL settled records and ignore one incomplete trailing line. */
267
+ export function readSubagentSettledEventsFile(
268
+ eventsFile: string,
269
+ expectedRunningChildId: string,
270
+ ): SettledActivityEvent[] {
271
+ if (!existsSync(eventsFile)) return [];
272
+ let text: string;
273
+ try {
274
+ text = readFileSync(eventsFile, "utf8");
275
+ } catch {
276
+ return [];
277
+ }
278
+ const lines = text.split("\n");
279
+ const events: SettledActivityEvent[] = [];
280
+ for (let index = 0; index < lines.length; index += 1) {
281
+ const line = lines[index]?.trim();
282
+ if (!line) continue;
283
+ try {
284
+ const parsed: unknown = JSON.parse(line);
285
+ if (isSettledActivityEvent(parsed, expectedRunningChildId))
286
+ events.push(parsed);
287
+ } catch {
288
+ if (index === lines.length - 1) break;
289
+ }
290
+ }
291
+ return events;
292
+ }
293
+
294
+ function requireObject(value: unknown): Record<string, unknown> | null {
295
+ if (value == null || typeof value !== "object" || Array.isArray(value))
296
+ return null;
297
+ return value as Record<string, unknown>;
298
+ }
299
+
300
+ function validateFiniteNumber(
301
+ object: Record<string, unknown>,
302
+ fieldName: string,
303
+ ): string | null {
304
+ return Number.isFinite(object[fieldName])
305
+ ? null
306
+ : `${fieldName} must be finite`;
307
+ }
308
+
309
+ function validateOptionalFiniteNumber(
310
+ object: Record<string, unknown>,
311
+ fieldName: string,
312
+ ): string | null {
313
+ const value = object[fieldName];
314
+ return value == null || Number.isFinite(value)
315
+ ? null
316
+ : `${fieldName} must be finite when present`;
317
+ }
318
+
319
+ function validateInteger(
320
+ object: Record<string, unknown>,
321
+ fieldName: string,
322
+ ): string | null {
323
+ return Number.isInteger(object[fieldName])
324
+ ? null
325
+ : `${fieldName} must be an integer`;
326
+ }
327
+
328
+ function validateOptionalInteger(
329
+ object: Record<string, unknown>,
330
+ fieldName: string,
331
+ ): string | null {
332
+ const value = object[fieldName];
333
+ return value == null || Number.isInteger(value)
334
+ ? null
335
+ : `${fieldName} must be an integer when present`;
336
+ }
337
+
338
+ function validateBoolean(
339
+ object: Record<string, unknown>,
340
+ fieldName: string,
341
+ ): string | null {
342
+ return typeof object[fieldName] === "boolean"
343
+ ? null
344
+ : `${fieldName} must be a boolean`;
345
+ }
346
+
347
+ function validateOptionalBoolean(
348
+ object: Record<string, unknown>,
349
+ fieldName: string,
350
+ ): string | null {
351
+ const value = object[fieldName];
352
+ return value == null || typeof value === "boolean"
353
+ ? null
354
+ : `${fieldName} must be a boolean when present`;
355
+ }
356
+
357
+ function validateOptionalActivityString(
358
+ object: Record<string, unknown>,
359
+ fieldName: string,
360
+ ): string | null {
361
+ const value = object[fieldName];
362
+ if (value == null) return null;
363
+ if (typeof value !== "string")
364
+ return `${fieldName} must be a string when present`;
365
+ if (/\r|\n/.test(value)) return `${fieldName} must not contain newlines`;
366
+ return value.length <= MAX_ACTIVITY_STRING_LENGTH
367
+ ? null
368
+ : `${fieldName} is too long`;
369
+ }
370
+
371
+ function invalidActivity(error: string): ActivityReadResult {
372
+ return { ok: false, reason: "invalid", error };
373
+ }
374
+
375
+ function validateActivity(
376
+ value: unknown,
377
+ expectedRunningChildId: string,
378
+ ): ActivityReadResult {
379
+ const object = requireObject(value);
380
+ if (!object) return invalidActivity("activity must be an object");
381
+ if (object.version !== 1)
382
+ return invalidActivity("unsupported activity version");
383
+ if (typeof object.runningChildId !== "string")
384
+ return invalidActivity("runningChildId must be a string");
385
+ if (object.runningChildId !== expectedRunningChildId)
386
+ return { ok: false, reason: "wrong-id" };
387
+ if (
388
+ typeof object.latestEvent !== "string" ||
389
+ !KNOWN_EVENTS.has(object.latestEvent as SubagentActivityEvent)
390
+ ) {
391
+ return invalidActivity("unknown latestEvent");
392
+ }
393
+ if (
394
+ typeof object.phase !== "string" ||
395
+ !KNOWN_PHASES.has(object.phase as SubagentActivityPhase)
396
+ ) {
397
+ return invalidActivity("unknown activity phase");
398
+ }
399
+ if (
400
+ object.activeScope != null &&
401
+ (typeof object.activeScope !== "string" ||
402
+ !KNOWN_SCOPES.has(object.activeScope as SubagentActivityScope))
403
+ ) {
404
+ return invalidActivity("unknown activeScope");
405
+ }
406
+
407
+ if (
408
+ object.compactionReason != null &&
409
+ (typeof object.compactionReason !== "string" ||
410
+ !KNOWN_COMPACTION_REASONS.has(object.compactionReason as SubagentCompactionReason))
411
+ ) {
412
+ return invalidActivity("unknown compactionReason");
413
+ }
414
+
415
+ if (
416
+ object.settledOutcome != null &&
417
+ (typeof object.settledOutcome !== "string" ||
418
+ !KNOWN_OUTCOMES.has(object.settledOutcome as SettledOutcomeKind))
419
+ ) {
420
+ return invalidActivity("unknown settledOutcome");
421
+ }
422
+ const settledEmptyError =
423
+ object.settledEmpty != null && typeof object.settledEmpty !== "boolean"
424
+ ? "settledEmpty must be a boolean when present"
425
+ : null;
426
+ if (settledEmptyError) return invalidActivity(settledEmptyError);
427
+ const validationError = [
428
+ validateFiniteNumber(object, "createdAt"),
429
+ validateFiniteNumber(object, "updatedAt"),
430
+ validateInteger(object, "sequence"),
431
+ validateBoolean(object, "agentActive"),
432
+ validateBoolean(object, "turnActive"),
433
+ validateBoolean(object, "providerActive"),
434
+ validateBoolean(object, "toolActive"),
435
+ validateOptionalBoolean(object, "compactionActive"),
436
+ validateOptionalBoolean(object, "compactionWillRetry"),
437
+ validateOptionalBoolean(object, "compactionAborted"),
438
+ validateOptionalActivityString(object, "compactionErrorMessage"),
439
+ validateOptionalFiniteNumber(object, "activeSince"),
440
+ validateOptionalFiniteNumber(object, "waitingSince"),
441
+ validateOptionalInteger(object, "turnIndex"),
442
+ validateOptionalFiniteNumber(object, "toolStartedAt"),
443
+ validateOptionalFiniteNumber(object, "toolEndedAt"),
444
+ validateOptionalActivityString(object, "messageEventType"),
445
+ validateOptionalActivityString(object, "toolCallId"),
446
+ validateOptionalActivityString(object, "toolName"),
447
+ validateOptionalActivityString(object, "settledAssistantId"),
448
+ validateOptionalActivityString(object, "settledStopReason"),
449
+ validateOptionalActivityString(object, "settledErrorMessage"),
450
+ ].find((error) => error != null);
451
+ if (validationError) return invalidActivity(validationError);
452
+
453
+ return { ok: true, activity: object as unknown as SubagentActivityState };
454
+ }
455
+
456
+ export function readSubagentActivityFile(
457
+ activityFile: string,
458
+ expectedRunningChildId: string,
459
+ ): ActivityReadResult {
460
+ if (!existsSync(activityFile)) return { ok: false, reason: "missing" };
461
+
462
+ let parsed: unknown;
463
+ try {
464
+ parsed = JSON.parse(readFileSync(activityFile, "utf8"));
465
+ } catch (error) {
466
+ const message = error instanceof Error ? error.message : String(error);
467
+ return { ok: false, reason: "invalid", error: message };
468
+ }
469
+
470
+ return validateActivity(parsed, expectedRunningChildId);
471
+ }
472
+
473
+ export function writeSubagentActivityFile(
474
+ activityFile: string,
475
+ activity: SubagentActivityState,
476
+ ): void {
477
+ const dir = dirname(activityFile);
478
+ mkdirSync(dir, { recursive: true });
479
+ const tempFile = join(
480
+ dir,
481
+ `${activity.runningChildId}.json.${process.pid}.${activity.sequence}.tmp`,
482
+ );
483
+
484
+ try {
485
+ writeFileSync(tempFile, `${JSON.stringify(activity)}\n`, "utf8");
486
+ renameSync(tempFile, activityFile);
487
+ } catch (error) {
488
+ try {
489
+ unlinkSync(tempFile);
490
+ } catch (cleanupError) {
491
+ // Temp cleanup is best effort; preserve the original write/rename failure
492
+ void cleanupError;
493
+ }
494
+ throw error;
495
+ }
496
+ }
497
+
498
+ function createNoopRecorder(): SubagentActivityRecorder {
499
+ return {
500
+ sessionStart() {},
501
+ compactionStart() {},
502
+ compactionSucceeded() {},
503
+ compactionFailed() {},
504
+ input() {},
505
+ beforeAgentStart() {},
506
+ agentStart() {},
507
+ agentEndWaiting() {},
508
+ agentEndDone() {},
509
+ agentSettled() {},
510
+ turnStart() {},
511
+ turnEnd() {},
512
+ beforeProviderRequest() {},
513
+ afterProviderResponse() {},
514
+ messageUpdate() {},
515
+ toolExecutionStart() {},
516
+ toolCall() {},
517
+ toolExecutionUpdate() {},
518
+ toolResult() {},
519
+ toolExecutionEnd() {},
520
+ callerPing() {},
521
+ subagentDone() {},
522
+ sessionShutdown() {},
523
+ };
524
+ }
525
+
526
+ function clearActiveState(activity: SubagentActivityState): void {
527
+ activity.agentActive = false;
528
+ activity.turnActive = false;
529
+ activity.providerActive = false;
530
+ activity.toolActive = false;
531
+ activity.compactionActive = false;
532
+ delete activity.activeScope;
533
+ delete activity.activeSince;
534
+ }
535
+
536
+ function refreshActiveScope(activity: SubagentActivityState): void {
537
+ if (activity.compactionActive) {
538
+ activity.phase = "active";
539
+ activity.activeScope = "compaction";
540
+ return;
541
+ }
542
+ if (activity.toolActive) {
543
+ activity.phase = "active";
544
+ activity.activeScope = "tool";
545
+ return;
546
+ }
547
+ if (activity.providerActive) {
548
+ activity.phase = "active";
549
+ activity.activeScope = "provider";
550
+ return;
551
+ }
552
+ if (activity.turnActive) {
553
+ activity.phase = "active";
554
+ activity.activeScope = "turn";
555
+ return;
556
+ }
557
+ if (activity.agentActive) {
558
+ activity.phase = "active";
559
+ activity.activeScope = "agent";
560
+ return;
561
+ }
562
+ delete activity.activeScope;
563
+ delete activity.activeSince;
564
+ }
565
+
566
+ function markActive(
567
+ activity: SubagentActivityState,
568
+ scope: SubagentActivityScope,
569
+ now: number,
570
+ resetActiveSince = false,
571
+ ): void {
572
+ activity.phase = "active";
573
+ activity.activeScope = scope;
574
+ if (activity.activeSince == null || resetActiveSince)
575
+ activity.activeSince = now;
576
+ delete activity.waitingSince;
577
+ }
578
+
579
+ function finishCompactionActivity(
580
+ activity: SubagentActivityState,
581
+ observedAt: number,
582
+ ): void {
583
+ activity.compactionActive = false;
584
+ refreshActiveScope(activity);
585
+ if (activity.activeScope) return;
586
+ activity.phase = "waiting";
587
+ activity.waitingSince = observedAt;
588
+ }
589
+
590
+ export function createSubagentActivityRecorder(params: {
591
+ runningChildId?: string;
592
+ activityFile?: string;
593
+ settledEventsFile?: string;
594
+ now?: () => number;
595
+ }): SubagentActivityRecorder {
596
+ const runningChildId = params.runningChildId?.trim();
597
+ const activityFile = params.activityFile?.trim();
598
+ const settledEventsFile = params.settledEventsFile?.trim();
599
+ if (!runningChildId || !activityFile) return createNoopRecorder();
600
+ const activityPath = activityFile;
601
+
602
+ const now = params.now ?? (() => Date.now());
603
+ const createdAt = now();
604
+ const activity: SubagentActivityState = {
605
+ version: 1,
606
+ runningChildId,
607
+ createdAt,
608
+ updatedAt: createdAt,
609
+ sequence: 0,
610
+ latestEvent: "session_start",
611
+ phase: "starting",
612
+ agentActive: false,
613
+ turnActive: false,
614
+ providerActive: false,
615
+ toolActive: false,
616
+ compactionActive: false,
617
+ };
618
+
619
+ let disabled = false;
620
+ let failureCount = 0;
621
+ let lastFlushAt = 0;
622
+ let pendingFlush: ReturnType<typeof setTimeout> | null = null;
623
+
624
+ function clearPendingFlush(): void {
625
+ if (!pendingFlush) return;
626
+ clearTimeout(pendingFlush);
627
+ pendingFlush = null;
628
+ }
629
+
630
+ function disable(): void {
631
+ disabled = true;
632
+ clearPendingFlush();
633
+ }
634
+
635
+ function flushNow(): void {
636
+ if (disabled) return;
637
+ try {
638
+ writeSubagentActivityFile(activityPath, activity);
639
+ lastFlushAt = now();
640
+ failureCount = 0;
641
+ } catch {
642
+ failureCount += 1;
643
+ if (failureCount >= MAX_WRITE_FAILURES) disable();
644
+ }
645
+ }
646
+
647
+ function scheduleFlush(): void {
648
+ if (disabled || pendingFlush) return;
649
+
650
+ const remainingMs = Math.max(
651
+ 0,
652
+ ACTIVITY_UPDATE_THROTTLE_MS - (now() - lastFlushAt),
653
+ );
654
+ if (remainingMs === 0) {
655
+ flushNow();
656
+ return;
657
+ }
658
+
659
+ pendingFlush = setTimeout(() => {
660
+ pendingFlush = null;
661
+ flushNow();
662
+ }, remainingMs);
663
+ }
664
+
665
+ function record(
666
+ latestEvent: SubagentActivityEvent,
667
+ update: (current: SubagentActivityState, now: number) => void,
668
+ flush: "immediate" | "throttled",
669
+ ): void {
670
+ if (disabled) return;
671
+ if (flush === "immediate") clearPendingFlush();
672
+
673
+ const observedAt = now();
674
+ activity.latestEvent = latestEvent;
675
+ activity.updatedAt = observedAt;
676
+ activity.sequence += 1;
677
+ update(activity, observedAt);
678
+
679
+ if (flush === "immediate") flushNow();
680
+ else scheduleFlush();
681
+ }
682
+
683
+ function markDone(latestEvent: SubagentActivityEvent): void {
684
+ record(
685
+ latestEvent,
686
+ (current) => {
687
+ current.phase = "done";
688
+ clearActiveState(current);
689
+ delete current.waitingSince;
690
+ },
691
+ "immediate",
692
+ );
693
+ disable();
694
+ }
695
+
696
+ return {
697
+ sessionStart() {
698
+ record(
699
+ "session_start",
700
+ (current) => {
701
+ current.phase = "starting";
702
+ clearActiveState(current);
703
+ delete current.waitingSince;
704
+ },
705
+ "immediate",
706
+ );
707
+ },
708
+ compactionStart(details) {
709
+ record(
710
+ "session_before_compact",
711
+ (current, observedAt) => {
712
+ current.compactionActive = true;
713
+ current.compactionReason = details.reason;
714
+ current.compactionWillRetry = details.willRetry;
715
+ delete current.compactionAborted;
716
+ delete current.compactionErrorMessage;
717
+ markActive(current, "compaction", observedAt, true);
718
+ },
719
+ "immediate",
720
+ );
721
+ },
722
+ compactionSucceeded(details) {
723
+ record(
724
+ "session_compact",
725
+ (current, observedAt) => {
726
+ current.compactionReason = details.reason;
727
+ current.compactionWillRetry = details.willRetry;
728
+ delete current.compactionAborted;
729
+ delete current.compactionErrorMessage;
730
+ finishCompactionActivity(current, observedAt);
731
+ },
732
+ "immediate",
733
+ );
734
+ },
735
+ compactionFailed(details) {
736
+ record(
737
+ "session_compact_failed",
738
+ (current, observedAt) => {
739
+ current.compactionReason = details.reason;
740
+ current.compactionWillRetry = details.willRetry;
741
+ current.compactionAborted = details.aborted;
742
+ current.compactionErrorMessage = details.errorMessage?.slice(
743
+ 0,
744
+ MAX_ACTIVITY_STRING_LENGTH,
745
+ );
746
+ finishCompactionActivity(current, observedAt);
747
+ },
748
+ "immediate",
749
+ );
750
+ },
751
+ input() {
752
+ record("input", () => {}, "immediate");
753
+ },
754
+ beforeAgentStart() {
755
+ record(
756
+ "before_agent_start",
757
+ (current, observedAt) => {
758
+ current.agentActive = true;
759
+ markActive(current, "agent", observedAt);
760
+ },
761
+ "immediate",
762
+ );
763
+ },
764
+ agentStart() {
765
+ record(
766
+ "agent_start",
767
+ (current, observedAt) => {
768
+ current.agentActive = true;
769
+ markActive(current, "agent", observedAt);
770
+ },
771
+ "immediate",
772
+ );
773
+ },
774
+ agentEndWaiting() {
775
+ record(
776
+ "agent_end",
777
+ (current, observedAt) => {
778
+ clearActiveState(current);
779
+ current.phase = "waiting";
780
+ current.waitingSince = observedAt;
781
+ },
782
+ "immediate",
783
+ );
784
+ },
785
+ agentEndDone() {
786
+ markDone("agent_end");
787
+ },
788
+ agentSettled(details) {
789
+ const autoExit =
790
+ details.autoExit === true &&
791
+ (details.outcome === "clean" || details.outcome === "empty");
792
+ record(
793
+ "agent_settled",
794
+ (current, observedAt) => {
795
+ clearActiveState(current);
796
+ current.phase = autoExit ? "done" : "waiting";
797
+ if (autoExit) delete current.waitingSince;
798
+ else current.waitingSince = observedAt;
799
+ current.settledOutcome = details.outcome;
800
+ current.settledAssistantId = details.assistantId;
801
+ current.settledStopReason = details.stopReason;
802
+ current.settledErrorMessage = details.errorMessage;
803
+ current.settledEmpty = details.empty;
804
+ if (details.turnIndex != null) current.turnIndex = details.turnIndex;
805
+ },
806
+ "immediate",
807
+ );
808
+ if (settledEventsFile && details.publish !== false) {
809
+ try {
810
+ mkdirSync(dirname(settledEventsFile), { recursive: true });
811
+ const event: SettledActivityEvent = {
812
+ schema: 1,
813
+ childId: runningChildId,
814
+ sequence: activity.sequence,
815
+ recordedAt: activity.updatedAt,
816
+ outcome: details.outcome,
817
+ ...(details.turnIndex == null
818
+ ? {}
819
+ : { turnIndex: details.turnIndex }),
820
+ ...(details.assistantId
821
+ ? { assistantId: details.assistantId }
822
+ : {}),
823
+ ...(details.stopReason ? { stopReason: details.stopReason } : {}),
824
+ ...(details.errorMessage
825
+ ? {
826
+ errorMessage: details.errorMessage.slice(
827
+ 0,
828
+ MAX_ACTIVITY_STRING_LENGTH,
829
+ ),
830
+ }
831
+ : {}),
832
+ ...(details.empty == null ? {} : { empty: details.empty }),
833
+ };
834
+ appendFileSync(
835
+ settledEventsFile,
836
+ `${JSON.stringify(event)}\n`,
837
+ "utf8",
838
+ );
839
+ } catch {
840
+ // The status snapshot remains available if the durable bridge cannot be written.
841
+ }
842
+ }
843
+ if (autoExit) disable();
844
+ },
845
+ turnStart(turnIndex) {
846
+ record(
847
+ "turn_start",
848
+ (current, observedAt) => {
849
+ current.agentActive = true;
850
+ current.turnActive = true;
851
+ if (turnIndex != null) current.turnIndex = turnIndex;
852
+ markActive(
853
+ current,
854
+ current.toolActive || current.providerActive
855
+ ? (current.activeScope ?? "turn")
856
+ : "turn",
857
+ observedAt,
858
+ );
859
+ },
860
+ "immediate",
861
+ );
862
+ },
863
+ turnEnd(turnIndex) {
864
+ record(
865
+ "turn_end",
866
+ (current) => {
867
+ current.turnActive = false;
868
+ current.providerActive = false;
869
+ current.toolActive = false;
870
+ if (turnIndex != null) current.turnIndex = turnIndex;
871
+ refreshActiveScope(current);
872
+ },
873
+ "immediate",
874
+ );
875
+ },
876
+ beforeProviderRequest() {
877
+ record(
878
+ "before_provider_request",
879
+ (current, observedAt) => {
880
+ current.providerActive = true;
881
+ markActive(current, "provider", observedAt, true);
882
+ },
883
+ "immediate",
884
+ );
885
+ },
886
+ afterProviderResponse() {
887
+ record(
888
+ "after_provider_response",
889
+ (current) => {
890
+ current.providerActive = false;
891
+ refreshActiveScope(current);
892
+ },
893
+ "immediate",
894
+ );
895
+ },
896
+ messageUpdate(messageEventType) {
897
+ record(
898
+ "message_update",
899
+ (current, observedAt) => {
900
+ current.agentActive = true;
901
+ current.turnActive = true;
902
+ current.messageEventType = messageEventType;
903
+ if (!current.toolActive) markActive(current, "streaming", observedAt);
904
+ },
905
+ "throttled",
906
+ );
907
+ },
908
+ toolExecutionStart(toolCallId, toolName) {
909
+ record(
910
+ "tool_execution_start",
911
+ (current, observedAt) => {
912
+ current.toolActive = true;
913
+ current.toolCallId = toolCallId;
914
+ current.toolName = toolName;
915
+ current.toolStartedAt = observedAt;
916
+ markActive(current, "tool", observedAt, true);
917
+ },
918
+ "immediate",
919
+ );
920
+ },
921
+ toolCall(toolCallId, toolName) {
922
+ record(
923
+ "tool_call",
924
+ (current, observedAt) => {
925
+ current.toolActive = true;
926
+ current.toolCallId = toolCallId ?? current.toolCallId;
927
+ current.toolName = toolName ?? current.toolName;
928
+ markActive(current, "tool", observedAt);
929
+ },
930
+ "immediate",
931
+ );
932
+ },
933
+ toolExecutionUpdate(toolCallId, toolName) {
934
+ record(
935
+ "tool_execution_update",
936
+ (current, observedAt) => {
937
+ current.toolActive = true;
938
+ current.toolCallId = toolCallId ?? current.toolCallId;
939
+ current.toolName = toolName ?? current.toolName;
940
+ markActive(current, "tool", observedAt);
941
+ },
942
+ "throttled",
943
+ );
944
+ },
945
+ toolResult(toolCallId, toolName) {
946
+ record(
947
+ "tool_result",
948
+ (current) => {
949
+ current.toolCallId = toolCallId ?? current.toolCallId;
950
+ current.toolName = toolName ?? current.toolName;
951
+ refreshActiveScope(current);
952
+ },
953
+ "immediate",
954
+ );
955
+ },
956
+ toolExecutionEnd(toolCallId, toolName) {
957
+ record(
958
+ "tool_execution_end",
959
+ (current, observedAt) => {
960
+ current.toolActive = false;
961
+ current.toolCallId = toolCallId ?? current.toolCallId;
962
+ current.toolName = toolName ?? current.toolName;
963
+ current.toolEndedAt = observedAt;
964
+ refreshActiveScope(current);
965
+ },
966
+ "immediate",
967
+ );
968
+ },
969
+ callerPing() {
970
+ markDone("caller_ping");
971
+ },
972
+ subagentDone() {
973
+ markDone("subagent_done");
974
+ },
975
+ sessionShutdown(reason) {
976
+ if (reason === "quit") markDone("session_shutdown");
977
+ else disable();
978
+ },
979
+ };
980
+ }