@jopqior/pi-subagents 1.0.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +2705 -0
  2. package/LICENSE +21 -0
  3. package/README.md +503 -0
  4. package/dist/public.d.ts +331 -0
  5. package/dist/settings.d.ts +82 -0
  6. package/docs/architecture/architecture.md +1566 -0
  7. package/docs/architecture/client-server-opportunities.md +127 -0
  8. package/docs/architecture/history/phase-1-api-boundary.md +8 -0
  9. package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
  10. package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
  11. package/docs/architecture/history/phase-12-complexity-test-fixtures.md +55 -0
  12. package/docs/architecture/history/phase-13-remaining-smells.md +88 -0
  13. package/docs/architecture/history/phase-14-strip-policy.md +49 -0
  14. package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
  15. package/docs/architecture/history/phase-16-invert-dependencies.md +144 -0
  16. package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
  17. package/docs/architecture/history/phase-18-reconsider-ui.md +166 -0
  18. package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
  19. package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
  20. package/docs/architecture/history/phase-20-result-delivery.md +245 -0
  21. package/docs/architecture/history/phase-21-classification-model-boundary.md +107 -0
  22. package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
  23. package/docs/architecture/history/phase-4-implement-service.md +8 -0
  24. package/docs/architecture/history/phase-5-decompose-index.md +42 -0
  25. package/docs/architecture/history/phase-7-encapsulation.md +173 -0
  26. package/docs/architecture/history/phase-8-testability.md +103 -0
  27. package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
  28. package/docs/comparison-with-upstream.md +77 -0
  29. package/docs/configuration.md +364 -0
  30. package/docs/decisions/0001-deferred-patches.md +80 -0
  31. package/docs/decisions/0002-extensions-on-a-minimal-core.md +125 -0
  32. package/docs/decisions/0003-publish-bundled-type-declarations.md +71 -0
  33. package/docs/decisions/0004-reconsider-ui-direction.md +279 -0
  34. package/docs/decisions/0005-subagent-record-admission-policy.md +106 -0
  35. package/docs/decisions/0006-inherited-prompt-is-identity-only.md +104 -0
  36. package/docs/decisions/0007-transcript-viewer-is-not-an-overlay.md +228 -0
  37. package/docs/decisions/0008-inherited-region-is-shared-parts.md +81 -0
  38. package/docs/decisions/0009-portable-inheritance-is-provider-scoped.md +116 -0
  39. package/package.json +91 -0
  40. package/src/config/agent-types.ts +135 -0
  41. package/src/config/custom-agents.ts +151 -0
  42. package/src/config/default-agents.ts +121 -0
  43. package/src/config/invocation-config.ts +167 -0
  44. package/src/config/thinking-level.ts +58 -0
  45. package/src/debug.ts +14 -0
  46. package/src/handlers/index.ts +3 -0
  47. package/src/handlers/interrupt.ts +58 -0
  48. package/src/handlers/lifecycle.ts +71 -0
  49. package/src/handlers/widget-events.ts +49 -0
  50. package/src/index.ts +292 -0
  51. package/src/layered-settings.ts +105 -0
  52. package/src/lifecycle/child-lifecycle.ts +115 -0
  53. package/src/lifecycle/child-shutdown.ts +105 -0
  54. package/src/lifecycle/concurrency-limiter.ts +55 -0
  55. package/src/lifecycle/create-subagent-session.ts +335 -0
  56. package/src/lifecycle/parent-snapshot.ts +119 -0
  57. package/src/lifecycle/run-listeners.ts +37 -0
  58. package/src/lifecycle/selection-scope.ts +116 -0
  59. package/src/lifecycle/spawn-selection.ts +259 -0
  60. package/src/lifecycle/subagent-manager.ts +546 -0
  61. package/src/lifecycle/subagent-session.ts +347 -0
  62. package/src/lifecycle/subagent-state.ts +404 -0
  63. package/src/lifecycle/subagent.ts +885 -0
  64. package/src/lifecycle/turn-limits.ts +13 -0
  65. package/src/lifecycle/usage.ts +60 -0
  66. package/src/lifecycle/workspace-bracket.ts +76 -0
  67. package/src/lifecycle/workspace.ts +46 -0
  68. package/src/observation/composite-subagent-observer.ts +74 -0
  69. package/src/observation/notification.ts +430 -0
  70. package/src/observation/outcome-delivery.ts +239 -0
  71. package/src/observation/record-observer.ts +78 -0
  72. package/src/observation/renderer.ts +161 -0
  73. package/src/observation/subagent-events-observer.ts +148 -0
  74. package/src/runtime.ts +137 -0
  75. package/src/service/service-adapter.ts +201 -0
  76. package/src/service/service.ts +246 -0
  77. package/src/session/ask-parent-tool.ts +69 -0
  78. package/src/session/content-items.ts +53 -0
  79. package/src/session/context.ts +80 -0
  80. package/src/session/conversation.ts +49 -0
  81. package/src/session/env.ts +40 -0
  82. package/src/session/model-resolver.ts +126 -0
  83. package/src/session/notify-parent-tool.ts +83 -0
  84. package/src/session/package-exclusions.ts +75 -0
  85. package/src/session/prompts.ts +231 -0
  86. package/src/session/provider-inheritance.ts +56 -0
  87. package/src/session/selection-catalogue.ts +143 -0
  88. package/src/session/session-config.ts +202 -0
  89. package/src/session/session-dir.ts +38 -0
  90. package/src/settings.ts +447 -0
  91. package/src/tools/agent-tool.ts +305 -0
  92. package/src/tools/background-spawner.ts +83 -0
  93. package/src/tools/foreground-runner.ts +159 -0
  94. package/src/tools/get-result-renderer.ts +119 -0
  95. package/src/tools/get-result-report.ts +84 -0
  96. package/src/tools/get-result-tool.ts +192 -0
  97. package/src/tools/helpers.ts +118 -0
  98. package/src/tools/result-renderer.ts +153 -0
  99. package/src/tools/spawn-config.ts +192 -0
  100. package/src/tools/steer-tool.ts +109 -0
  101. package/src/types.ts +143 -0
  102. package/src/ui/agent-widget.ts +333 -0
  103. package/src/ui/bounded-lines.ts +45 -0
  104. package/src/ui/display.ts +180 -0
  105. package/src/ui/glyphs.ts +62 -0
  106. package/src/ui/session-navigation.ts +150 -0
  107. package/src/ui/session-navigator.ts +255 -0
  108. package/src/ui/subagents-settings.ts +179 -0
  109. package/src/ui/transcript-content.ts +374 -0
  110. package/src/ui/widget-renderer.ts +301 -0
