@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,4 +1,5 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions -- Pi SDK render types are intentionally narrow at this boundary */
2
+
2
3
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
3
4
  import { defineTool } from "@earendil-works/pi-coding-agent";
4
5
  import { Text } from "@earendil-works/pi-tui";
@@ -6,266 +7,187 @@ import { Type } from "@sinclair/typebox";
6
7
  import { AgentTypeRegistry } from "#src/config/agent-types";
7
8
  import { THINKING_LEVELS_DESCRIPTION } from "#src/config/thinking-levels";
8
9
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
9
- import type { AgentSpawnConfig } from "#src/lifecycle/subagent-manager";
10
- import { SubagentBusyError } from "#src/lifecycle/subagent";
11
- import { spawnBackground } from "#src/tools/background-spawner";
12
- import { runForeground } from "#src/tools/foreground-runner";
13
- import { buildAgentGuidelines, buildDetails, buildTypeListText, textResult } from "#src/tools/helpers";
10
+ import type { AgentSpawnConfig, DeliveryOutcome } from "#src/lifecycle/subagent-manager";
11
+ import type { Subagent } from "#src/lifecycle/subagent";
12
+ import { buildAgentGuidelines, buildDetails, buildTypeListText, formatLifetimeTokens, textResult } from "#src/tools/helpers";
14
13
  import { renderAgentResult } from "#src/tools/result-renderer";
15
14
  import { type ModelInfo, resolveSpawnConfig } from "#src/tools/spawn-config";
16
- import type { ParentSessionInfo, Subagent } from "#src/types";
17
- import { type AgentDetails, formatDuration, formatModelThinking, getDisplayName } from "#src/ui/display";
18
-
19
- // ---- Deps interfaces ----
15
+ import type { ParentSessionInfo } from "#src/types";
16
+ import { describeActivity, type AgentDetails, formatModelThinking, formatMs, getDisplayName } from "#src/ui/display";
20
17
 
21
- /** Narrow manager interface — only the methods the Agent tool calls. */
22
18
  export interface AgentToolManager {
23
- spawn: (snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig) => string;
24
- spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<Subagent>;
25
- resume: (id: string, prompt: string, signal: AbortSignal) => Promise<Subagent | undefined>;
26
- getRecord: (id: string) => Subagent | undefined;
19
+ launch(snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig): Promise<DeliveryOutcome>;
20
+ getRecord(id: string): Subagent | undefined;
27
21
  }
28
22
 
29
- /** Narrow runtime interface — the Agent tool's slice of SubagentRuntime. */
30
23
  export interface AgentToolRuntime {
31
- buildSnapshot(inheritContext: boolean): ParentSnapshot;
32
- getModelInfo(): ModelInfo;
33
- getSessionInfo(): { parentSessionFile: string; parentSessionId: string };
24
+ buildSnapshot(inheritContext: boolean): ParentSnapshot;
25
+ getModelInfo(): ModelInfo;
26
+ getSessionInfo(): { parentSessionFile: string; parentSessionId: string };
34
27
  }
35
28
 
36
- /** Narrow settings accessor — only the fields the Agent tool reads. */
37
29
  export type AgentToolSettings = {
38
- readonly defaultMaxTurns: number | undefined;
39
- readonly maxConcurrent: number;
40
- readonly fallbackSubagent?: string | false;
30
+ readonly defaultMaxTurns: number | undefined;
31
+ readonly fallbackSubagent?: string | false;
41
32
  };
42
33
 
