@nklisch/pi-enhanced 0.2.7 → 0.3.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 (70) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/node_modules/@nklisch/pi-clearance/native/clearance-core.linux-x64-gnu.node +0 -0
  3. package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
  4. package/node_modules/@nklisch/pi-plugins/README.md +4 -2
  5. package/node_modules/@nklisch/pi-plugins/dist/hooks.js +81 -8
  6. package/node_modules/@nklisch/pi-plugins/dist/hooks.js.map +1 -1
  7. package/node_modules/@nklisch/pi-plugins/dist/mcp.js +32 -1
  8. package/node_modules/@nklisch/pi-plugins/dist/mcp.js.map +1 -1
  9. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.d.ts +10 -4
  10. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js +144 -10
  11. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js.map +1 -1
  12. package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js +40 -14
  13. package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js.map +1 -1
  14. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/CHANGELOG.md +19 -0
  15. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +75 -47
  16. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/public.d.ts +171 -140
  17. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/settings.d.ts +2 -2
  18. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/FORK-MAINTENANCE.md +8 -7
  19. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/VISION.md +12 -10
  20. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +111 -104
  21. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/comparison-with-upstream.md +3 -3
  22. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0004-reconsider-ui-direction.md +5 -0
  23. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +1 -1
  24. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/custom-agents.ts +31 -4
  25. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/default-agents.ts +1 -1
  26. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/invocation-config.ts +7 -4
  27. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/index.ts +18 -9
  28. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/layered-settings.ts +2 -2
  29. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/child-lifecycle.ts +10 -63
  30. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/concurrency-limiter.ts +122 -76
  31. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/create-subagent-session.ts +3 -5
  32. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/lifecycle-interceptor.ts +4 -3
  33. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/run-listeners.ts +2 -23
  34. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-manager.ts +177 -252
  35. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-session.ts +90 -280
  36. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-state.ts +153 -239
  37. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent.ts +569 -700
  38. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/workspace.ts +1 -1
  39. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/notification.ts +78 -122
  40. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/renderer.ts +15 -42
  41. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/subagent-events-observer.ts +44 -95
  42. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service-adapter.ts +157 -90
  43. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service.ts +60 -68
  44. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/conversation.ts +3 -3
  45. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query-source.ts +28 -0
  46. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query.ts +327 -0
  47. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/settings.ts +1 -1
  48. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/agent-tool.ts +171 -249
  49. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-tool.ts +51 -94
  50. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/helpers.ts +9 -6
  51. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/list-tool.ts +46 -0
  52. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/parent-tool-registry.ts +17 -0
  53. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/query-session-tool.ts +330 -0
  54. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/result-renderer.ts +27 -111
  55. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/resume-tool.ts +105 -0
  56. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/spawn-config.ts +26 -8
  57. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/steer-tool.ts +27 -111
  58. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/stop-tool.ts +53 -0
  59. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/types.ts +11 -5
  60. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/agent-widget.ts +33 -27
  61. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/display.ts +6 -3
  62. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigation.ts +121 -10
  63. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigator.ts +430 -112
  64. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/subagents-settings.ts +1 -1
  65. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/widget-renderer.ts +8 -9
  66. package/node_modules/@nklisch/pi-plugins/package.json +2 -2
  67. package/package.json +1 -1
  68. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/background-spawner.ts +0 -81
  69. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/foreground-runner.ts +0 -149
  70. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-report.ts +0 -77
@@ -1,9 +1,4 @@
1
- /**
2
- * service-adapter.ts — Adapter that wraps SubagentManager to satisfy SubagentsService.
3
- *
4
- * Handles model resolution at the API boundary, record serialization
5
- * (stripping non-serializable fields), and session gating.
6
- */
1
+ /** Adapter from the public service contract to the authoritative manager. */
7
2
 
8
3
  import type { Model } from "@earendil-works/pi-ai";
9
4
  import { resolveDispatchAgentType } from "#src/config/agent-type-resolution";
@@ -14,38 +9,45 @@ import type {
14
9
  } from "#src/lifecycle/lifecycle-interceptor";