@@ -0,0 +1,404 @@
1
+ /**
2
+ * subagent-state.ts — SubagentState value object: lifecycle status, metrics, and live activity.
3
+ *
4
+ * Owns the passive, readable state of a subagent — status, result, error,
5
+ * timestamps, stats (toolUses, lifetimeUsage, compactionCount), and live-activity
6
+ * fields (turnCount, activeTools, responseText) — together with the transition
7
+ * methods (markRunning, markCompleted, …), accumulation methods
8
+ * (incrementToolUses, addUsage, incrementCompactions), and live-activity
9
+ * transition methods (incrementTurnCount, addActiveTool, removeActiveTool,
10
+ * resetResponseText, appendResponseText) that mutate them.
11
+ *
12
+ * State is encapsulated behind getters; external code reads through them but
13
+ * mutates only via the transition/accumulation methods. The value object owns
14
+ * all of its own mutations — no field is written from outside.
15
+ *
16
+ * Subagent holds one of these privately and delegates its getters and mutation
17
+ * methods to it. Extracting it lets the lifecycle state machine and the
18
+ * session-event observer be unit-tested without constructing an executor.
19
+ */
20
+
21
+ import type { LifetimeUsage } from "#src/lifecycle/usage";
22
+ import { addUsage } from "#src/lifecycle/usage";
23
+
24
+ export type SubagentStatus =
25
+ | "queued"
26
+ | "running"
27
+ | "completed"
28
+ | "steered"
29
+ | "aborted"
30
+ | "stopped"
31
+ | "error";
32
+
33
+ // ---- Status classification predicates ----
34
+ // The single decision point for the re-derived status groupings. Instance
35
+ // methods on SubagentState delegate here; DTO consumers holding a bare
36
+ // SubagentStatus (no SubagentState instance) call these directly.
37
+
38
+ /** Running or queued — the agent is still live (started or awaiting a slot). */
39
+ export function isActiveStatus(status: SubagentStatus): boolean {
40
+ return status === "running" || status === "queued";
41
+ }
42
+
43
+ /** Terminated by error, abort, or external stop (excludes the successful `steered`). */
44
+ export function isTerminalErrorStatus(status: SubagentStatus): boolean {
45
+ return status === "error" || status === "stopped" || status === "aborted";
46
+ }
47
+
48
+ /** Actively running (excludes queued). */
49
+ export function isRunningStatus(status: SubagentStatus): boolean {
50
+ return status === "running";
51
+ }
52
+
53
+ /** One update a child sent during a run, and whether an announcement delivered it. */
54
+ interface RunUpdate {
55
+ message: string;
56
+ announced: boolean;
57
+ }
58
+
59
+ export interface SubagentStateInit {
60
+ status?: SubagentStatus;
61
+ result?: string;
62
+ /** The question the agent ended its turn with — an outcome fact, like result. */
63
+ pendingQuestion?: string;
64
+ /** What a teardown with no result text reported — an outcome fact, like result. */
65
+ workspaceNotice?: string;
66
+ error?: string;
67
+ /** Whether the agent was stopped before the limiter ever admitted it. */
68
+ stoppedWhileQueued?: boolean;
69
+ startedAt?: number;
70
+ completedAt?: number;
71
+ /** Time the parent collected the outcome; undefined = obligation still open. */
72
+ consumedAt?: number;
73
+ // Stats — seed a populated value without replaying the accumulation methods
74
+ toolUses?: number;
75
+ lifetimeUsage?: LifetimeUsage;
76
+ compactionCount?: number;
77
+ // Live activity — activeTools is seeded by name (each entry calls addActiveTool)
78
+ turnCount?: number;
79
+ activeTools?: string[];
80
+ responseText?: string;
81
+ }
82
+
83
+ export class SubagentState {
84
+ // Transition state — encapsulated behind getters, mutated only via transition methods
85
+ private _status: SubagentStatus;
86
+ get status(): SubagentStatus { return this._status; }
87
+
88
+ private _result?: string;
89
+ get result(): string | undefined { return this._result; }
90
+
91
+ private _error?: string;
92
+ get error(): string | undefined { return this._error; }
93
+
94
+ // Never-started marker — a queued agent stopped before its slot opened has no
95
+ // result, as distinct from a started agent that produced none.
96
+ private _stoppedWhileQueued: boolean;
97
+ get stoppedWhileQueued(): boolean { return this._stoppedWhileQueued; }
98
+
99
+ private _startedAt: number;
100
+ get startedAt(): number { return this._startedAt; }
101
+
102
+ private _completedAt?: number;
103
+ get completedAt(): number | undefined { return this._completedAt; }
104
+
105
+ // Result delivery — whether the parent has collected the outcome (orthogonal to status)
106
+ private _consumedAt?: number;
107
+ get consumedAt(): number | undefined { return this._consumedAt; }
108
+ get consumed(): boolean { return this._consumedAt != null; }
109
+
110
+ // Result delivery — whether a carrier has committed to delivering the outcome.
111
+ // Distinct from consumption in two ways. It is revocable, where consumption is
112
+ // a one-way latch that also times session retention. And it is scoped to the
113
+ // caller rather than the run: consumedAt records a delivery that has already
114
+ // happened, so a resume must clear it, while a claim records one that has not
115
+ // happened yet and stays live across the reset (see resetForResume).
116
+ // Transient runtime ownership, so deliberately not seedable via
117
+ // SubagentStateInit — a rehydrated record must not claim a carrier that no
118
+ // longer exists.
119
+ private _claimed = false;
120
+ get claimed(): boolean { return this._claimed; }
121
+
122
+ // The question this agent ended its turn with, if it declared one. Part of the
123
+ // outcome like _result, and set alongside it at the terminal transition.
124
+ private _pendingQuestion?: string;
125
+ get pendingQuestion(): string | undefined { return this._pendingQuestion; }
126
+
127
+ // What the workspace reported at a teardown with no result text to fold it
128
+ // into. Part of the outcome like _result, and set at the disposal that
129
+ // produced it. Undefined for a run whose addendum rode the result instead.
130
+ private _workspaceNotice?: string;
131
+ get workspaceNotice(): string | undefined { return this._workspaceNotice; }
132
+
133
+ // The updates the child sent during this run, each remembering whether the
134
+ // announcement channel delivered it — so a message reaches the parent once,
135
+ // through whichever channel could reach it, and no carrier repeats it.
136
+ // Scoped to the run, so it clears wherever a run begins.
137
+ // Transient runtime state, so deliberately not seedable via SubagentStateInit
138
+ // — a rehydrated record has no run to have produced these.
139
+ private _runUpdates: RunUpdate[] = [];
140
+ /** The updates no announcement delivered — what an outcome carrier must render. */
141
+ get runUpdates(): readonly string[] {
142
+ return this._runUpdates.filter((update) => !update.announced).map((update) => update.message);
143
+ }
144
+
145
+ // Stats — accumulated via mutation methods, readable via getters
146
+ private _toolUses: number;
147
+ get toolUses(): number { return this._toolUses; }
148
+
149
+ private _lifetimeUsage: LifetimeUsage;
150
+ get lifetimeUsage(): Readonly<LifetimeUsage> { return this._lifetimeUsage; }
151
+
152
+ private _compactionCount: number;
153
+ get compactionCount(): number { return this._compactionCount; }
154
+
155
+ // Live activity — accumulated via transition methods, readable via getters
156
+ private _turnCount: number;
157
+ get turnCount(): number { return this._turnCount; }
158
+
159
+ // The run is waiting for a human model/thinking selection. Private activity,
160
+ // never a public status: the record stays `running` while it waits, and the
161
+ // projections that display it read it as activity, like activeTools.
162
+ private _awaitingSelection = false;
163
+ get awaitingSelection(): boolean { return this._awaitingSelection; }
164
+ /** Record that this admitted run is waiting for a human selection. */
165
+ markAwaitingSelection(): void { this._awaitingSelection = true; }
166
+ /** Clear the waiting marker — the selection resolved, failed, or the run ended. */
167
+ clearAwaitingSelection(): void { this._awaitingSelection = false; }
168
+
169
+ private _activeTools = new Map<string, string>();
170
+ get activeTools(): ReadonlyMap<string, string> { return this._activeTools; }
171
+
172
+ private _toolKeySeq = 0;
173
+
174
+ private _responseText: string;
175
+ get responseText(): string { return this._responseText; }
176
+
177
+ constructor(init: SubagentStateInit = {}) {
178
+ this._status = init.status ?? "queued";
179
+ this._result = init.result;
180
+ this._pendingQuestion = init.pendingQuestion;
181
+ this._workspaceNotice = init.workspaceNotice;
182
+ this._error = init.error;
183
+ this._stoppedWhileQueued = init.stoppedWhileQueued ?? false;
184
+ this._startedAt = init.startedAt ?? Date.now();
185
+ this._completedAt = init.completedAt;
186
+ this._consumedAt = init.consumedAt;
187
+ this._toolUses = init.toolUses ?? 0;
188
+ // Copy so a later addUsage() cannot mutate the caller's object.
189
+ this._lifetimeUsage = init.lifetimeUsage
190
+ ? { ...init.lifetimeUsage }
191
+ : { input: 0, output: 0, cacheWrite: 0 };
192
+ this._compactionCount = init.compactionCount ?? 0;
193
+ this._turnCount = init.turnCount ?? 1;
194
+ this._responseText = init.responseText ?? "";
195
+ for (const name of init.activeTools ?? []) {
196
+ this.addActiveTool(name);
197
+ }
198
+ }
199
+
200
+ /** Running or queued — still live. */
201
+ isActive(): boolean {
202
+ return isActiveStatus(this._status);
203
+ }
204
+
205
+ /** Terminated by error, abort, or external stop (excludes `steered`). */
206
+ isTerminalError(): boolean {
207
+ return isTerminalErrorStatus(this._status);
208
+ }
209
+
210
+ /** Actively running (excludes queued). */
211
+ isRunning(): boolean {
212
+ return isRunningStatus(this._status);
213
+ }
214
+
215
+ /** Whether a steer message can be delivered — the agent must be running. */
216
+ canBeSteered(): boolean {
217
+ return isRunningStatus(this._status);
218
+ }
219
+
220
+ /** Increment tool use count. Called by record-observer on tool_execution_end. */
221
+ incrementToolUses(): void {
222
+ this._toolUses++;
223
+ }
224
+
225
+ /** Accumulate a usage delta into lifetimeUsage. Called by record-observer on message_end. */
226
+ addUsage(delta: { input: number; output: number; cacheWrite: number }): void {
227
+ addUsage(this._lifetimeUsage, delta);
228
+ }
229
+
230
+ /** Increment compaction count. Called by record-observer on compaction_end. */
231
+ incrementCompactions(): void {
232
+ this._compactionCount++;
233
+ }
234
+
235
+ /** Record a turn boundary. Called by record-observer on turn_end. */
236
+ incrementTurnCount(): void {
237
+ this._turnCount++;
238
+ }
239
+
240
+ /** Record a tool starting. Called by record-observer on tool_execution_start. */
241
+ addActiveTool(toolName: string): void {
242
+ this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
243
+ }
244
+
245
+ /** Remove one active tool by name (first match). Called by record-observer on tool_execution_end. */
246
+ removeActiveTool(toolName: string): void {
247
+ for (const [key, name] of this._activeTools) {
248
+ if (name === toolName) {
249
+ this._activeTools.delete(key);
250
+ break;
251
+ }
252
+ }
253
+ }
254
+
255
+ /** Reset the current response text. Called by record-observer on message_start. */
256
+ resetResponseText(): void {
257
+ this._responseText = "";
258
+ }
259
+
260
+ /** Append a text delta to the current response text. Called by record-observer on message_update. */
261
+ appendResponseText(delta: string): void {
262
+ this._responseText += delta;
263
+ }
264
+
265
+ /** Transition to running state. Sets status and startedAt. */
266
+ markRunning(startedAt: number): void {
267
+ this._status = "running";
268
+ this._startedAt = startedAt;
269
+ this._runUpdates.length = 0;
270
+ }
271
+
272
+ /** Record an update the child sent during this run, owed to a carrier until delivered. */
273
+ recordUpdate(message: string): void {
274
+ this._runUpdates.push({ message, announced: false });
275
+ }
276
+
277
+ /**
278
+ * The announcement channel delivered this message, so no outcome carrier may
279
+ * repeat it. Marks the first copy still owed: two identical messages are two
280
+ * facts the child sent twice, and one announcement delivered one of them.
281
+ */
282
+ markUpdateAnnounced(message: string): void {
283
+ const owed = this._runUpdates.find((update) => !update.announced && update.message === message);
284
+ if (owed) owed.announced = true;
285
+ }
286
+
287
+ /**
288
+ * Transition to completed state.
289
+ * Always sets result and completedAt (??=). Only changes status if not stopped.
290
+ */
291
+ markCompleted(result: string, completedAt?: number): void {
292
+ this._result = result;
293
+ this._completedAt ??= completedAt ?? Date.now();
294
+ if (this._status !== "stopped") {
295
+ this._status = "completed";
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Transition to aborted state.
301
+ * Always sets result and completedAt (??=). Only changes status if not stopped.
302
+ */
303
+ markAborted(result: string, completedAt?: number): void {
304
+ this._result = result;
305
+ this._completedAt ??= completedAt ?? Date.now();
306
+ if (this._status !== "stopped") {
307
+ this._status = "aborted";
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Transition to steered state.
313
+ * Always sets result and completedAt (??=). Only changes status if not stopped.
314
+ */
315
+ markSteered(result: string, completedAt?: number): void {
316
+ this._result = result;
317
+ this._completedAt ??= completedAt ?? Date.now();
318
+ if (this._status !== "stopped") {
319
+ this._status = "steered";
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Transition to error state.
325
+ * Always sets error (formatted) and completedAt (??=). Only changes status if not stopped.
326
+ */
327
+ markError(error: unknown, completedAt?: number): void {
328
+ this._error = error instanceof Error ? error.message : String(error);
329
+ this._completedAt ??= completedAt ?? Date.now();
330
+ if (this._status !== "stopped") {
331
+ this._status = "error";
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Record the parent collected the outcome. Idempotent — keeps the first
337
+ * collection time (??=), so a re-read does not advance the retention clock.
338
+ */
339
+ markConsumed(at?: number): void {
340
+ this._consumedAt ??= at ?? Date.now();
341
+ }
342
+
343
+ /** Record the question the agent ended its turn with. */
344
+ setPendingQuestion(question: string | undefined): void {
345
+ this._pendingQuestion = question;
346
+ }
347
+
348
+ /** Record what a teardown reported when no result text could carry it. */
349
+ setWorkspaceNotice(notice: string): void {
350
+ this._workspaceNotice = notice;
351
+ }
352
+
353
+ /**
354
+ * A carrier has committed to delivering this outcome, so nothing else should
355
+ * announce it. Unlike every other transition here, this one is revocable.
356
+ */
357
+ claim(): void {
358
+ this._claimed = true;
359
+ }
360
+
361
+ /** The carrier abandoned its commitment; announcing is owed again. */
362
+ release(): void {
363
+ this._claimed = false;
364
+ }
365
+
366
+ /** Transition to stopped state. Always valid — no guard. */
367
+ markStopped(completedAt?: number): void {
368
+ this._status = "stopped";
369
+ this._completedAt = completedAt ?? Date.now();
370
+ }
371
+
372
+ /**
373
+ * Stop an agent that is still awaiting a concurrency slot. Records the
374
+ * never-started fact only when the agent is genuinely still queued, so a
375
+ * mis-targeted call cannot claim it.
376
+ */
377
+ stopQueued(completedAt?: number): void {
378
+ if (this._status === "queued") this._stoppedWhileQueued = true;
379
+ this.markStopped(completedAt);
380
+ }
381
+
382
+ /**
383
+ * Reset for resume: running status, new startedAt, clear
384
+ * completedAt/result/error/consumedAt.
385
+ *
386
+ * The carrier claim deliberately survives: it belongs to the caller that asked
387
+ * for the resume and will deliver its outcome, not to the run being reset.
388
+ * Clearing it here would drop the claim before the caller could observe it,
389
+ * since this runs synchronously before resume() returns.
390
+ */
391
+ resetForResume(startedAt: number): void {
392
+ this._status = "running";
393
+ this._startedAt = startedAt;
394
+ this._completedAt = undefined;
395
+ this._result = undefined;
396
+ this._error = undefined;
397
+ this._consumedAt = undefined;
398
+ // A resumed run answers the old question; whether it asks a new one is
399
+ // decided when it terminates.
400
+ this._pendingQuestion = undefined;
401
+ // The updates belong to the run that produced them, and this starts another.
402
+ this._runUpdates.length = 0;
403
+ }
404
+ }