43
- // ---- Class ----
44
-
45
34
  export class AgentTool {
46
- private readonly typeListText: string;
47
- private readonly availableTypesText: string;
48
- private readonly agentGuidelines: string[];
49
-
50
- constructor(
51
- private readonly manager: AgentToolManager,
52
- private readonly runtime: AgentToolRuntime,
53
- private readonly settings: AgentToolSettings,
54
- private readonly registry: AgentTypeRegistry,
55
- private readonly agentDir: string,
56
- ) {
57
- this.typeListText = buildTypeListText(registry, agentDir);
58
- this.availableTypesText = registry.getAvailableTypes().join(", ");
59
- this.agentGuidelines = buildAgentGuidelines(registry);
60
- }
61
-
62
- async execute(
63
- toolCallId: string,
64
- params: Record<string, unknown>,
65
- signal: AbortSignal | undefined,
66
- onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
67
- _ctx: any,
68
- ) {
69
- // Reload custom agents so new .pi/agents/*.md files are picked up without restart
70
- this.registry.reload();
71
-
72
- // ---- Config resolution (pure) ----
73
- const config = resolveSpawnConfig(
74
- params,
75
- this.registry,
76
- this.runtime.getModelInfo(),
77
- this.settings,
78
- );
79
- if ("error" in config) return textResult(config.error);
80
-
81
- // ---- Boundary extraction (after config so inheritContext is resolved) ----
82
- const snapshot = this.runtime.buildSnapshot(config.execution.inheritContext);
83
- const { parentSessionFile, parentSessionId } = this.runtime.getSessionInfo();
84
- const parentSession: ParentSessionInfo = { parentSessionFile, parentSessionId, toolCallId };
85
-
86
- // ---- Resume existing agent ----
87
- if (params.resume) {
88
- const existing = this.manager.getRecord(params.resume as string);
89
- if (!existing) {
90
- return textResult(
91
- `Agent not found: "${params.resume}". It may have been cleaned up.`,
92
- );
93
- }
94
- if (!existing.isSessionReady()) {
95
- return textResult(
96
- existing.sessionReleased
97
- ? `Agent "${params.resume}" retained its result and transcript, but its live session was released after the retention window. Start a new subagent for fresh context.`
98
- : `Agent "${params.resume}" has no active session to resume.`,
99
- );
100
- }
101
- let record: Subagent | undefined;
102
- try {
103
- record = await this.manager.resume(
104
- params.resume as string,
105
- params.prompt as string,
106
- signal ?? new AbortController().signal,
107
- );
108
- } catch (error) {
109
- if (error instanceof SubagentBusyError) return textResult(error.message);
110
- throw error;
111
- }
112
- if (!record) {
113
- return textResult(`Failed to resume agent "${params.resume}".`);
114
- }
115
- record.markConsumed();
116
- const partial = record.result?.trim();
117
- const resultText = record.status === "error"
118
- ? partial
119
- ? `Error: ${record.error}\n\nPartial output before the failure:\n${partial}`
120
- : `Error: ${record.error}`
121
- : partial ?? "No output.";
122
- return textResult(
123
- `Model: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatDuration(record.startedAt, record.completedAt)}\n\n${resultText}`,
124
- buildDetails(
125
- { ...config.presentation.detailBase, modelName: record.modelLabel, thinkingLevel: record.effectiveThinkingLevel },
126
- record,
127
- ),
128
- );
129
- }
130
-
131
- // ---- Background execution ----
132
- if (config.execution.runInBackground) {
133
- return spawnBackground(
134
- this.manager,
135
- { config, snapshot, parentSession, settings: this.settings },
136
- );
137
- }
138
-
139
- // ---- Foreground execution — stream progress via onUpdate ----
140
- return runForeground(
141
- this.manager,
142
- { config, snapshot, parentSession },
143
- signal,
144
- onUpdate,
145
- );
146
- }
147
-
148
- toToolDefinition() {
149
- const typeListText = this.typeListText;
150
- const availableTypesText = this.availableTypesText;
151
- const agentDir = this.agentDir;
152
- const registry = this.registry;
153
-
154
- const guidelines = [
155
- "- For parallel work, use run_in_background: true on each agent. Foreground calls run sequentially — only one executes at a time.",
156
- ...this.agentGuidelines,
157
- "- Provide clear, detailed prompts so the agent can work autonomously.",
158
- "- Subagent results are returned as text summarize them for the user.",
159
- "- Background completion automatically wakes you with a result preview. Continue other work instead of polling.",
160
- "- Use get_subagent_result only for full output, verbose conversation, an explicit status check or synchronization point, or recovery after a missed notification.",
161
- "- Use steer_subagent to redirect an agent that is still running.",
162
- "- Use resume only after an agent finishes when it should continue with the same retained conversation history.",
163
- "- Launch a new subagent without resume when prior conversation history is unnecessary.",
164
- '- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").',
165
- "- Use thinking to control extended thinking level.",
166
- "- Use inherit_context if the agent needs the parent conversation history.",
167
- ].join("\n");
168
-
169
- return defineTool({
170
- name: "subagent" as const,
171
- label: "Subagent",
172
- promptSnippet: "subagent: Launch a specialized agent for complex, multi-step tasks.",
173
- description: `Launch a new agent to handle complex, multi-step tasks autonomously.
174
-
175
- The subagent tool launches specialized agents that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
176
-
177
- Available agent types:
178
- ${typeListText}
179
-
180
- Guidelines:
181
- ${guidelines}
182
- `,
183
- parameters: Type.Object({
184
- prompt: Type.String({
185
- description: "The task for the agent to perform.",
186
- }),
187
- description: Type.String({
188
- description: "A short (3-5 word) description of the task (shown in UI).",
189
- }),
190
- subagent_type: Type.String({
191
- description: `The type of specialized agent to use. Available types: ${availableTypesText}. Custom agents from .pi/agents/<name>.md (project) or ${agentDir}/agents/<name>.md (global) are also available.`,
192
- }),
193
- model: Type.Optional(
194
- Type.String({
195
- description:
196
- 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
197
- }),
198
- ),
199
- thinking: Type.Optional(
200
- Type.String({
201
- description:
202
- `Thinking level: ${THINKING_LEVELS_DESCRIPTION}. Overrides agent default. Availability depends on the selected model and host Pi version.`,
203
- }),
204
- ),
205
- max_turns: Type.Optional(
206
- Type.Number({
207
- description:
208
- "Maximum number of agentic turns before stopping. Omit for unlimited (default).",
209
- minimum: 1,
210
- }),
211
- ),
212
- run_in_background: Type.Optional(
213
- Type.Boolean({
214
- description:
215
- "Set to true to run in background. Returns an agent ID immediately; completion automatically wakes you with a result preview.",
216
- }),
217
- ),
218
- resume: Type.Optional(
219
- Type.String({
220
- description:
221
- "Agent ID of a retained, finished session to continue with the same conversation history. Omit to start a new context.",
222
- }),
223
- ),
224
- inherit_context: Type.Optional(
225
- Type.Boolean({
226
- description:
227
- "If true, fork parent conversation into the agent. Default: false (fresh context).",
228
- }),
229
- ),
230
- }),
231
-
232
- // ---- Custom rendering: inline subagent results ----
233
-
234
- renderCall(args: Record<string, unknown>, theme: any) {
235
- const displayName = args.subagent_type
236
- ? getDisplayName(args.subagent_type as string, registry)
237
- : "Subagent";
238
- const desc = (args.description as string | undefined) ?? "";
239
- return new Text(
240
- "▸ " +
241
- theme.fg("toolTitle", theme.bold(displayName)) +
242
- (desc ? " " + theme.fg("muted", desc) : ""),
243
- 0,
244
- 0,
245
- );
246
- },
247
-
248
- renderResult(result: any, { expanded, isPartial }: any, theme: any) {
249
- const details = result.details as AgentDetails | undefined;
250
- if (!details) {
251
- const text = result.content[0]?.type === "text" ? result.content[0].text : "";
252
- return new Text(text, 0, 0);
253
- }
254
- const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
255
- return new Text(
256
- renderAgentResult(details, resultText, expanded, isPartial, theme),
257
- 0,
258
- 0,
259
- );
260
- },
35
+ private readonly typeListText: string;
36
+ private readonly availableTypesText: string;
37
+ private readonly agentGuidelines: string[];
38
+
39
+ constructor(
40
+ private readonly manager: AgentToolManager,
41
+ private readonly runtime: AgentToolRuntime,
42
+ private readonly settings: AgentToolSettings,
43
+ private readonly registry: AgentTypeRegistry,
44
+ private readonly agentDir: string,
45
+ ) {
46
+ this.typeListText = buildTypeListText(registry, agentDir);
47
+ this.availableTypesText = registry.getAvailableTypes().join(", ");
48
+ this.agentGuidelines = buildAgentGuidelines(registry);
49
+ }
50
+
51
+ async execute(
52
+ toolCallId: string,
53
+ params: Record<string, unknown>,
54
+ signal: AbortSignal | undefined,
55
+ onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
56
+ _ctx: any,
57
+ ) {
58
+ this.registry.reload();
59
+ const config = resolveSpawnConfig(params, this.registry, this.runtime.getModelInfo(), this.settings);
60
+ if ("error" in config) return textResult(config.error);
61
+
62
+ const snapshot = this.runtime.buildSnapshot(config.execution.inheritContext);
63
+ const sessionInfo = this.runtime.getSessionInfo();
64
+ const parentSession: ParentSessionInfo = { ...sessionInfo, toolCallId };
65
+ let currentId: string | undefined;
66
+ let spinnerFrame = 0;
67
+ const update = (): void => {
68
+ const record = currentId ? this.manager.getRecord(currentId) : undefined;
69
+ if (!record || !onUpdate || config.execution.mode !== "joined") return;
70
+ const details = buildDetails(config.presentation.detailBase, record, {
71
+ durationMs: record.activeRuntimeMs,
72
+ status: record.status,
73
+ terminalReason: record.stateTerminalReason,
74
+ tokens: formatLifetimeTokens(record),
75
+ activity: record.stateTerminalReason ? undefined : describeActivity(record.activeTools, record.responseText),
76
+ spinnerFrame: spinnerFrame++,
77
+ });
78
+ onUpdate({ content: [{ type: "text", text: `${record.toolUses} tool uses...` }], details: details as any });
79
+ };
80
+ const timer = setInterval(update, 500);
81
+
82
+ let outcome: DeliveryOutcome;
83
+ try {
84
+ outcome = await this.manager.launch(snapshot, config.identity.subagentType, config.execution.prompt, {
85
+ description: config.execution.description,
86
+ model: config.execution.model,
87
+ maxTurns: config.execution.effectiveMaxTurns,
88
+ inheritContext: config.execution.inheritContext,
89
+ thinkingLevel: config.execution.effectiveThinkingLevel,
90
+ mode: config.execution.mode,
91
+ timeoutSeconds: config.execution.timeoutSeconds,
92
+ origin: "tool",
93
+ invocation: config.execution.agentInvocation,
94
+ parentSession,
95
+ signal,
96
+ onCreated: (record) => { currentId = record.id; update(); },
97
+ });
98
+ currentId = outcome.kind === "joined" ? outcome.record.id : outcome.agentId;
99
+ update();
100
+ } catch (error) {
101
+ return textResult(error instanceof Error ? error.message : String(error));
102
+ } finally {
103
+ clearInterval(timer);
104
+ }
105
+
106
+ const record = this.manager.getRecord(currentId!);
107
+ if (!record) return textResult(`Agent not found after launch: "${currentId}".`);
108
+ if (outcome.kind === "detached") {
109
+ return textResult(
110
+ `Agent detached and ${record.status === "queued" ? "queued" : "started"}.\n` +
111
+ `Agent ID: ${record.id}\nRun ID: ${record.runId}\nType: ${config.identity.displayName}\n` +
112
+ `Mode: detached\nModel: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\n` +
113
+ `Description: ${record.description}\n` +
114
+ (record.status === "queued" ? "Admission: queued\n" : "") +
115
+ "Completion will send one notification. Use get_subagent_result for the bounded final result.",
116
+ buildDetails(config.presentation.detailBase, record, {
117
+ status: record.status,
118
+ terminalReason: record.stateTerminalReason,
119
+ durationMs: record.activeRuntimeMs,
120
+ }),
121
+ );
122
+ }
123
+
124
+ record.markConsumed();
125
+ const details = buildDetails(config.presentation.detailBase, record, {
126
+ status: record.status,
127
+ terminalReason: record.stateTerminalReason,
128
+ tokens: formatLifetimeTokens(record),
129
+ durationMs: record.activeRuntimeMs,
130
+ });
131
+ const fallback = config.identity.fellBack ? `Unknown agent type "${config.identity.rawType}" — using ${config.identity.subagentType}.\n\n` : "";
132
+ const boundedResult = boundResult(record.result, record.outputFile);
133
+ if (record.status === "error") {
134
+ return textResult(`${fallback}Model: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatMs(record.activeRuntimeMs)}\nAgent failed: ${record.error}${boundedResult ? `\n\nPartial output:\n${boundedResult}` : ""}`, details);
135
+ }
136
+ return textResult(`${fallback}Model: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatMs(record.activeRuntimeMs)}\nAgent ${record.status}${record.stateTerminalReason ? ` (${record.stateTerminalReason.replaceAll("_", " ")})` : ""}.\n\n${boundedResult}`, details);
137
+ }
138
+
139
+ toToolDefinition() {
140
+ const typeListText = this.typeListText;
141
+ const availableTypesText = this.availableTypesText;
142
+ const agentDir = this.agentDir;
143
+ const registry = this.registry;
144
+ const guidelines = [
145
+ "- Use mode: joined when the parent needs the result in this turn; use mode: detached for independent work (default).",
146
+ ...this.agentGuidelines,
147
+ "- Provide clear, detailed prompts so the agent can work autonomously.",
148
+ "- Detached completion automatically wakes you with one bounded result preview; do not poll.",
149
+ "- Use resume_subagent to continue a retained session and stop_subagent to request cooperative cancellation.",
150
+ '- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").',
151
+ "- Use thinking to control extended thinking level.",
152
+ "- Use inherit_context if the agent needs the parent conversation history.",
153
+ ].join("\n");
154
+
155
+ return defineTool({
156
+ name: "subagent" as const,
157
+ label: "Subagent",
158
+ promptSnippet: "subagent: Launch a specialized agent for complex, multi-step tasks.",
159
+ description: `Launch one specialized agent. Delivery defaults to detached; choose joined when this parent turn must receive the settled result.\n\nAvailable agent types:\n${typeListText}\n\nGuidelines:\n${guidelines}\n`,
160
+ parameters: Type.Object({
161
+ prompt: Type.String({ description: "The task for the agent to perform." }),
162
+ description: Type.String({ description: "A short (3-5 word) description of the task." }),
163
+ subagent_type: Type.String({ description: `The specialized agent type. Available types: ${availableTypesText}. Custom agents from .pi/agents/<name>.md or ${agentDir}/agents/<name>.md are also available.` }),
164
+ model: Type.Optional(Type.String({ description: 'Optional model override: "provider/modelId" or fuzzy name.' })),
165
+ thinking: Type.Optional(Type.String({ description: `Thinking level: ${THINKING_LEVELS_DESCRIPTION}.` })),
166
+ max_turns: Type.Optional(Type.Integer({ description: "Maximum agentic turns. Use 0 or omit for unlimited.", minimum: 0 })),
167
+ timeout_seconds: Type.Optional(Type.Integer({ description: "Active runtime deadline in seconds; queued time is excluded.", minimum: 1 })),
168
+ mode: Type.Optional(Type.Union([Type.Literal("joined"), Type.Literal("detached")], { description: "Result delivery mode. Defaults to detached." })),
169
+ inherit_context: Type.Optional(Type.Boolean({ description: "Fork parent conversation into the agent. Default: false." })),
170
+ }),
171
+ renderCall(args: Record<string, unknown>, theme: any) {
172
+ const displayName = args.subagent_type ? getDisplayName(args.subagent_type as string, registry) : "Subagent";
173
+ const desc = (args.description as string | undefined) ?? "";
174
+ return new Text(" " + theme.fg("toolTitle", theme.bold(displayName)) + (desc ? " " + theme.fg("muted", desc) : ""), 0, 0);
175
+ },
176
+ renderResult(result: any, { expanded, isPartial }: any, theme: any) {
177
+ const details = result.details as AgentDetails | undefined;
178
+ if (!details) return new Text(result.content[0]?.type === "text" ? result.content[0].text : "", 0, 0);
179
+ const text = result.content[0]?.type === "text" ? result.content[0].text : "";
180
+ return new Text(renderAgentResult(details, text, expanded, isPartial, theme), 0, 0);
181
+ },
182
+ execute: (toolCallId: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: ((update: AgentToolResult<any>) => void) | undefined, ctx: any) => this.execute(toolCallId, params, signal, onUpdate, ctx),
183
+ });
184
+ }
185
+ }
261
186
 
262
- execute: (
263
- toolCallId: string,
264
- params: Record<string, unknown>,
265
- signal: AbortSignal | undefined,
266
- onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
267
- ctx: any,
268
- ) => this.execute(toolCallId, params, signal, onUpdate, ctx),
269
- });
270
- }
187
+ const MAX_RESULT_OUTPUT = 12_000;
188
+ function boundResult(result: string | undefined, outputFile: string | undefined): string {
189
+ const output = result?.trim() || "No output.";
190
+ return output.length > MAX_RESULT_OUTPUT
191
+ ? output.slice(0, MAX_RESULT_OUTPUT) + `\n\nOutput truncated. Full transcript: ${outputFile ?? "unavailable"}`
192
+ : output;
271
193
  }