15
10
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
16
11
  import type { WorkspaceProvider } from "#src/lifecycle/workspace";
17
- import type { SpawnOptions, SubagentRecord, SubagentsService } from "#src/service/service";
12
+ import type { AgentSpawnConfig, DeliveryOutcome, ManagerSteerOutcome, ResumeOutcome, StopOutcome } from "#src/lifecycle/subagent-manager";
13
+ import type { Subagent } from "#src/lifecycle/subagent";
14
+ import type {
15
+ LaunchDelivery,
16
+ LaunchOptions,
17
+ ListOptions,
18
+ ResultDelivery,
19
+ ResumeDelivery,
20
+ StopDelivery,
21
+ SteerDelivery,
22
+ SubagentRecord,
23
+ SubagentsService,
24
+ } from "#src/service/service";
18
25
  import { formatModelLabel } from "#src/session/model-label";
19
26
  import { resolveEffectiveThinkingLevel } from "#src/session/thinking-level";
20
27
  import type { ModelRegistry } from "#src/session/model-resolver";
21
28
  import { resolveDefaultModel } from "#src/session/session-config";
22
- import type { AgentInvocation, SessionContext, Subagent, ThinkingLevel } from "#src/types";
29
+ import type { AgentInvocation, SessionContext, SubagentMode, ThinkingLevel } from "#src/types";
30
+ import { describeActivity } from "#src/ui/display";
23
31
 
24
- /** Narrow interface for the SubagentManager — avoids coupling to the concrete class. */
25
32
  export interface SubagentManagerLike {
26
- spawn(snapshot: ParentSnapshot, type: string, prompt: string, options: unknown): string;
33
+ launch(snapshot: ParentSnapshot, type: string, prompt: string, options: AgentSpawnConfig): Promise<DeliveryOutcome>;
34
+ resume(id: string, prompt: string, mode: SubagentMode, timeoutSeconds: number | undefined, signal?: AbortSignal): Promise<ResumeOutcome>;
35
+ stop(id: string, settlementTimeoutSeconds?: number): Promise<StopOutcome>;
36
+ steer(id: string, message: string): Promise<ManagerSteerOutcome>;
27
37
  getRecord(id: string): Subagent | undefined;
28
38
  listAgents(): Subagent[];
29
- abort(id: string): boolean;
30
39
  waitForAll(): Promise<void>;
31
40
  hasRunning(): boolean;
32
41
  registerWorkspaceProvider(provider: WorkspaceProvider): () => void;
33
- registerLifecycleInterceptor(
34
- interceptor: SubagentLifecycleInterceptor,
35
- ): SubagentLifecycleRegistration;
42
+ registerLifecycleInterceptor(interceptor: SubagentLifecycleInterceptor): SubagentLifecycleRegistration;
36
43
  }
37
44
 
38
- /**
39
- * Narrow runtime interface consumed by the service adapter.
40
- * `SubagentRuntime` satisfies this structurally; tests use plain stubs.
41
- */
42
45
  export interface ServiceRuntimeLike {
43
46
  readonly currentCtx: SessionContext | undefined;
44
47
  buildSnapshot(inheritContext: boolean): ParentSnapshot;
45
48
  getSessionInfo(): { parentSessionFile: string; parentSessionId: string };
46
49
  }
47
50
 
