@tt-a1i/openpi 0.1.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.
- package/README.md +643 -0
- package/SETUP.md +74 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-package.png +0 -0
- package/assets/readme-hero-mobile.svg +72 -0
- package/assets/readme-hero.svg +118 -0
- package/assets/readme-runtime-mobile.svg +91 -0
- package/assets/readme-runtime.svg +111 -0
- package/extensions/ask-user/handoff.ts +205 -0
- package/extensions/ask-user/index.ts +1110 -0
- package/extensions/ask-user/limits.ts +89 -0
- package/extensions/ask-user/prompt.ts +76 -0
- package/extensions/background-terminals/index.ts +653 -0
- package/extensions/background-terminals/src/domain.ts +99 -0
- package/extensions/background-terminals/src/manager.ts +989 -0
- package/extensions/background-terminals/src/output.ts +84 -0
- package/extensions/background-terminals/src/prompt.ts +195 -0
- package/extensions/background-terminals/src/result-delivery.ts +43 -0
- package/extensions/background-terminals/src/runtime.ts +36 -0
- package/extensions/background-terminals/src/ui/output-view.ts +55 -0
- package/extensions/background-terminals/src/ui/ps.ts +642 -0
- package/extensions/background-terminals/src/ui/tool-result.ts +146 -0
- package/extensions/background-terminals/src/watch.ts +192 -0
- package/extensions/context-pivot/index.ts +222 -0
- package/extensions/copy-all/index.ts +65 -0
- package/extensions/cron/index.ts +173 -0
- package/extensions/cron/schedule.ts +127 -0
- package/extensions/file-mutation-display/index.ts +105 -0
- package/extensions/file-mutation-display/render.ts +107 -0
- package/extensions/file-search/index.ts +515 -0
- package/extensions/file-search/src/args.ts +129 -0
- package/extensions/file-search/src/binaries.ts +419 -0
- package/extensions/file-search/src/output.ts +142 -0
- package/extensions/file-search/src/process.ts +309 -0
- package/extensions/file-search/src/prompt.ts +53 -0
- package/extensions/git-info/index.ts +272 -0
- package/extensions/git-info/src/changed-files-view.ts +414 -0
- package/extensions/git-info/src/process.ts +107 -0
- package/extensions/git-info/src/refresh-coordinator.ts +13 -0
- package/extensions/git-info/src/runtime.ts +28 -0
- package/extensions/goal/controller.ts +794 -0
- package/extensions/goal/index.ts +521 -0
- package/extensions/goal/prompts.ts +122 -0
- package/extensions/goal/state.ts +763 -0
- package/extensions/goal/ui.ts +158 -0
- package/extensions/model-info/index.ts +234 -0
- package/extensions/plan-mode/bash-policy.ts +313 -0
- package/extensions/plan-mode/index.ts +539 -0
- package/extensions/post-edit/index.ts +129 -0
- package/extensions/sessions/LICENSE.upstream +21 -0
- package/extensions/sessions/git-stats.ts +226 -0
- package/extensions/sessions/index.ts +1092 -0
- package/extensions/sessions/sessions.ts +385 -0
- package/extensions/setup/index.ts +408 -0
- package/extensions/shared/activity-status.ts +65 -0
- package/extensions/shared/below-editor-navigation.ts +343 -0
- package/extensions/shared/child-session.ts +352 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +102 -0
- package/extensions/shared/plan-mode-state.ts +65 -0
- package/extensions/shared/setup-config.ts +971 -0
- package/extensions/shared/subagent-roles.ts +22 -0
- package/extensions/shared/terminal-text.ts +38 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/worktree.ts +526 -0
- package/extensions/subagents/index.ts +1225 -0
- package/extensions/subagents/navigation.ts +121 -0
- package/extensions/subagents/src/agent-types.ts +543 -0
- package/extensions/subagents/src/backend.ts +63 -0
- package/extensions/subagents/src/backends/pi.ts +493 -0
- package/extensions/subagents/src/backends/stub.ts +296 -0
- package/extensions/subagents/src/by-the-way.ts +21 -0
- package/extensions/subagents/src/domain.ts +271 -0
- package/extensions/subagents/src/format.ts +48 -0
- package/extensions/subagents/src/manager.ts +769 -0
- package/extensions/subagents/src/prompt.ts +190 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +51 -0
- package/extensions/subagents/src/ui/takeover.ts +615 -0
- package/extensions/subagents/src/ui/transcript.ts +293 -0
- package/extensions/subagents/src/ui/wait-result.ts +89 -0
- package/extensions/suggestions/index.ts +172 -0
- package/extensions/suggestions/src/config.ts +12 -0
- package/extensions/suggestions/src/predictor.ts +147 -0
- package/extensions/suggestions/src/prompt.ts +20 -0
- package/extensions/suggestions/src/transcript.ts +233 -0
- package/extensions/suggestions/src/ui.ts +224 -0
- package/extensions/tasks/index.ts +512 -0
- package/extensions/tasks/tasks.ts +649 -0
- package/extensions/tasks/ui.ts +421 -0
- package/extensions/turn-time/index.ts +61 -0
- package/extensions/ui-customization/footer.ts +512 -0
- package/extensions/ui-customization/index.ts +217 -0
- package/extensions/workflows/acceptance.ts +298 -0
- package/extensions/workflows/artifacts.ts +225 -0
- package/extensions/workflows/controller.ts +210 -0
- package/extensions/workflows/dashboard.ts +1226 -0
- package/extensions/workflows/index.ts +1884 -0
- package/extensions/workflows/journal.ts +188 -0
- package/extensions/workflows/meta.ts +250 -0
- package/extensions/workflows/model.ts +423 -0
- package/extensions/workflows/navigation.ts +93 -0
- package/extensions/workflows/prompt.ts +212 -0
- package/extensions/workflows/replay-safety.ts +577 -0
- package/extensions/workflows/runner.ts +786 -0
- package/extensions/workflows/sandbox-child.cjs +402 -0
- package/extensions/workflows/sandbox.ts +397 -0
- package/extensions/workflows/serialization.ts +162 -0
- package/extensions/workflows/worktree-handoff.ts +216 -0
- package/package.json +87 -0
- package/scripts/prepare-effect-tsgo.mjs +16 -0
- package/skills/background-terminals/SKILL.md +30 -0
- package/skills/subagents/SKILL.md +15 -0
- package/themes/github-dark-default.json +89 -0
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagents — spawn background subagents as in-process pi sessions, behind a
|
|
3
|
+
* single Effect service interface.
|
|
4
|
+
*
|
|
5
|
+
* Tools (for the parent LLM):
|
|
6
|
+
* - subagent_spawn: fire-and-forget spawn (prompt, name, optional harness,
|
|
7
|
+
* working_dir, model, reasoning_effort, and agent_type when this environment
|
|
8
|
+
* defines any). Model-spawned subagents and user /btw asides run in separate
|
|
9
|
+
* pools (MAX_RUNNING and MAX_RUNNING_BTW).
|
|
10
|
+
* - subagent_wait: block until the listed subagents settle, return results.
|
|
11
|
+
* - subagent_cancel: stop one or more running subagents.
|
|
12
|
+
* - subagent_check: peek at a subagent's status and recent activity.
|
|
13
|
+
* - subagent_list: list all subagents.
|
|
14
|
+
*
|
|
15
|
+
* Unawaited subagents queue their result as a follow-up message when they
|
|
16
|
+
* settle. `/subagents` opens a picker + full interactive takeover view.
|
|
17
|
+
*
|
|
18
|
+
* Agent types (`src/agent-types.ts`) are optional named presets that fix a
|
|
19
|
+
* child's system prompt, model, and tool allowlist; see `docs/agent-types.md`.
|
|
20
|
+
*
|
|
21
|
+
* Architecture: Effect v4 generators throughout (backend -> manager ->
|
|
22
|
+
* runtime); this file is the async boundary where tool handlers run effects
|
|
23
|
+
* against one shared ManagedRuntime.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
29
|
+
import type {
|
|
30
|
+
ExtensionAPI,
|
|
31
|
+
ExtensionCommandContext,
|
|
32
|
+
ExtensionContext,
|
|
33
|
+
ExtensionUIContext,
|
|
34
|
+
} from "@earendil-works/pi-coding-agent";
|
|
35
|
+
import {
|
|
36
|
+
CustomEditor,
|
|
37
|
+
DEFAULT_MAX_BYTES,
|
|
38
|
+
DEFAULT_MAX_LINES,
|
|
39
|
+
defineTool,
|
|
40
|
+
formatSize,
|
|
41
|
+
getAgentDir,
|
|
42
|
+
getMarkdownTheme,
|
|
43
|
+
keyHint,
|
|
44
|
+
truncateHead,
|
|
45
|
+
} from "@earendil-works/pi-coding-agent";
|
|
46
|
+
import { Markdown, Text } from "@earendil-works/pi-tui";
|
|
47
|
+
import { Type } from "typebox";
|
|
48
|
+
import {
|
|
49
|
+
formatAgentTypeDiagnostics,
|
|
50
|
+
loadAgentTypes,
|
|
51
|
+
roleModelForAgentType,
|
|
52
|
+
selectSubagentModel,
|
|
53
|
+
type AgentType,
|
|
54
|
+
} from "./src/agent-types.ts";
|
|
55
|
+
import { deriveBtwTitle, isModelVisible } from "./src/by-the-way.ts";
|
|
56
|
+
import {
|
|
57
|
+
BACKEND_NAMES,
|
|
58
|
+
formatElapsed,
|
|
59
|
+
latestText,
|
|
60
|
+
REASONING_EFFORTS,
|
|
61
|
+
type SubagentSnapshot,
|
|
62
|
+
} from "./src/domain.ts";
|
|
63
|
+
import {
|
|
64
|
+
formatActivityStatus,
|
|
65
|
+
hasActivity,
|
|
66
|
+
unreadActivityCounts,
|
|
67
|
+
} from "../shared/activity-status.ts";
|
|
68
|
+
import { formatContextUtilization } from "./src/format.ts";
|
|
69
|
+
import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
|
|
70
|
+
import {
|
|
71
|
+
buildSubagentResultMessage,
|
|
72
|
+
createAgentTypeParameterSchema,
|
|
73
|
+
buildSubagentSendResult,
|
|
74
|
+
buildSubagentSpawnResult,
|
|
75
|
+
buildSubagentSpawnToolDescription,
|
|
76
|
+
SUBAGENT_CANCEL_PARAMETER_DESCRIPTIONS,
|
|
77
|
+
SUBAGENT_CANCEL_TOOL_DESCRIPTION,
|
|
78
|
+
SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS,
|
|
79
|
+
SUBAGENT_CHECK_TOOL_DESCRIPTION,
|
|
80
|
+
SUBAGENT_LIST_TOOL_DESCRIPTION,
|
|
81
|
+
SUBAGENT_SEND_PARAMETER_DESCRIPTIONS,
|
|
82
|
+
SUBAGENT_SEND_TOOL_DESCRIPTION,
|
|
83
|
+
SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS,
|
|
84
|
+
SUBAGENT_SPAWN_PROMPT_GUIDELINES,
|
|
85
|
+
SUBAGENT_SPAWN_PROMPT_SNIPPET,
|
|
86
|
+
SUBAGENT_SPAWN_TOOL_DESCRIPTION,
|
|
87
|
+
SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS,
|
|
88
|
+
SUBAGENT_WAIT_TOOL_DESCRIPTION,
|
|
89
|
+
} from "./src/prompt.ts";
|
|
90
|
+
import { createDeferredResultDelivery } from "./src/result-delivery.ts";
|
|
91
|
+
import { resultDeliveryOptions } from "../background-terminals/src/result-delivery.ts";
|
|
92
|
+
import {
|
|
93
|
+
effectiveChildToolAllowlist,
|
|
94
|
+
resolveStandaloneChildProjectTrust,
|
|
95
|
+
} from "../shared/child-session.ts";
|
|
96
|
+
import {
|
|
97
|
+
BelowEditorNavigationEditor,
|
|
98
|
+
BelowEditorStripState,
|
|
99
|
+
} from "../shared/below-editor-navigation.ts";
|
|
100
|
+
import { loadSetupConfig } from "../shared/setup-config.ts";
|
|
101
|
+
import {
|
|
102
|
+
PLAN_MODE_CHANNEL,
|
|
103
|
+
planModeAllowsDeclaredTools,
|
|
104
|
+
planModeChildTools,
|
|
105
|
+
type PlanModeState,
|
|
106
|
+
} from "../shared/plan-mode-state.ts";
|
|
107
|
+
import {
|
|
108
|
+
createWorktree,
|
|
109
|
+
reclaimWorktree,
|
|
110
|
+
type Worktree,
|
|
111
|
+
} from "../shared/worktree.ts";
|
|
112
|
+
import {
|
|
113
|
+
createSubagentRuntime,
|
|
114
|
+
runTool,
|
|
115
|
+
type SubagentRuntime,
|
|
116
|
+
} from "./src/runtime.ts";
|
|
117
|
+
import {
|
|
118
|
+
normalizeSubagentTitle,
|
|
119
|
+
selectSubagentStripEntry,
|
|
120
|
+
SubagentStripWidget,
|
|
121
|
+
} from "./navigation.ts";
|
|
122
|
+
import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
|
|
123
|
+
import {
|
|
124
|
+
renderWaitResult,
|
|
125
|
+
type WaitResultDetails,
|
|
126
|
+
} from "./src/ui/wait-result.ts";
|
|
127
|
+
|
|
128
|
+
const SUBAGENT_OUTPUT_MAX_BYTES = 24 * 1024;
|
|
129
|
+
const WAIT_OUTPUT_MAX_BYTES = 48 * 1024;
|
|
130
|
+
const WAIT_PER_AGENT_MAX_BYTES = 16 * 1024;
|
|
131
|
+
|
|
132
|
+
interface SpawnResultDetails {
|
|
133
|
+
readonly id?: string;
|
|
134
|
+
readonly title?: string;
|
|
135
|
+
readonly harness?: string;
|
|
136
|
+
readonly model?: string;
|
|
137
|
+
readonly agentType?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface SubagentFinishedData {
|
|
141
|
+
readonly id: string;
|
|
142
|
+
readonly title: string;
|
|
143
|
+
readonly status: SubagentSnapshot["status"];
|
|
144
|
+
readonly elapsed: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface BtwResultData {
|
|
148
|
+
readonly id: string;
|
|
149
|
+
readonly title: string;
|
|
150
|
+
readonly status: SubagentSnapshot["status"];
|
|
151
|
+
readonly errorText?: string;
|
|
152
|
+
readonly prompt: string;
|
|
153
|
+
readonly answer: string;
|
|
154
|
+
readonly sessionFilePath?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function describeSubagent(snap: SubagentSnapshot) {
|
|
158
|
+
const details = [
|
|
159
|
+
`${snap.backend}: ${snap.meta.modelLabel ?? "?"}`,
|
|
160
|
+
formatContextUtilization(snap.usage),
|
|
161
|
+
formatElapsed(snap),
|
|
162
|
+
snap.cwd,
|
|
163
|
+
].filter(Boolean);
|
|
164
|
+
return `${snap.id} [${snap.status}] "${snap.title}" (${details.join(", ")})`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function truncatedOutput(
|
|
168
|
+
snap: SubagentSnapshot,
|
|
169
|
+
maxBytes = SUBAGENT_OUTPUT_MAX_BYTES,
|
|
170
|
+
): string {
|
|
171
|
+
const output = snap.finalText || "(no output)";
|
|
172
|
+
const truncation = truncateHead(output, {
|
|
173
|
+
maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
|
|
174
|
+
maxLines: Math.min(600, DEFAULT_MAX_LINES),
|
|
175
|
+
});
|
|
176
|
+
let text = truncation.content;
|
|
177
|
+
if (truncation.truncated) {
|
|
178
|
+
text += `\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)} shown. Full transcript in session file: ${snap.meta.sessionFilePath ?? "?"}]`;
|
|
179
|
+
}
|
|
180
|
+
return text;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export default function (pi: ExtensionAPI) {
|
|
184
|
+
let runtime: SubagentRuntime | undefined;
|
|
185
|
+
let managerPromise: Promise<SubagentManagerShape> | undefined;
|
|
186
|
+
let sessionContext: ExtensionContext | undefined;
|
|
187
|
+
let ui: ExtensionUIContext | undefined;
|
|
188
|
+
let unsubStatus: (() => void) | undefined;
|
|
189
|
+
/**
|
|
190
|
+
* Settled subagents are an unread notice: the user's next explicit request
|
|
191
|
+
* acknowledges everything that had already finished.
|
|
192
|
+
*/
|
|
193
|
+
let settledAcknowledgedAt = 0;
|
|
194
|
+
const stripState = new BelowEditorStripState();
|
|
195
|
+
const widgetKey = "subagent-navigation";
|
|
196
|
+
let navigationManager: SubagentManagerShape | undefined;
|
|
197
|
+
let widgetVisible = false;
|
|
198
|
+
let requestWidgetRender: (() => void) | undefined;
|
|
199
|
+
let dashboardOpen = false;
|
|
200
|
+
const resultDelivery = createDeferredResultDelivery<SubagentSnapshot>();
|
|
201
|
+
|
|
202
|
+
const getRuntime = () => (runtime ??= createSubagentRuntime());
|
|
203
|
+
|
|
204
|
+
/** Resolve the manager service once per runtime and wire the extension hooks. */
|
|
205
|
+
const getManager = () => {
|
|
206
|
+
managerPromise ??= getRuntime()
|
|
207
|
+
.runPromise(SubagentManager)
|
|
208
|
+
.then((manager) => {
|
|
209
|
+
navigationManager = manager;
|
|
210
|
+
manager.view.setOnSettled(onSettled);
|
|
211
|
+
unsubStatus?.();
|
|
212
|
+
unsubStatus = manager.view.subscribe(() => updateStatus(manager));
|
|
213
|
+
updateStatus(manager);
|
|
214
|
+
return manager;
|
|
215
|
+
});
|
|
216
|
+
return managerPromise;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const stripEntry = () =>
|
|
220
|
+
navigationManager
|
|
221
|
+
? selectSubagentStripEntry(
|
|
222
|
+
navigationManager.view.list(),
|
|
223
|
+
settledAcknowledgedAt,
|
|
224
|
+
)
|
|
225
|
+
: undefined;
|
|
226
|
+
|
|
227
|
+
const updateSubagentWidget = () => {
|
|
228
|
+
const ctx = sessionContext;
|
|
229
|
+
if (!ctx || ctx.mode !== "tui") return;
|
|
230
|
+
const visible = Boolean(stripEntry());
|
|
231
|
+
if (visible === widgetVisible) return;
|
|
232
|
+
if (!visible) {
|
|
233
|
+
stripState.focused = false;
|
|
234
|
+
requestWidgetRender = undefined;
|
|
235
|
+
ctx.ui.setWidget(widgetKey, undefined);
|
|
236
|
+
widgetVisible = false;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
ctx.ui.setWidget(
|
|
240
|
+
widgetKey,
|
|
241
|
+
(tui, theme) => {
|
|
242
|
+
requestWidgetRender = () => tui.requestRender();
|
|
243
|
+
return new SubagentStripWidget(tui, theme, stripState, stripEntry);
|
|
244
|
+
},
|
|
245
|
+
{ placement: "belowEditor" },
|
|
246
|
+
);
|
|
247
|
+
widgetVisible = true;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const updateStatus = (manager: SubagentManagerShape) => {
|
|
251
|
+
if (!ui) return;
|
|
252
|
+
const counts = unreadActivityCounts(
|
|
253
|
+
manager.view.list(),
|
|
254
|
+
settledAcknowledgedAt,
|
|
255
|
+
);
|
|
256
|
+
ui.setStatus(
|
|
257
|
+
"subagents",
|
|
258
|
+
hasActivity(counts)
|
|
259
|
+
? formatActivityStatus(ui.theme, "subagents", counts)
|
|
260
|
+
: undefined,
|
|
261
|
+
);
|
|
262
|
+
updateSubagentWidget();
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const openDashboard = async (ctx: ExtensionContext, initialId?: string) => {
|
|
266
|
+
if (dashboardOpen || ctx.mode !== "tui") return;
|
|
267
|
+
dashboardOpen = true;
|
|
268
|
+
stripState.focused = false;
|
|
269
|
+
let manager: SubagentManagerShape | undefined;
|
|
270
|
+
try {
|
|
271
|
+
manager = await getManager();
|
|
272
|
+
if (manager.view.size() === 0) return;
|
|
273
|
+
await openSubagentPicker(ctx, manager.view, initialId);
|
|
274
|
+
settledAcknowledgedAt = Date.now();
|
|
275
|
+
} finally {
|
|
276
|
+
dashboardOpen = false;
|
|
277
|
+
if (manager) updateStatus(manager);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const installSubagentNavigation = (ctx: ExtensionContext) => {
|
|
282
|
+
if (ctx.mode !== "tui") return;
|
|
283
|
+
const previous = ctx.ui.getEditorComponent();
|
|
284
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
285
|
+
const base =
|
|
286
|
+
previous?.(tui, theme, keybindings) ??
|
|
287
|
+
new CustomEditor(tui, theme, keybindings);
|
|
288
|
+
return new BelowEditorNavigationEditor(
|
|
289
|
+
base,
|
|
290
|
+
keybindings,
|
|
291
|
+
stripState,
|
|
292
|
+
() => Boolean(stripEntry()),
|
|
293
|
+
() => {
|
|
294
|
+
const entry = stripEntry();
|
|
295
|
+
if (entry) void openDashboard(ctx, entry.snapshot.id);
|
|
296
|
+
},
|
|
297
|
+
() => {
|
|
298
|
+
requestWidgetRender?.();
|
|
299
|
+
tui.requestRender();
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* `wake` decides whether this costs the model a turn. A subagent that
|
|
307
|
+
* settled while the model sits idle is the result it is waiting on. A
|
|
308
|
+
* backlog that piled up while it worked is not: waking once per stale
|
|
309
|
+
* subagent forces a turn each, and the model can only answer "that one
|
|
310
|
+
* already finished". `nextTurn` still enters context with the user's next
|
|
311
|
+
* message, without demanding a reply.
|
|
312
|
+
*/
|
|
313
|
+
const deliverResults = (
|
|
314
|
+
snaps: readonly SubagentSnapshot[],
|
|
315
|
+
wake: boolean,
|
|
316
|
+
) => {
|
|
317
|
+
if (snaps.length === 0) return;
|
|
318
|
+
pi.sendMessage(
|
|
319
|
+
{
|
|
320
|
+
customType: "subagent-result",
|
|
321
|
+
// One message per flush, not per subagent.
|
|
322
|
+
content: snaps
|
|
323
|
+
.map((snap) =>
|
|
324
|
+
buildSubagentResultMessage({
|
|
325
|
+
id: snap.id,
|
|
326
|
+
title: snap.title,
|
|
327
|
+
status: snap.status,
|
|
328
|
+
errorText: snap.errorText,
|
|
329
|
+
output: truncatedOutput(snap),
|
|
330
|
+
}),
|
|
331
|
+
)
|
|
332
|
+
.join("\n\n"),
|
|
333
|
+
display: true,
|
|
334
|
+
details:
|
|
335
|
+
snaps.length === 1
|
|
336
|
+
? {
|
|
337
|
+
id: snaps[0]!.id,
|
|
338
|
+
title: snaps[0]!.title,
|
|
339
|
+
status: snaps[0]!.status,
|
|
340
|
+
}
|
|
341
|
+
: {
|
|
342
|
+
count: snaps.length,
|
|
343
|
+
results: snaps.map((snap) => ({
|
|
344
|
+
id: snap.id,
|
|
345
|
+
title: snap.title,
|
|
346
|
+
status: snap.status,
|
|
347
|
+
})),
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
resultDeliveryOptions(wake),
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const flushResults = (wake: boolean) => {
|
|
355
|
+
deliverResults(resultDelivery.drain(), wake);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const deliverBtwResult = (snap: SubagentSnapshot) => {
|
|
359
|
+
// appendEntry is a synchronous SessionManager operation and emits an
|
|
360
|
+
// entry_appended event, so it is safe while the parent is streaming and
|
|
361
|
+
// never enters the model's context or follow-up queue.
|
|
362
|
+
pi.appendEntry<BtwResultData>("btw-result", {
|
|
363
|
+
id: snap.id,
|
|
364
|
+
title: snap.title,
|
|
365
|
+
status: snap.status,
|
|
366
|
+
errorText: snap.errorText,
|
|
367
|
+
prompt: snap.prompt,
|
|
368
|
+
answer: truncatedOutput(snap),
|
|
369
|
+
sessionFilePath: snap.meta.sessionFilePath,
|
|
370
|
+
});
|
|
371
|
+
ui?.notify(
|
|
372
|
+
snap.status === "error"
|
|
373
|
+
? `by the way “${snap.title}” failed — reopen it with /subagents`
|
|
374
|
+
: `by the way “${snap.title}” answered — reopen it with /subagents`,
|
|
375
|
+
snap.status === "error" ? "error" : "info",
|
|
376
|
+
);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const onSettled = (snap: SubagentSnapshot, consumed: boolean) => {
|
|
380
|
+
// A shutdown can settle children while disposing their scopes. Never
|
|
381
|
+
// append into a session whose extension runtime is already closing.
|
|
382
|
+
if (!sessionContext) return;
|
|
383
|
+
if (snap.origin === "btw") {
|
|
384
|
+
deliverBtwResult({ ...snap, meta: { ...snap.meta } });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
// Mark the finish in the transcript. The result itself reaches the model
|
|
388
|
+
// separately; this line is for the reader watching the run.
|
|
389
|
+
pi.appendEntry<SubagentFinishedData>("subagent-finished", {
|
|
390
|
+
id: snap.id,
|
|
391
|
+
title: snap.title,
|
|
392
|
+
status: snap.status,
|
|
393
|
+
elapsed: formatElapsed(snap),
|
|
394
|
+
});
|
|
395
|
+
if (consumed) {
|
|
396
|
+
resultDelivery.consume([snap.id]);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// Keep the result retractable while the parent is working. A later
|
|
400
|
+
// subagent_wait can consume it before agent_settled flushes follow-ups.
|
|
401
|
+
// Defer a copy: the live snapshot keeps mutating if the subagent is
|
|
402
|
+
// restarted before the deferred result flushes.
|
|
403
|
+
resultDelivery.defer({ ...snap, meta: { ...snap.meta } });
|
|
404
|
+
// Settled while the model sits idle: it has nothing else in flight, so
|
|
405
|
+
// this is the result it is waiting on — wake it.
|
|
406
|
+
if (sessionContext?.isIdle()) flushResults(true);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
pi.on("session_start", (_event, ctx) => {
|
|
410
|
+
refreshAgentTypes(ctx.cwd, ctx.isProjectTrusted());
|
|
411
|
+
sessionContext = ctx;
|
|
412
|
+
settledAcknowledgedAt = 0;
|
|
413
|
+
if (ctx.hasUI) ui = ctx.ui;
|
|
414
|
+
installSubagentNavigation(ctx);
|
|
415
|
+
updateSubagentWidget();
|
|
416
|
+
// A malformed agent type is silently missing from the roster otherwise, so
|
|
417
|
+
// report it once. Never fatal: the rest still loaded. Non-UI modes receive
|
|
418
|
+
// stderr rather than a model-context message.
|
|
419
|
+
const notice = formatAgentTypeDiagnostics(agentTypeDiagnostics);
|
|
420
|
+
if (notice && ctx.hasUI) ctx.ui.notify(notice, "warning");
|
|
421
|
+
else if (notice) process.stderr.write(`${notice}\n`);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// A new explicit request starts a fresh unread window: previously finished
|
|
425
|
+
// subagents stop being reported, running ones keep reporting.
|
|
426
|
+
pi.on("input", (event) => {
|
|
427
|
+
if (event.source === "extension") return;
|
|
428
|
+
settledAcknowledgedAt = Date.now();
|
|
429
|
+
managerPromise?.then(updateStatus).catch(() => undefined);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// These settled while the model was working on something else, so they go
|
|
433
|
+
// into context without forcing a turn per stale subagent.
|
|
434
|
+
pi.on("agent_settled", () => flushResults(false));
|
|
435
|
+
|
|
436
|
+
pi.on("session_shutdown", async () => {
|
|
437
|
+
resultDelivery.clear();
|
|
438
|
+
unsubStatus?.();
|
|
439
|
+
unsubStatus = undefined;
|
|
440
|
+
try {
|
|
441
|
+
ui?.setStatus("subagents", undefined);
|
|
442
|
+
sessionContext?.ui.setWidget(widgetKey, undefined);
|
|
443
|
+
} catch {
|
|
444
|
+
// UI may already be disposed.
|
|
445
|
+
}
|
|
446
|
+
sessionContext = undefined;
|
|
447
|
+
ui = undefined;
|
|
448
|
+
navigationManager = undefined;
|
|
449
|
+
widgetVisible = false;
|
|
450
|
+
requestWidgetRender = undefined;
|
|
451
|
+
stripState.focused = false;
|
|
452
|
+
dashboardOpen = false;
|
|
453
|
+
const closing = runtime;
|
|
454
|
+
runtime = undefined;
|
|
455
|
+
managerPromise = undefined;
|
|
456
|
+
// Disposing the runtime runs the manager finalizer, which tears down all
|
|
457
|
+
// subagent scopes (and, later, their real child processes).
|
|
458
|
+
await closing?.dispose();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// --- Agent types ---------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Register a safe initial roster before Pi gives us a session context. It
|
|
465
|
+
* includes only built-ins and global types; session_start immediately
|
|
466
|
+
* refreshes it with ctx.cwd and ctx.isProjectTrusted(), including temporary
|
|
467
|
+
* trust decisions and cross-cwd session replacements.
|
|
468
|
+
*/
|
|
469
|
+
let { agentTypes, diagnostics: agentTypeDiagnostics } = loadAgentTypes({
|
|
470
|
+
agentDir: getAgentDir(),
|
|
471
|
+
cwd: process.cwd(),
|
|
472
|
+
projectTrusted: false,
|
|
473
|
+
});
|
|
474
|
+
let agentTypeList = [...agentTypes.values()];
|
|
475
|
+
/**
|
|
476
|
+
* Disambiguates worktree directory/branch names. The subagent id is only
|
|
477
|
+
* assigned inside the manager, after the worktree already has to exist, and
|
|
478
|
+
* two children with the same title would otherwise collide on the branch.
|
|
479
|
+
*/
|
|
480
|
+
let worktreeCounter = 0;
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Mirror of the session's `/plan` stance, published by plan-mode. Kept here
|
|
484
|
+
* rather than queried because spawning must not depend on that extension
|
|
485
|
+
* being loaded: absent it, this stays false and behaviour is unchanged.
|
|
486
|
+
*/
|
|
487
|
+
let planning = false;
|
|
488
|
+
pi.events.on(PLAN_MODE_CHANNEL, (state: unknown) => {
|
|
489
|
+
planning =
|
|
490
|
+
typeof state === "object" &&
|
|
491
|
+
state !== null &&
|
|
492
|
+
(state as PlanModeState).planning === true;
|
|
493
|
+
});
|
|
494
|
+
/**
|
|
495
|
+
* Session trust is not just persisted trust: Pi may grant it for this
|
|
496
|
+
* session only. Re-registering refreshes both the agent_type enum and its
|
|
497
|
+
* model-facing roster before the parent can call subagent_spawn.
|
|
498
|
+
*/
|
|
499
|
+
const refreshAgentTypes = (cwd: string, projectTrusted: boolean) => {
|
|
500
|
+
const loaded = loadAgentTypes({
|
|
501
|
+
agentDir: getAgentDir(),
|
|
502
|
+
cwd,
|
|
503
|
+
projectTrusted,
|
|
504
|
+
});
|
|
505
|
+
agentTypes = loaded.agentTypes;
|
|
506
|
+
agentTypeDiagnostics = loaded.diagnostics;
|
|
507
|
+
agentTypeList = [...agentTypes.values()];
|
|
508
|
+
subagentSpawnTool.description =
|
|
509
|
+
buildSubagentSpawnToolDescription(agentTypeList);
|
|
510
|
+
subagentSpawnTool.parameters = createSubagentSpawnParameters();
|
|
511
|
+
registerSubagentSpawnTool();
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// --- Tools -------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
const createSubagentSpawnParameters = () =>
|
|
517
|
+
Type.Object({
|
|
518
|
+
agent_type: createAgentTypeParameterSchema(agentTypeList),
|
|
519
|
+
prompt: Type.String({
|
|
520
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.prompt,
|
|
521
|
+
}),
|
|
522
|
+
name: Type.String({
|
|
523
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.name,
|
|
524
|
+
}),
|
|
525
|
+
harness: Type.Optional(
|
|
526
|
+
StringEnum(BACKEND_NAMES, {
|
|
527
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.harness,
|
|
528
|
+
}),
|
|
529
|
+
),
|
|
530
|
+
working_dir: Type.Optional(
|
|
531
|
+
Type.String({
|
|
532
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.workingDir,
|
|
533
|
+
}),
|
|
534
|
+
),
|
|
535
|
+
isolation: Type.Optional(
|
|
536
|
+
StringEnum(["worktree"] as const, {
|
|
537
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.isolation,
|
|
538
|
+
}),
|
|
539
|
+
),
|
|
540
|
+
model: Type.Optional(
|
|
541
|
+
Type.String({
|
|
542
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.model,
|
|
543
|
+
}),
|
|
544
|
+
),
|
|
545
|
+
reasoning_effort: Type.Optional(
|
|
546
|
+
StringEnum(REASONING_EFFORTS, {
|
|
547
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort,
|
|
548
|
+
}),
|
|
549
|
+
),
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const subagentSpawnTool = defineTool({
|
|
553
|
+
name: "subagent_spawn",
|
|
554
|
+
label: "Spawn Subagent",
|
|
555
|
+
description: buildSubagentSpawnToolDescription(agentTypeList),
|
|
556
|
+
promptSnippet: SUBAGENT_SPAWN_PROMPT_SNIPPET,
|
|
557
|
+
promptGuidelines: SUBAGENT_SPAWN_PROMPT_GUIDELINES,
|
|
558
|
+
parameters: createSubagentSpawnParameters(),
|
|
559
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
560
|
+
// Only one backend exists; harness is optional and defaults to it.
|
|
561
|
+
const harness = params.harness ?? BACKEND_NAMES[0];
|
|
562
|
+
|
|
563
|
+
const cwd = path.resolve(ctx.cwd, params.working_dir ?? ".");
|
|
564
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
|
565
|
+
throw new Error(`working_dir is not a directory: ${cwd}`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// The enum always includes built-in roles and any loaded overrides.
|
|
569
|
+
const requestedType = (params as { agent_type?: string }).agent_type;
|
|
570
|
+
const agentType = requestedType
|
|
571
|
+
? agentTypes.get(requestedType)
|
|
572
|
+
: undefined;
|
|
573
|
+
|
|
574
|
+
const title = normalizeSubagentTitle(params.name);
|
|
575
|
+
const declaredChildTools = effectiveChildToolAllowlist(agentType?.tools);
|
|
576
|
+
|
|
577
|
+
// A worktree creation mutates git metadata, so reject before attempting
|
|
578
|
+
// it. Plan-mode children are investigation-only and never need one.
|
|
579
|
+
if (planning && params.isolation === "worktree") {
|
|
580
|
+
throw new Error(
|
|
581
|
+
'Plan mode is active: isolation: "worktree" creates a checkout and is unavailable while planning. Omit isolation and use an explorer, reviewer, advisor, or no incompatible agent type for read-only investigation.',
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (
|
|
585
|
+
planning &&
|
|
586
|
+
agentType &&
|
|
587
|
+
!planModeAllowsDeclaredTools(declaredChildTools)
|
|
588
|
+
) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Plan mode is active: agent type "${agentType.name}" would be narrowed to capabilities that contradict its unchanged prompt. Use explorer, reviewer, advisor, or omit agent_type for read-only investigation.`,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Isolation is requested, not best-effort: a caller asks for a worktree
|
|
596
|
+
* precisely because a shared checkout would let this child collide with
|
|
597
|
+
* its siblings, so quietly falling back to `cwd` would deliver the
|
|
598
|
+
* hazard they were avoiding. Fail loudly with git's own reason instead.
|
|
599
|
+
*/
|
|
600
|
+
let worktree: Worktree | undefined;
|
|
601
|
+
if (params.isolation === "worktree") {
|
|
602
|
+
const created = await createWorktree({
|
|
603
|
+
cwd,
|
|
604
|
+
label: title,
|
|
605
|
+
id: String(++worktreeCounter),
|
|
606
|
+
});
|
|
607
|
+
if (!created.ok) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`isolation: "worktree" requested but could not be created (${created.reason}). Omit isolation to run in ${cwd}.`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
worktree = created.worktree;
|
|
613
|
+
}
|
|
614
|
+
const childCwd = worktree?.path ?? cwd;
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Trust follows the directory the worktree was branched from, not the
|
|
618
|
+
* worktree path. It is the same project at the same commit, so a live
|
|
619
|
+
* "trusted" decision that was never persisted must not be downgraded
|
|
620
|
+
* just because the checkout moved into `.git/`.
|
|
621
|
+
*/
|
|
622
|
+
const projectTrusted = resolveStandaloneChildProjectTrust({
|
|
623
|
+
parentCwd: ctx.cwd,
|
|
624
|
+
childCwd: cwd,
|
|
625
|
+
parentTrusted: ctx.isProjectTrusted(),
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Planning narrows the child to investigation tools. This is what makes
|
|
629
|
+
// delegation safe during `/plan`: the allowlist is enforced by the
|
|
630
|
+
// harness, so the child has no write/edit/bash to call, where a
|
|
631
|
+
// tool_call handler in this session could never have reached it.
|
|
632
|
+
const requestedChildTools = planning
|
|
633
|
+
? planModeChildTools(declaredChildTools)
|
|
634
|
+
: declaredChildTools;
|
|
635
|
+
const childTools = effectiveChildToolAllowlist(requestedChildTools);
|
|
636
|
+
// Read at spawn time so `/my-pi-setup` changes affect the next child
|
|
637
|
+
// without reloading this extension. Undefined preserves parent-model
|
|
638
|
+
// inheritance in the backend.
|
|
639
|
+
const model = selectSubagentModel(
|
|
640
|
+
params.model,
|
|
641
|
+
agentType,
|
|
642
|
+
roleModelForAgentType(
|
|
643
|
+
agentType,
|
|
644
|
+
loadSetupConfig().subagents.roleModels,
|
|
645
|
+
),
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
const manager = await getManager();
|
|
649
|
+
const spawn = manager.spawn(harness, {
|
|
650
|
+
prompt: params.prompt,
|
|
651
|
+
title,
|
|
652
|
+
cwd: childCwd,
|
|
653
|
+
// Explicit spawn > role file > package role assignment > parent.
|
|
654
|
+
model,
|
|
655
|
+
reasoningEffort: params.reasoning_effort ?? agentType?.reasoningEffort,
|
|
656
|
+
...(agentType?.body ? { appendSystemPrompt: [agentType.body] } : {}),
|
|
657
|
+
...(childTools ? { tools: childTools } : {}),
|
|
658
|
+
...(agentType ? { agentTypeName: agentType.name } : {}),
|
|
659
|
+
...(worktree ? { worktree: { ...worktree, repoCwd: cwd } } : {}),
|
|
660
|
+
parent: {
|
|
661
|
+
parentCwd: ctx.cwd,
|
|
662
|
+
projectTrusted,
|
|
663
|
+
inheritedModel: ctx.model
|
|
664
|
+
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
665
|
+
: undefined,
|
|
666
|
+
inheritedThinkingLevel: pi.getThinkingLevel(),
|
|
667
|
+
modelRegistry: ctx.modelRegistry,
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
let snap;
|
|
672
|
+
try {
|
|
673
|
+
snap = await runTool(getRuntime(), spawn, {
|
|
674
|
+
signal,
|
|
675
|
+
interruptMessage: "Subagent spawn aborted.",
|
|
676
|
+
});
|
|
677
|
+
} catch (error) {
|
|
678
|
+
// The session scope owns reclamation, but it never opened, so this
|
|
679
|
+
// worktree would otherwise be orphaned on disk.
|
|
680
|
+
if (worktree) await reclaimWorktree(cwd, worktree).catch(() => {});
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
content: [
|
|
686
|
+
{
|
|
687
|
+
type: "text",
|
|
688
|
+
text: buildSubagentSpawnResult({
|
|
689
|
+
id: snap.id,
|
|
690
|
+
title: snap.title,
|
|
691
|
+
harness,
|
|
692
|
+
modelLabel: snap.meta.modelLabel ?? "?",
|
|
693
|
+
cwd: childCwd,
|
|
694
|
+
...(worktree ? { worktreeBranch: worktree.branch } : {}),
|
|
695
|
+
...(agentType ? { agentTypeName: agentType.name } : {}),
|
|
696
|
+
...(childTools ? { tools: childTools } : {}),
|
|
697
|
+
}),
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
details: {
|
|
701
|
+
id: snap.id,
|
|
702
|
+
title: snap.title,
|
|
703
|
+
cwd: childCwd,
|
|
704
|
+
harness,
|
|
705
|
+
model: snap.meta.modelLabel,
|
|
706
|
+
...(agentType ? { agentType: agentType.name } : {}),
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
renderCall() {
|
|
711
|
+
// The result line already names the agent; a bare tool header adds nothing.
|
|
712
|
+
return new Text("");
|
|
713
|
+
},
|
|
714
|
+
renderResult(result, _options, theme) {
|
|
715
|
+
const details = result.details as SpawnResultDetails | undefined;
|
|
716
|
+
if (!details?.id) {
|
|
717
|
+
const first = result.content[0];
|
|
718
|
+
return new Text(
|
|
719
|
+
first?.type === "text" ? first.text : "(no output)",
|
|
720
|
+
0,
|
|
721
|
+
0,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
const meta = [details.harness, details.model]
|
|
725
|
+
.filter(Boolean)
|
|
726
|
+
.join(" \u00b7 ");
|
|
727
|
+
return new Text(
|
|
728
|
+
`${theme.fg("success", "\u25cf")} ${theme.bold(details.title ?? details.id)} ${theme.fg("dim", meta)}`,
|
|
729
|
+
0,
|
|
730
|
+
0,
|
|
731
|
+
);
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const registerSubagentSpawnTool = () => pi.registerTool(subagentSpawnTool);
|
|
736
|
+
registerSubagentSpawnTool();
|
|
737
|
+
|
|
738
|
+
pi.registerTool({
|
|
739
|
+
name: "subagent_wait",
|
|
740
|
+
label: "Wait for Subagents",
|
|
741
|
+
description: SUBAGENT_WAIT_TOOL_DESCRIPTION,
|
|
742
|
+
parameters: Type.Object({
|
|
743
|
+
ids: Type.Array(Type.String(), {
|
|
744
|
+
maxItems: 64,
|
|
745
|
+
description: SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS.ids,
|
|
746
|
+
}),
|
|
747
|
+
}),
|
|
748
|
+
async execute(_toolCallId, params, signal, onUpdate) {
|
|
749
|
+
const manager = await getManager();
|
|
750
|
+
const ids = [...new Set(params.ids)];
|
|
751
|
+
if (ids.length === 0)
|
|
752
|
+
throw new Error("Provide at least one subagent id.");
|
|
753
|
+
const known = manager.view
|
|
754
|
+
.list()
|
|
755
|
+
.filter(isModelVisible)
|
|
756
|
+
.map((snap) => snap.id);
|
|
757
|
+
const unknown = ids.filter((id) => {
|
|
758
|
+
const snap = manager.view.get(id);
|
|
759
|
+
return !snap || !isModelVisible(snap);
|
|
760
|
+
});
|
|
761
|
+
if (unknown.length > 0) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`Unknown subagent id(s): ${unknown.join(", ")}. Known: ${known.join(", ") || "none"}.`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
await runTool(
|
|
768
|
+
getRuntime(),
|
|
769
|
+
manager.waitFor(ids, (pending) => {
|
|
770
|
+
onUpdate?.({
|
|
771
|
+
content: [
|
|
772
|
+
{ type: "text", text: `Waiting for ${pending.join(", ")}...` },
|
|
773
|
+
],
|
|
774
|
+
details: { pending },
|
|
775
|
+
});
|
|
776
|
+
}),
|
|
777
|
+
{ signal, interruptMessage: "Wait aborted. Subagents keep running." },
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
// Settlement may have happened before this wait began. Remove any
|
|
781
|
+
// deferred automatic delivery now that the tool is returning the result.
|
|
782
|
+
resultDelivery.consume(ids);
|
|
783
|
+
|
|
784
|
+
const sections: string[] = [];
|
|
785
|
+
let remainingBytes = WAIT_OUTPUT_MAX_BYTES;
|
|
786
|
+
for (const id of ids) {
|
|
787
|
+
const snap = manager.view.get(id);
|
|
788
|
+
if (!snap) {
|
|
789
|
+
sections.push(`## ${id}\n\n(no longer tracked)`);
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const verb = snap.status === "error" ? "failed" : "finished";
|
|
793
|
+
let section = `## ${snap.id} "${snap.title}" ${verb}`;
|
|
794
|
+
if (snap.errorText) section += `\nError: ${snap.errorText}`;
|
|
795
|
+
const headerBytes = Buffer.byteLength(section, "utf8") + 2;
|
|
796
|
+
const outputBudget = Math.max(
|
|
797
|
+
512,
|
|
798
|
+
Math.min(WAIT_PER_AGENT_MAX_BYTES, remainingBytes - headerBytes),
|
|
799
|
+
);
|
|
800
|
+
section += `\n\n${truncatedOutput(snap, outputBudget)}`;
|
|
801
|
+
const sectionBytes = Buffer.byteLength(section, "utf8");
|
|
802
|
+
if (sectionBytes > remainingBytes) {
|
|
803
|
+
sections.push(
|
|
804
|
+
`## ${snap.id} "${snap.title}"\n\n[omitted: total wait output limit reached]`,
|
|
805
|
+
);
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
sections.push(section);
|
|
809
|
+
remainingBytes -= sectionBytes;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const combined = sections.join("\n\n---\n\n");
|
|
813
|
+
const bounded = truncateHead(combined, {
|
|
814
|
+
maxBytes: WAIT_OUTPUT_MAX_BYTES - 128,
|
|
815
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
816
|
+
});
|
|
817
|
+
const text = bounded.truncated
|
|
818
|
+
? `${bounded.content}\n\n[wait output truncated at the total output limit]`
|
|
819
|
+
: bounded.content;
|
|
820
|
+
return {
|
|
821
|
+
content: [{ type: "text", text }],
|
|
822
|
+
details: {
|
|
823
|
+
results: ids.map((id) => {
|
|
824
|
+
const snap = manager.view.get(id);
|
|
825
|
+
return { id, title: snap?.title, status: snap?.status };
|
|
826
|
+
}),
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
},
|
|
830
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
831
|
+
const first = result.content[0];
|
|
832
|
+
const content = first?.type === "text" ? first.text : "(no output)";
|
|
833
|
+
if (isPartial) {
|
|
834
|
+
const pending = (result.details as { pending?: string[] } | undefined)
|
|
835
|
+
?.pending?.length;
|
|
836
|
+
return new Text(
|
|
837
|
+
theme.fg(
|
|
838
|
+
"warning",
|
|
839
|
+
pending
|
|
840
|
+
? `\u273b Waiting for ${pending} subagent${pending === 1 ? "" : "s"} to finish`
|
|
841
|
+
: content,
|
|
842
|
+
),
|
|
843
|
+
0,
|
|
844
|
+
0,
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
return renderWaitResult(
|
|
848
|
+
content,
|
|
849
|
+
result.details as WaitResultDetails | undefined,
|
|
850
|
+
expanded || loadSetupConfig().ui.subagentResultDisplay === "full",
|
|
851
|
+
theme,
|
|
852
|
+
);
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
pi.registerTool({
|
|
857
|
+
name: "subagent_cancel",
|
|
858
|
+
label: "Cancel Subagents",
|
|
859
|
+
description: SUBAGENT_CANCEL_TOOL_DESCRIPTION,
|
|
860
|
+
parameters: Type.Object({
|
|
861
|
+
ids: Type.Array(Type.String(), {
|
|
862
|
+
description: SUBAGENT_CANCEL_PARAMETER_DESCRIPTIONS.ids,
|
|
863
|
+
}),
|
|
864
|
+
}),
|
|
865
|
+
async execute(_toolCallId, params, signal) {
|
|
866
|
+
const manager = await getManager();
|
|
867
|
+
const ids = [...new Set(params.ids)];
|
|
868
|
+
if (ids.length === 0)
|
|
869
|
+
throw new Error("Provide at least one subagent id.");
|
|
870
|
+
|
|
871
|
+
const known = manager.view
|
|
872
|
+
.list()
|
|
873
|
+
.filter(isModelVisible)
|
|
874
|
+
.map((snap) => snap.id);
|
|
875
|
+
const unknown = ids.filter((id) => {
|
|
876
|
+
const snap = manager.view.get(id);
|
|
877
|
+
return !snap || !isModelVisible(snap);
|
|
878
|
+
});
|
|
879
|
+
if (unknown.length > 0) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`Unknown subagent id(s): ${unknown.join(", ")}. Known: ${known.join(", ") || "none"}.`,
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const report = await runTool(getRuntime(), manager.cancel(ids), {
|
|
886
|
+
signal,
|
|
887
|
+
interruptMessage: "Subagent cancellation aborted.",
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
const lines = report.map((entry) =>
|
|
891
|
+
entry.cancelled
|
|
892
|
+
? `Cancelled ${entry.id} "${entry.title}".`
|
|
893
|
+
: `${entry.id} "${entry.title}" was already ${entry.status}.`,
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
898
|
+
details: {
|
|
899
|
+
results: report.map((entry) => ({
|
|
900
|
+
id: entry.id,
|
|
901
|
+
title: entry.title,
|
|
902
|
+
status: entry.status,
|
|
903
|
+
})),
|
|
904
|
+
},
|
|
905
|
+
};
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
pi.registerTool({
|
|
910
|
+
name: "subagent_send",
|
|
911
|
+
label: "Send to Subagent",
|
|
912
|
+
description: SUBAGENT_SEND_TOOL_DESCRIPTION,
|
|
913
|
+
parameters: Type.Object({
|
|
914
|
+
id: Type.String({
|
|
915
|
+
description: SUBAGENT_SEND_PARAMETER_DESCRIPTIONS.id,
|
|
916
|
+
}),
|
|
917
|
+
text: Type.String({
|
|
918
|
+
description: SUBAGENT_SEND_PARAMETER_DESCRIPTIONS.text,
|
|
919
|
+
}),
|
|
920
|
+
}),
|
|
921
|
+
async execute(_toolCallId, params, signal) {
|
|
922
|
+
const manager = await getManager();
|
|
923
|
+
const snap = manager.view.get(params.id);
|
|
924
|
+
if (!snap || !isModelVisible(snap)) {
|
|
925
|
+
const known = manager.view
|
|
926
|
+
.list()
|
|
927
|
+
.filter(isModelVisible)
|
|
928
|
+
.map((s) => s.id);
|
|
929
|
+
throw new Error(
|
|
930
|
+
`Unknown subagent id "${params.id}". Known: ${known.join(", ") || "none"}.`,
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
const text = params.text.trim();
|
|
934
|
+
if (!text) throw new Error("Provide a non-empty message.");
|
|
935
|
+
|
|
936
|
+
// Captured before the send: a settled subagent restarts, a running one
|
|
937
|
+
// is steered, and the result message must say which happened.
|
|
938
|
+
const wasRunning = snap.status === "running";
|
|
939
|
+
await runTool(getRuntime(), manager.send(params.id, text), {
|
|
940
|
+
signal,
|
|
941
|
+
interruptMessage: "Subagent send aborted.",
|
|
942
|
+
});
|
|
943
|
+
// A settled subagent may already have an undelivered result buffered;
|
|
944
|
+
// the restart supersedes it, so drop it and let the new run deliver.
|
|
945
|
+
resultDelivery.consume([params.id]);
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
content: [
|
|
949
|
+
{
|
|
950
|
+
type: "text",
|
|
951
|
+
text: buildSubagentSendResult({
|
|
952
|
+
id: snap.id,
|
|
953
|
+
title: snap.title,
|
|
954
|
+
wasRunning,
|
|
955
|
+
}),
|
|
956
|
+
},
|
|
957
|
+
],
|
|
958
|
+
details: {
|
|
959
|
+
id: snap.id,
|
|
960
|
+
title: snap.title,
|
|
961
|
+
status: manager.view.get(params.id)?.status ?? snap.status,
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
pi.registerTool({
|
|
968
|
+
name: "subagent_check",
|
|
969
|
+
label: "Check Subagent",
|
|
970
|
+
description: SUBAGENT_CHECK_TOOL_DESCRIPTION,
|
|
971
|
+
parameters: Type.Object({
|
|
972
|
+
id: Type.String({
|
|
973
|
+
description: SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS.id,
|
|
974
|
+
}),
|
|
975
|
+
}),
|
|
976
|
+
async execute(_toolCallId, params) {
|
|
977
|
+
const manager = await getManager();
|
|
978
|
+
const snap = manager.view.get(params.id);
|
|
979
|
+
if (!snap || !isModelVisible(snap)) {
|
|
980
|
+
const known = manager.view
|
|
981
|
+
.list()
|
|
982
|
+
.filter(isModelVisible)
|
|
983
|
+
.map((s) => s.id);
|
|
984
|
+
throw new Error(
|
|
985
|
+
`Unknown subagent id "${params.id}". Known: ${known.join(", ") || "none"}.`,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
let text = `${describeSubagent(snap)}\nTurns: ${snap.turns}`;
|
|
990
|
+
if (snap.errorText) text += `\nError: ${snap.errorText}`;
|
|
991
|
+
|
|
992
|
+
const output = latestText(snap);
|
|
993
|
+
if (output) {
|
|
994
|
+
const preview = truncateHead(output, { maxBytes: 2048, maxLines: 20 });
|
|
995
|
+
text += `\n\nLatest output:\n${preview.content}`;
|
|
996
|
+
if (preview.truncated) text += "\n[...]";
|
|
997
|
+
} else if (snap.status === "running") {
|
|
998
|
+
text += "\n\n(no text output yet)";
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return {
|
|
1002
|
+
content: [{ type: "text", text }],
|
|
1003
|
+
details: { id: snap.id, status: snap.status, turns: snap.turns },
|
|
1004
|
+
};
|
|
1005
|
+
},
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
pi.registerTool({
|
|
1009
|
+
name: "subagent_list",
|
|
1010
|
+
label: "List Subagents",
|
|
1011
|
+
description: SUBAGENT_LIST_TOOL_DESCRIPTION,
|
|
1012
|
+
parameters: Type.Object({}),
|
|
1013
|
+
async execute() {
|
|
1014
|
+
const manager = await getManager();
|
|
1015
|
+
const subs = manager.view.list().filter(isModelVisible);
|
|
1016
|
+
const text =
|
|
1017
|
+
subs.length === 0
|
|
1018
|
+
? "No subagents."
|
|
1019
|
+
: subs.map((snap) => describeSubagent(snap)).join("\n");
|
|
1020
|
+
return {
|
|
1021
|
+
content: [{ type: "text", text }],
|
|
1022
|
+
details: {
|
|
1023
|
+
subagents: subs.map((snap) => ({
|
|
1024
|
+
id: snap.id,
|
|
1025
|
+
title: snap.title,
|
|
1026
|
+
harness: snap.backend,
|
|
1027
|
+
status: snap.status,
|
|
1028
|
+
})),
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
1031
|
+
},
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// --- Result message rendering ------------------------------------------
|
|
1035
|
+
|
|
1036
|
+
pi.registerMessageRenderer(
|
|
1037
|
+
"subagent-result",
|
|
1038
|
+
(message, { expanded }, theme) => {
|
|
1039
|
+
const details = (message.details ?? {}) as {
|
|
1040
|
+
id?: string;
|
|
1041
|
+
title?: string;
|
|
1042
|
+
status?: string;
|
|
1043
|
+
};
|
|
1044
|
+
const failed = details.status === "error";
|
|
1045
|
+
const icon = failed ? theme.fg("error", "x") : theme.fg("success", "■");
|
|
1046
|
+
const header =
|
|
1047
|
+
`${icon} ` +
|
|
1048
|
+
theme.fg("accent", theme.bold(`subagent ${details.id ?? "?"}`)) +
|
|
1049
|
+
theme.fg(
|
|
1050
|
+
"muted",
|
|
1051
|
+
` · ${details.title ?? ""} · ${failed ? "failed" : "finished"}`,
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
const content =
|
|
1055
|
+
typeof message.content === "string" ? message.content : "";
|
|
1056
|
+
// Remove only the summary line. The following Error line (when present)
|
|
1057
|
+
// is part of the actual result and must remain visible.
|
|
1058
|
+
const body = content.split("\n").slice(1).join("\n").trim();
|
|
1059
|
+
|
|
1060
|
+
if (expanded || loadSetupConfig().ui.subagentResultDisplay === "full") {
|
|
1061
|
+
const md = new Markdown(`${body}`, 0, 0, getMarkdownTheme());
|
|
1062
|
+
const container = new Text(header, 0, 0);
|
|
1063
|
+
return {
|
|
1064
|
+
render: (width: number) => [
|
|
1065
|
+
...container.render(width),
|
|
1066
|
+
...md.render(width),
|
|
1067
|
+
],
|
|
1068
|
+
invalidate: () => {
|
|
1069
|
+
container.invalidate();
|
|
1070
|
+
md.invalidate();
|
|
1071
|
+
},
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const previewLines = body.split("\n").slice(0, 8);
|
|
1076
|
+
let text = header;
|
|
1077
|
+
for (const line of previewLines)
|
|
1078
|
+
text += `\n${theme.fg("toolOutput", line)}`;
|
|
1079
|
+
if (body.split("\n").length > 8)
|
|
1080
|
+
text += `\n${theme.fg("dim", `... (${keyHint("app.tools.expand", "to expand")})`)}`;
|
|
1081
|
+
return new Text(text, 0, 0);
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
pi.registerEntryRenderer<SubagentFinishedData>(
|
|
1086
|
+
"subagent-finished",
|
|
1087
|
+
(entry, _options, theme) => {
|
|
1088
|
+
const data = entry.data;
|
|
1089
|
+
const failed = data?.status === "error";
|
|
1090
|
+
return new Text(
|
|
1091
|
+
`${theme.fg(failed ? "error" : "success", "\u25cf")} ` +
|
|
1092
|
+
theme.fg(
|
|
1093
|
+
"muted",
|
|
1094
|
+
`Agent "${data?.title ?? "?"}" ${failed ? "failed" : "finished"} \u00b7 ${data?.elapsed ?? "?"}`,
|
|
1095
|
+
),
|
|
1096
|
+
1,
|
|
1097
|
+
0,
|
|
1098
|
+
);
|
|
1099
|
+
},
|
|
1100
|
+
);
|
|
1101
|
+
|
|
1102
|
+
pi.registerEntryRenderer<BtwResultData>(
|
|
1103
|
+
"btw-result",
|
|
1104
|
+
(entry, { expanded }, theme) => {
|
|
1105
|
+
const data = entry.data;
|
|
1106
|
+
const failed = data?.status === "error";
|
|
1107
|
+
const icon = failed ? theme.fg("error", "x") : theme.fg("success", "■");
|
|
1108
|
+
const header =
|
|
1109
|
+
`${icon} ` +
|
|
1110
|
+
theme.fg("accent", theme.bold(`by the way · ${data?.title ?? "?"}`)) +
|
|
1111
|
+
theme.fg(
|
|
1112
|
+
"muted",
|
|
1113
|
+
` · ${failed ? "failed" : "answered"} · ${data?.id ?? "?"}`,
|
|
1114
|
+
);
|
|
1115
|
+
const body = [
|
|
1116
|
+
data?.errorText ? `Error: ${data.errorText}` : "",
|
|
1117
|
+
data?.answer ?? "(no answer)",
|
|
1118
|
+
]
|
|
1119
|
+
.filter(Boolean)
|
|
1120
|
+
.join("\n\n");
|
|
1121
|
+
|
|
1122
|
+
if (expanded || loadSetupConfig().ui.subagentResultDisplay === "full") {
|
|
1123
|
+
const md = new Markdown(body, 0, 0, getMarkdownTheme());
|
|
1124
|
+
const container = new Text(header, 0, 0);
|
|
1125
|
+
return {
|
|
1126
|
+
render: (width: number) => [
|
|
1127
|
+
...container.render(width),
|
|
1128
|
+
...md.render(width),
|
|
1129
|
+
],
|
|
1130
|
+
invalidate: () => {
|
|
1131
|
+
container.invalidate();
|
|
1132
|
+
md.invalidate();
|
|
1133
|
+
},
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const lines = body.split("\n");
|
|
1138
|
+
let text = header;
|
|
1139
|
+
for (const line of lines.slice(0, 8))
|
|
1140
|
+
text += `\n${theme.fg("toolOutput", line)}`;
|
|
1141
|
+
if (lines.length > 8)
|
|
1142
|
+
text += `\n${theme.fg("dim", `... (${keyHint("app.tools.expand", "to expand")})`)}`;
|
|
1143
|
+
return new Text(text, 0, 0);
|
|
1144
|
+
},
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
// --- Commands -----------------------------------------------------------
|
|
1148
|
+
|
|
1149
|
+
const runByTheWay = async (rawArgs: string, ctx: ExtensionCommandContext) => {
|
|
1150
|
+
if (ctx.mode !== "tui") {
|
|
1151
|
+
if (ctx.hasUI)
|
|
1152
|
+
ctx.ui.notify("by the way is only available in the TUI", "error");
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
let prompt = rawArgs.trim();
|
|
1157
|
+
if (!prompt) {
|
|
1158
|
+
const input = await ctx.ui.input("by the way", "Ask a one-off question…");
|
|
1159
|
+
prompt = input?.trim() ?? "";
|
|
1160
|
+
if (!prompt) return;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const manager = await getManager();
|
|
1164
|
+
let snap: SubagentSnapshot;
|
|
1165
|
+
try {
|
|
1166
|
+
snap = await runTool(
|
|
1167
|
+
getRuntime(),
|
|
1168
|
+
manager.spawn("pi", {
|
|
1169
|
+
origin: "btw",
|
|
1170
|
+
prompt,
|
|
1171
|
+
title: normalizeSubagentTitle(deriveBtwTitle(prompt), "by the way"),
|
|
1172
|
+
cwd: ctx.cwd,
|
|
1173
|
+
parent: {
|
|
1174
|
+
parentCwd: ctx.cwd,
|
|
1175
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
1176
|
+
inheritedModel: ctx.model
|
|
1177
|
+
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
1178
|
+
: undefined,
|
|
1179
|
+
inheritedThinkingLevel: pi.getThinkingLevel(),
|
|
1180
|
+
modelRegistry: ctx.modelRegistry,
|
|
1181
|
+
},
|
|
1182
|
+
}),
|
|
1183
|
+
);
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
ctx.ui.notify(
|
|
1186
|
+
error instanceof Error ? error.message : String(error),
|
|
1187
|
+
"error",
|
|
1188
|
+
);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
await openSubagentTakeover(ctx, manager.view, snap.id, {
|
|
1193
|
+
badge: "by the way",
|
|
1194
|
+
});
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
pi.registerCommand("btw", {
|
|
1198
|
+
description:
|
|
1199
|
+
"Ask a one-off side question while the main agent keeps working",
|
|
1200
|
+
handler: runByTheWay,
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
pi.registerCommand("subagents", {
|
|
1204
|
+
description: "List, inspect, and take over subagents",
|
|
1205
|
+
handler: async (_args, ctx) => {
|
|
1206
|
+
if (ctx.mode !== "tui") {
|
|
1207
|
+
if (ctx.hasUI)
|
|
1208
|
+
ctx.ui.notify(
|
|
1209
|
+
"Subagent takeover is only available in the TUI",
|
|
1210
|
+
"error",
|
|
1211
|
+
);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const manager = await getManager();
|
|
1215
|
+
if (manager.view.size() === 0) {
|
|
1216
|
+
ctx.ui.notify(
|
|
1217
|
+
"No subagents yet. The agent spawns them with subagent_spawn.",
|
|
1218
|
+
"info",
|
|
1219
|
+
);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
await openDashboard(ctx);
|
|
1223
|
+
},
|
|
1224
|
+
});
|
|
1225
|
+
}
|