@@ -1,104 +1,61 @@
1
1
  import { defineTool } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import type { AgentConfigLookup } from "#src/config/agent-types";
4
- import { type AgentReport, formatAgentReport } from "#src/tools/get-result-report";
4
+ import type { Subagent } from "#src/lifecycle/subagent";
5
5
  import { formatLifetimeTokens, textResult } from "#src/tools/helpers";
6
- import type { Subagent } from "#src/types";
7
- import { formatDuration, getDisplayName } from "#src/ui/display";
6
+ import type { AgentDetails } from "#src/ui/display";
7
+ import { describeActivity, formatMs, formatModelThinking } from "#src/ui/display";
8
8
 
9
- // ---- Deps interfaces ----
9
+ const MAX_RESULT_OUTPUT = 12_000;
10
10
 
11
- export interface GetResultToolManager {
12
- getRecord(id: string): Subagent | undefined;
13
- }
14
-
15
- // ---- Class ----
11
+ export interface GetResultToolManager { getRecord(id: string): Subagent | undefined; }
16
12
 
17
13
  export class GetResultTool {
18
- constructor(
19
- private readonly manager: GetResultToolManager,
20
- private readonly registry: AgentConfigLookup,
21
- ) {}
22
-
23
- async execute(
24
- _toolCallId: string,
25
- params: { agent_id: string; wait?: boolean; verbose?: boolean },
26
- signal: AbortSignal,
27
- _onUpdate: unknown,
28
- _ctx: unknown,
29
- ) {
30
- const record = this.manager.getRecord(params.agent_id);
31
- if (!record) {
32
- return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
33
- }
34
-
35
- // A queued record is awaitable from spawn, and a resumed record republishes
36
- // its live promise. This claim is synchronous so completion cannot race the
37
- // direct result with an asynchronous follow-up notification. Interrupting
38
- // this tool stops only the wait.
39
- if (params.wait) await record.waitForResult(signal);
40
-
41
- // Pull delivery: only a terminal result was actually collected.
42
- if (!record.isActive()) record.markConsumed();
43
-
44
- return textResult(formatAgentReport(this.buildReport(record, params.verbose)));
45
- }
46
-
47
- private buildReport(record: Subagent, verbose?: boolean): AgentReport {
48
- return {
49
- id: record.id,
50
- displayName: getDisplayName(record.type, this.registry),
51
- modelLabel: record.modelLabel,
52
- thinkingLevel: record.effectiveThinkingLevel,
53
- status: record.status,
54
- toolUses: record.toolUses,
55
- tokens: formatLifetimeTokens(record),
56
- contextPercent: record.getContextPercent(),
57
- compactionCount: record.compactionCount,
58
- duration: formatDuration(record.startedAt, record.completedAt),
59
- description: record.description,
60
- result: record.result,
61
- error: record.error,
62
- stoppedWhileQueued: record.stoppedWhileQueued,
63
- conversation: verbose ? record.getConversation() : undefined,
64
- transcriptPath: record.outputFile,
65
- };
66
- }
14
+ constructor(private readonly manager: GetResultToolManager, private readonly registry: AgentConfigLookup) {}
15
+
16
+ async execute(_toolCallId: string, params: { agent_id: string }, _signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: unknown) {
17
+ const record = this.manager.getRecord(params.agent_id);
18
+ if (!record) return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
19
+ if (!record.isActive()) record.markConsumed();
20
+
21
+ const activity = describeActivity(record.activeTools, record.responseText);
22
+ const text = record.isActive()
23
+ ? `Agent ${record.id} is ${record.status}.\nRun ID: ${record.runId}\nMode: ${record.mode}\nModel: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatMs(record.activeRuntimeMs)} (running)\nActivity: ${activity}`
24
+ : renderTerminalResult(record);
25
+ const details: AgentDetails = {
26
+ displayName: this.registry.resolveAgentConfig(record.type).displayName ?? record.type,
27
+ description: record.description,
28
+ subagentType: record.type,
29
+ toolUses: record.toolUses,
30
+ tokens: formatLifetimeTokens(record),
31
+ durationMs: record.activeRuntimeMs,
32
+ status: record.status,
33
+ terminalReason: record.stateTerminalReason,
34
+ modelName: record.modelLabel,
35
+ thinkingLevel: record.effectiveThinkingLevel,
36
+ turnCount: record.turnCount,
37
+ maxTurns: record.maxTurns,
38
+ activity,
39
+ agentId: record.id,
40
+ error: record.error,
41
+ };
42
+ return textResult(text, details);
43
+ }
44
+
45
+ toToolDefinition() {
46
+ return defineTool({
47
+ name: "get_subagent_result" as const,
48
+ label: "Get Subagent Result",
49
+ promptSnippet: "get_subagent_result: Read a bounded subagent result without waiting.",
50
+ description: "Read a subagent's current status or bounded terminal result. This tool never waits and never dumps the full conversation. Use the transcript path when output is truncated.",
51
+ parameters: Type.Object({ agent_id: Type.String({ description: "The subagent ID to inspect." }) }),
52
+ execute: (toolCallId: string, params: { agent_id: string }, signal: AbortSignal, onUpdate: unknown, ctx: unknown) => this.execute(toolCallId, params, signal, onUpdate, ctx),
53
+ });
54
+ }
55
+ }
67
56
 