48
- /** Adapter that wraps SubagentManager to satisfy SubagentsService. */
49
51
  export class SubagentsServiceAdapter implements SubagentsService {
50
52
  constructor(
51
53
  private readonly manager: SubagentManagerLike,
@@ -55,139 +57,204 @@ export class SubagentsServiceAdapter implements SubagentsService {
55
57
  private readonly settings?: { readonly fallbackSubagent?: string | false },
56
58
  ) {}
57
59
 
58
- spawn(type: string, prompt: string, options?: SpawnOptions): string {
59
- if (!this.runtime.currentCtx) {
60
- throw new Error("No active session — cannot spawn agents outside a session.");
61
- }
62
-
60
+ async launch(type: string, prompt: string, options?: LaunchOptions): Promise<LaunchDelivery> {
61
+ const ctx = this.requireContext();
63
62
  this.agentRegistry?.reload();
64
63
  const resolution = this.agentRegistry
65
64
  ? resolveDispatchAgentType(type, this.agentRegistry, this.settings?.fallbackSubagent)
66
65
  : { type, requestedType: type, fellBack: false };
67
66
  if ("error" in resolution) throw new Error(resolution.error);
68
- const resolvedType = resolution.type;
69
67
 
68
+ const resolvedType = resolution.type;
69
+ const agentConfig = this.agentRegistry?.resolveAgentConfig(resolvedType);
70
70
  const model = options?.model
71
71
  ? this.resolveModelOption(options.model)
72
72
  : this.resolveTypeDefaultModel(resolvedType);
73
+ const mode = normalizeMode(options?.mode ?? agentConfig?.mode);
73
74
  const description = options?.description ?? prompt.slice(0, 80);
74
- const isBackground = !(options?.foreground ?? false);
75
- const snapshot = this.runtime.buildSnapshot(options?.inheritContext ?? false);
76
- const requestedThinking = (options?.thinkingLevel
77
- ?? this.agentRegistry?.resolveAgentConfig(resolvedType).thinking) as ThinkingLevel | undefined;
78
- const effectiveThinkingLevel = resolveEffectiveThinkingLevel(
79
- model,
80
- requestedThinking,
81
- snapshot.thinkingLevel,
82
- );
75
+ const thinking = options?.thinkingLevel ?? agentConfig?.thinking;
76
+ const maxTurns = normalizeMaxTurns(options?.maxTurns ?? agentConfig?.maxTurns);
77
+ const timeoutSeconds = options?.timeoutSeconds ?? agentConfig?.timeoutSeconds;
78
+ const inheritContext = options?.inheritContext ?? agentConfig?.inheritContext ?? false;
79
+ const snapshot = this.runtime.buildSnapshot(inheritContext);
80
+ const effectiveThinkingLevel = resolveEffectiveThinkingLevel(model, thinking, snapshot.thinkingLevel);
83
81
  const invocation: AgentInvocation = {
84
82
  modelName: formatModelLabel(model),
85
- maxTurns: options?.maxTurns,
86
- inheritContext: options?.inheritContext,
87
- runInBackground: isBackground,
83
+ maxTurns,
84
+ inheritContext,
85
+ mode,
86
+ timeoutSeconds,
88
87
  };
89
-
90
88
  const parent = this.runtime.getSessionInfo();
91
- return this.manager.spawn(snapshot, resolvedType, prompt, {
89
+
90
+ const outcome = await this.manager.launch(snapshot, resolvedType, prompt, {
92
91
  description,
93
92
  model,
94
- maxTurns: options?.maxTurns,
93
+ maxTurns,
95
94
  thinkingLevel: effectiveThinkingLevel,
96
- inheritContext: options?.inheritContext,
97
- bypassQueue: options?.bypassQueue,
98
- isBackground,
95
+ inheritContext,
96
+ mode,
97
+ timeoutSeconds: normalizeTimeout(timeoutSeconds),
99
98
  origin: "service",
100
99
  invocation,
100
+ signal: options?.signal,
101
101
  lifecycleParentSession: parent.parentSessionId
102
- ? {
103
- parentSessionFile: parent.parentSessionFile || undefined,
104
- parentSessionId: parent.parentSessionId,
105
- }
102
+ ? { parentSessionFile: parent.parentSessionFile || undefined, parentSessionId: parent.parentSessionId }
106
103
  : undefined,
107
104
  });
105
+ if (outcome.kind === "joined") outcome.record.markConsumed();
106
+ return this.toLaunchDelivery(outcome);
108
107
  }
109
108
 
110
- getRecord(id: string): SubagentRecord | undefined {
111
- const record = this.manager.getRecord(id);
112
- return record ? toSubagentRecord(record) : undefined;
109
+ async resume(
110
+ agentId: string,
111
+ prompt: string,
112
+ options?: Pick<LaunchOptions, "mode" | "timeoutSeconds" | "signal">,
113
+ ): Promise<ResumeDelivery> {
114
+ const mode = options?.mode ?? "detached";
115
+ const outcome = await this.manager.resume(
116
+ agentId,
117
+ prompt,
118
+ mode,
119
+ normalizeTimeout(options?.timeoutSeconds),
120
+ options?.signal,
121
+ );
122
+ if (outcome.kind === "joined") {
123
+ outcome.record.markConsumed();
124
+ return { kind: "joined", record: toSubagentRecord(outcome.record) };
125
+ }
126
+ if (outcome.kind === "detached") return outcome;
127
+ return outcome;
113
128
  }
114
129
 
115
- listAgents(): SubagentRecord[] {
116
- return this.manager.listAgents().map(toSubagentRecord);
130
+ async stop(agentId: string, settlementTimeoutSeconds = 5): Promise<StopDelivery> {
131
+ const timeout = normalizeSettlementTimeout(settlementTimeoutSeconds);
132
+ const outcome = await this.manager.stop(agentId, timeout);
133
+ if (outcome.kind === "not_found") return outcome;
134
+ if (outcome.kind === "already_terminal") return { ...outcome, record: toSubagentRecord(outcome.record) };
135
+ return { ...outcome, record: toSubagentRecord(outcome.record) };
117
136
  }
118
137
 
119
- abort(id: string): boolean {
120
- return this.manager.abort(id);
138
+ async steer(agentId: string, message: string): Promise<SteerDelivery> {
139
+ const outcome = await this.manager.steer(agentId, message);
140
+ if (outcome.kind === "not_found") return outcome;
141
+ if (outcome.kind === "rejected") return { ...outcome, agentId };
142
+ return { ...outcome, agentId };
121
143
  }
122
144
 
123
- async steer(id: string, message: string): Promise<boolean> {
124
- const record = this.manager.getRecord(id);
125
- if (!record) {
126
- return false;
127
- }
128
- const outcome = await record.steer(message);
129
- return outcome.kind !== "rejected";
145
+ list(options?: ListOptions): SubagentRecord[] {
146
+ const state = options?.state ?? "all";
147
+ const limit = normalizeListLimit(options?.limit);
148
+ return this.manager.listAgents()
149
+ .filter((record) => state === "all" || (state === "active" ? record.isActive() : !record.isActive()))
150
+ .slice(0, limit)
151
+ .map(toSubagentRecord);
130
152
  }
131
153
 
132
- async waitForAll(): Promise<void> {
133
- return this.manager.waitForAll();
154
+ getResult(agentId: string): ResultDelivery {
155
+ const record = this.manager.getRecord(agentId);
156
+ if (!record) return { kind: "not_found", agentId };
157
+ if (!record.isActive()) record.markConsumed();
158
+ return { kind: "result", record: toSubagentRecord(record) };
134
159
  }
135
160
 
136
- hasRunning(): boolean {
137
- return this.manager.hasRunning();
161
+ getRecord(agentId: string): SubagentRecord | undefined {
162
+ const record = this.manager.getRecord(agentId);
163
+ return record ? toSubagentRecord(record) : undefined;
138
164
  }
139
165
 
140
- registerWorkspaceProvider(provider: WorkspaceProvider): () => void {
141
- return this.manager.registerWorkspaceProvider(provider);
142
- }
166
+ waitForAll(): Promise<void> { return this.manager.waitForAll(); }
167
+ hasRunning(): boolean { return this.manager.hasRunning(); }
168
+ registerWorkspaceProvider(provider: WorkspaceProvider): () => void { return this.manager.registerWorkspaceProvider(provider); }
169
+ registerLifecycleInterceptor(interceptor: SubagentLifecycleInterceptor): SubagentLifecycleRegistration { return this.manager.registerLifecycleInterceptor(interceptor); }
143
170
 
144
- registerLifecycleInterceptor(
145
- interceptor: SubagentLifecycleInterceptor,
146
- ): SubagentLifecycleRegistration {
147
- return this.manager.registerLifecycleInterceptor(interceptor);
171
+ private requireContext(): SessionContext {
172
+ if (!this.runtime.currentCtx) throw new Error("No active session — cannot launch agents outside a session.");
173
+ return this.runtime.currentCtx;
148
174
  }
149
175
 
150
- /** Resolve a model-string override against the current session's registry. */
151
- private resolveModelOption(modelInput: string): Model<any> {
176
+ private resolveModelOption(input: string): Model<any> {
152
177
  const registry = this.runtime.currentCtx?.modelRegistry;
153
- if (!registry) {
154
- throw new Error("No model registry available.");
155
- }
156
- const resolved = this.resolveModel(modelInput, registry);
157
- if (typeof resolved === "string") {
158
- throw new Error(resolved);
159
- }
178
+ if (!registry) throw new Error("No model registry available.");
179
+ const resolved = this.resolveModel(input, registry);
180
+ if (typeof resolved === "string") throw new Error(resolved);
160
181
  return resolved;
161
182
  }
162
183
 
163
- /** Resolve the agent type's configured model with the same fallback used by session assembly. */
164
184
  private resolveTypeDefaultModel(type: string): Model<any> {
165
185
  const ctx = this.runtime.currentCtx!;
166
186
  const configured = this.agentRegistry?.resolveAgentConfig(type).model;
167
187
  if (!ctx.modelRegistry) return ctx.model as Model<any>;
168
188
  return resolveDefaultModel(ctx.model, ctx.modelRegistry, configured) as Model<any>;
169
189
  }
190
+
191
+ private toLaunchDelivery(outcome: DeliveryOutcome): LaunchDelivery {
192
+ return outcome.kind === "joined"
193
+ ? { kind: "joined", record: toSubagentRecord(outcome.record) }
194
+ : outcome;
195
+ }
170
196
  }
171
197
 
172
- /**
173
- * Convert an internal Subagent to a serializable SubagentRecord.
174
- * Uses an explicit allowlist — new fields must be opted in.
175
- */
176
198
  export function toSubagentRecord(record: Subagent): SubagentRecord {
199
+ const activeTools = [...record.activeTools.values()];
177
200
  const out: SubagentRecord = {
178
201
  id: record.id,
179
202
  type: record.type,
180
203
  description: record.description,
204
+ runId: record.runId,
205
+ mode: record.mode,
181
206
  status: record.status,
207
+ stopRequested: record.stopRequested,
208
+ terminalReason: record.stateTerminalReason,
182
209
  toolUses: record.toolUses,
183
210
  startedAt: record.startedAt,
184
- lifetimeUsage: record.lifetimeUsage,
211
+ activeRuntimeMs: record.activeRuntimeMs,
212
+ modelLabel: record.modelLabel,
213
+ thinkingLevel: record.effectiveThinkingLevel,
214
+ activeTools,
215
+ currentActivity: describeActivity(record.activeTools, record.responseText),
216
+ lifetimeUsage: { ...record.lifetimeUsage },
185
217
  compactionCount: record.compactionCount,
186
218
  };
187
-
188
- if (record.result !== undefined) out.result = record.result;
219
+ if (record.result !== undefined) out.result = boundResult(record.result, record.outputFile);
189
220
  if (record.error !== undefined) out.error = record.error;
190
221
  if (record.completedAt !== undefined) out.completedAt = record.completedAt;
191
-
222
+ if (record.outputFile !== undefined) out.outputFile = record.outputFile;
192
223
  return out;
193
224
  }
225
+
226
+ const MAX_RESULT_OUTPUT = 12_000;
227
+ function boundResult(result: string, outputFile: string | undefined): string {
228
+ return result.length > MAX_RESULT_OUTPUT
229
+ ? result.slice(0, MAX_RESULT_OUTPUT) + `\n\nOutput truncated. Full transcript: ${outputFile ?? "unavailable"}`
230
+ : result;
231
+ }
232
+
233
+ function normalizeMode(value: string | undefined): SubagentMode {
234
+ if (value == null) return "detached";
235
+ if (value !== "joined" && value !== "detached") throw new RangeError("mode must be joined or detached");
236
+ return value;
237
+ }
238
+
239
+ function normalizeMaxTurns(value: number | undefined): number | undefined {
240
+ if (value == null) return undefined;
241
+ if (!Number.isInteger(value) || value < 0) throw new RangeError("maxTurns must be a non-negative integer");
242
+ return value;
243
+ }
244
+
245
+ function normalizeTimeout(value: number | undefined): number | undefined {
246
+ if (value == null) return undefined;
247
+ if (!Number.isInteger(value) || value <= 0) throw new RangeError("timeoutSeconds must be a positive integer");
248
+ return value;
249
+ }
250
+
251
+ function normalizeSettlementTimeout(value: number): number {
252
+ if (!Number.isInteger(value) || value <= 0 || value > 30) throw new RangeError("settlementTimeoutSeconds must be an integer from 1 to 30");
253
+ return value;
254
+ }
255
+
256
+ function normalizeListLimit(value: number | undefined): number {
257
+ const limit = value ?? 20;
258
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new RangeError("limit must be an integer from 1 to 100");
259
+ return limit;
260
+ }
@@ -1,19 +1,11 @@
1
- /**
2
- * service.ts — Public API surface for cross-extension access to subagents.
3
- *
4
- * Consumers declare this package as an optional peer dependency and use
5
- * dynamic import to access the accessor functions:
6
- *
7
- * const { getSubagentsService } = await import("@nklisch/pi-subagents");
8
- * const svc = getSubagentsService();
9
- * svc?.spawn("Explore", "Check for stale TODOs");
10
- */
1
+ /** Public cross-extension contract for subagent execution control. */
11
2
 
12
3
  import type {
13
4
  SubagentLifecycleInterceptor,
14
5
  SubagentLifecycleRegistration,
6
+ SubagentExecutionMode,
15
7
  } from "#src/lifecycle/lifecycle-interceptor";
16
- import type { SubagentStatus } from "#src/lifecycle/subagent";
8
+ import type { SubagentStatus, SubagentStopReason, SubagentTerminalReason } from "#src/lifecycle/subagent-state";
17
9
  import type { LifetimeUsage } from "#src/lifecycle/usage";
18
10
  import type {
19
11
  Workspace,
@@ -22,7 +14,7 @@ import type {
22
14
  WorkspacePrepareContext,
23
15
  WorkspaceProvider,
24
16
  } from "#src/lifecycle/workspace";
25
-
17
+ import type { ThinkingLevel } from "#src/types";
26
18
 
27
19
  export {
28
20
  MAX_LIFECYCLE_CONTINUATION_ROUNDS,
@@ -40,13 +32,7 @@ export {
40
32
  type SubagentLifecycleStartContext,
41
33
  type SubagentLifecycleStartDecision,
42
34
  } from "#src/lifecycle/lifecycle-interceptor";
43
- // SubagentStatus is defined in the lifecycle layer (single home) and re-exported
44
- // here for the public API surface — mirrors the LifetimeUsage / workspace pattern.
45
- export type { SubagentStatus } from "#src/lifecycle/subagent";
46
- // Generative extension seam (ADR 0002, Phase 16 Step 2). The provider type
47
- // and all four collaborator types it references are re-exported by name so
48
- // consumers can import them directly rather than recovering them via
49
- // indexed-access inference (e.g. `Parameters<WorkspaceProvider["prepare"]>[0]`).
35
+ export type { SubagentStatus, SubagentStopReason, SubagentTerminalReason } from "#src/lifecycle/subagent-state";
50
36
  export type {
51
37
  LifetimeUsage,
52
38
  Workspace,
@@ -55,73 +41,86 @@ export type {
55
41
  WorkspacePrepareContext,
56
42
  WorkspaceProvider,
57
43
  };
44
+ export type { SubagentExecutionMode as SubagentMode };
58
45
 
59
- /** Serializable snapshot of an agent's state — no live session objects. */
60
46
  export interface SubagentRecord {
61
47
  id: string;
62
48
  type: string;
63
49
  description: string;
50
+ runId: number;
51
+ mode: SubagentExecutionMode;
64
52
  status: SubagentStatus;
53
+ stopRequested: boolean;
54
+ terminalReason?: SubagentTerminalReason;
65
55
  result?: string;
66
56
  error?: string;
67
57
  toolUses: number;
68
58
  startedAt: number;
69
59
  completedAt?: number;
60
+ activeRuntimeMs: number;
61
+ modelLabel: string;
62
+ thinkingLevel: ThinkingLevel;
63
+ activeTools: string[];
64
+ currentActivity: string;
70
65
  lifetimeUsage: LifetimeUsage;
71
66
  compactionCount: number;
67
+ outputFile?: string;
72
68
  }
73
69
 
74
- /** Options for spawning an agent via the service. */
75
- export interface SpawnOptions {
70
+ export interface LaunchOptions {
76
71
  description?: string;
77
72
  model?: string;
78
73
  maxTurns?: number;
79
- thinkingLevel?: string;
74
+ thinkingLevel?: ThinkingLevel;
80
75
  inheritContext?: boolean;
81
- foreground?: boolean;
82
- bypassQueue?: boolean;
76
+ mode?: SubagentExecutionMode;
77
+ timeoutSeconds?: number;
78
+ signal?: AbortSignal;
83
79
  }
84
80
 
85
- /** The public service contract for cross-extension subagent access. */
86
- export interface SubagentsService {
87
- /** Spawn an agent. Returns the agent ID immediately. */
88
- spawn(type: string, prompt: string, options?: SpawnOptions): string;
89
-
90
- /** Get a snapshot of an agent's current state. */
91
- getRecord(id: string): SubagentRecord | undefined;
92
-
93
- /** List all tracked agents, most recent first. */
94
- listAgents(): SubagentRecord[];
95
-
96
- /** Abort a running or queued agent. Returns false if not found. */
97
- abort(id: string): boolean;
98
-
99
- /** Send a steering message to a running agent. */
100
- steer(id: string, message: string): Promise<boolean>;
81
+ export type LaunchDelivery =
82
+ | { kind: "detached"; agentId: string; runId: number }
83
+ | { kind: "joined"; record: SubagentRecord };
84
+
85
+ export type ResumeDelivery = LaunchDelivery |
86
+ { kind: "not_found"; agentId: string } |
87
+ { kind: "wrong_state"; agentId: string; status: SubagentStatus };
88
+
89
+ export type StopDelivery =
90
+ | { kind: "stopped"; agentId: string; runId: number; reason: SubagentStopReason; record: SubagentRecord }
91
+ | { kind: "stop_pending"; agentId: string; runId: number; reason: SubagentStopReason; record: SubagentRecord }
92
+ | { kind: "already_terminal"; agentId: string; runId: number; record: SubagentRecord }
93
+ | { kind: "not_found"; agentId: string };
94
+
95
+ export type SteerDelivery =
96
+ | { kind: "delivered" | "buffered"; agentId: string; runId: number }
97
+ | { kind: "rejected"; agentId: string; runId: number; status: SubagentStatus }
98
+ | { kind: "not_found"; agentId: string };
99
+
100
+ export type ResultDelivery =
101
+ | { kind: "result"; record: SubagentRecord }
102
+ | { kind: "not_found"; agentId: string };
103
+
104
+ export type SubagentListState = "active" | "terminal" | "all";
105
+ export interface ListOptions {
106
+ state?: SubagentListState;
107
+ limit?: number;
108
+ }
101
109
 
102
- /** Wait for all running and queued agents to complete. */
110
+ export interface SubagentsService {
111
+ launch(type: string, prompt: string, options?: LaunchOptions): Promise<LaunchDelivery>;
112
+ resume(agentId: string, prompt: string, options?: Pick<LaunchOptions, "mode" | "timeoutSeconds" | "signal">): Promise<ResumeDelivery>;
113
+ stop(agentId: string, settlementTimeoutSeconds?: number): Promise<StopDelivery>;
114
+ steer(agentId: string, message: string): Promise<SteerDelivery>;
115
+ list(options?: ListOptions): SubagentRecord[];
116
+ getResult(agentId: string): ResultDelivery;
117
+ getRecord(agentId: string): SubagentRecord | undefined;
103
118
  waitForAll(): Promise<void>;
104
-
105
- /** Whether any agents are running or queued. */
106
119
  hasRunning(): boolean;
107
-
108
- /**
109
- * Register the single workspace provider that supplies a child's working
110
- * directory plus bracketed setup/teardown. Throws if one is already
111
- * registered. Returns a disposer that unregisters the provider.
112
- */
113
120
  registerWorkspaceProvider(provider: WorkspaceProvider): () => void;
114
-
115
- /**
116
- * Register an ordered async lifecycle provider. It receives only immutable
117
- * execution facts and prompt/result decisions, never a manager or session.
118
- */
119
- registerLifecycleInterceptor(
120
- interceptor: SubagentLifecycleInterceptor,
121
- ): SubagentLifecycleRegistration;
121
+ registerLifecycleInterceptor(interceptor: SubagentLifecycleInterceptor): SubagentLifecycleRegistration;
122
122
  }
123
123
 
124
- /** Event channel constants for pi.events subscriptions. */
125
124
  export const SUBAGENT_EVENTS = {
126
125
  STARTED: "subagents:started",
127
126
  COMPLETED: "subagents:completed",
@@ -132,24 +131,17 @@ export const SUBAGENT_EVENTS = {
132
131
  STEERED: "subagents:steered",
133
132
  } as const;
134
133
 
135
- // ---- Accessor functions ----
136
-
137
134
  const SERVICE_KEY = Symbol.for("@nklisch/pi-subagents:service");
138
135
 
139
- /** Publish the SubagentsService on globalThis for cross-extension access. */
140
136
  export function publishSubagentsService(service: SubagentsService): void {
141
137
  (globalThis as Record<symbol, unknown>)[SERVICE_KEY] = service;
142
138
  }
143
139
 
144
- /** Retrieve the published SubagentsService, or undefined if not yet published. */
145
140
  export function getSubagentsService(): SubagentsService | undefined {
146
- return (globalThis as Record<symbol, unknown>)[SERVICE_KEY] as
147
- | SubagentsService
148
- | undefined;
141
+ return (globalThis as Record<symbol, unknown>)[SERVICE_KEY] as SubagentsService | undefined;
149
142
  }
150
143
 
151
- /** Remove the SubagentsService from globalThis (call on shutdown/reload). */
152
144
  export function unpublishSubagentsService(): void {
153
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Symbol-keyed global property; Map.delete() is not applicable
145
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Symbol-keyed global property
154
146
  delete (globalThis as Record<symbol, unknown>)[SERVICE_KEY];
155
147
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * conversation.ts — Render a subagent session's messages as formatted text.
3
3
  *
4
- * Extracted from agent-runner.ts (issue #265) into the session domain, where the
5
- * other message-extraction helpers (content-items, context) live. Consumed by
6
- * the get_subagent_result tool's verbose output.
4
+ * Extracted into the session domain, alongside the other message-extraction
5
+ * helpers. Kept for read-only transcript/navigation surfaces; parent result
6
+ * delivery itself remains bounded.
7
7
  */
8
8
 
9
9
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
@@ -0,0 +1,28 @@
1
+ /** Shared file adapter for query and transcript consumers. */
2
+ import {
3
+ buildSessionContext,
4
+ parseSessionEntries,
5
+ type SessionEntry,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import type { SessionMessage } from "#src/types";
8
+
9
+ /** Parse exactly the same public Pi JSONL/context pipeline used by navigation. */
10
+ export function parseSessionFileMessages(
11
+ content: string,
12
+ ): readonly SessionMessage[] {
13
+ const entries = parseSessionEntries(content);
14
+ const sessionEntries = entries.filter((entry): entry is SessionEntry => entry.type !== "session");
15
+ return buildSessionContext(sessionEntries).messages;
16
+ }
17
+
18
+ export interface SessionFileReader {
19
+ readFile(path: string): string;
20
+ }
21
+
22
+ export function readSessionFileMessages(
23
+ outputFile: string,
24
+ reader: SessionFileReader | ((path: string) => string),
25
+ ): readonly SessionMessage[] {
26
+ const readFile = typeof reader === "function" ? reader : (path: string) => reader.readFile(path);
27
+ return parseSessionFileMessages(readFile(outputFile));
28
+ }