68
- toToolDefinition() {
69
- return defineTool({
70
- name: "get_subagent_result" as const,
71
- label: "Get Agent Result",
72
- promptSnippet:
73
- "get_subagent_result: Inspect status or retrieve full results from a background agent.",
74
- description:
75
- "Background completion automatically wakes you with a result preview, so do not poll. " +
76
- "Use this tool only for full output, verbose conversation, an explicit status check or synchronization point, " +
77
- "or recovery after a missed notification.",
78
- parameters: Type.Object({
79
- agent_id: Type.String({
80
- description: "The agent ID to check.",
81
- }),
82
- wait: Type.Optional(
83
- Type.Boolean({
84
- description:
85
- "If true, wait for the agent to complete before returning. Default: false.",
86
- }),
87
- ),
88
- verbose: Type.Optional(
89
- Type.Boolean({
90
- description:
91
- "If true, include the agent's full conversation (messages + tool calls). Default: false.",
92
- }),
93
- ),
94
- }),
95
- execute: (
96
- toolCallId: string,
97
- params: { agent_id: string; wait?: boolean; verbose?: boolean },
98
- signal: AbortSignal,
99
- onUpdate: unknown,
100
- ctx: unknown,
101
- ) => this.execute(toolCallId, params, signal, onUpdate, ctx),
102
- });
103
- }
57
+ function renderTerminalResult(record: Subagent): string {
58
+ const output = record.result?.trim() || "No output.";
59
+ const bounded = output.length > MAX_RESULT_OUTPUT ? output.slice(0, MAX_RESULT_OUTPUT) + `\n\nOutput truncated. Full transcript: ${record.outputFile ?? "unavailable"}` : output;
60
+ return `Agent ${record.id} ${record.status}.\nRun ID: ${record.runId}\nMode: ${record.mode}\nModel: ${formatModelThinking(record.modelLabel, record.effectiveThinkingLevel)}\nRuntime: ${formatMs(record.activeRuntimeMs)}\nReason: ${record.stateTerminalReason ?? "unknown"}\n\n${record.status === "error" ? `Error: ${record.error}\n\n` : ""}${bounded}`;
104
61
  }