pi-subagents 0.21.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +14 -2
- package/agent-management.ts +0 -4
- package/agent-manager-list.ts +10 -3
- package/agent-manager.ts +10 -7
- package/execution.ts +12 -4
- package/index.ts +3 -3
- package/intercom-bridge.ts +17 -8
- package/package.json +1 -1
- package/result-watcher.ts +90 -20
- package/skills/pi-subagents/SKILL.md +4 -2
- package/slash-commands.ts +6 -3
- package/subagent-runner.ts +13 -5
- package/types.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.21.1] - 2026-04-30
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Changed the `/agents` new-agent shortcut from `Alt+N` to `Shift+Ctrl+N`, and added `agentManager.newShortcut` config for overriding it.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Fall back to polling async result files when native result watching is unavailable due to `EMFILE` or `ENOSPC`.
|
|
12
|
+
- Treat forced final-drain termination after a valid final assistant output as cleanup success instead of failing the subagent run.
|
|
13
|
+
- Hide disabled builtin agents from `subagent({ action: "list" })` output so agent-facing choices match executable runtime discovery.
|
|
14
|
+
- Resolve intercom bridge default paths at runtime so tests and isolated environments that change `HOME` use the correct `pi-intercom` location.
|
|
15
|
+
- Made the tool-description source check tolerant of Windows line endings.
|
|
16
|
+
|
|
5
17
|
## [0.21.0] - 2026-04-29
|
|
6
18
|
|
|
7
19
|
### Changed
|
package/README.md
CHANGED
|
@@ -374,7 +374,7 @@ Useful keys:
|
|
|
374
374
|
|
|
375
375
|
- type to search the list
|
|
376
376
|
- `Enter` opens detail screens
|
|
377
|
-
- `
|
|
377
|
+
- `Shift+Ctrl+N` creates an agent or chain from a template
|
|
378
378
|
- `Ctrl+R` launches selected agents as a run or chain
|
|
379
379
|
- `Ctrl+P` opens the parallel builder
|
|
380
380
|
- `Tab` selects agents in the list or toggles skip-clarify in task input
|
|
@@ -432,7 +432,7 @@ Supported override fields are `model`, `fallbackModels`, `thinking`, `systemProm
|
|
|
432
432
|
|
|
433
433
|
You can also manage builtin overrides from `/agents`. On a builtin detail screen, press `e`, choose user or project scope if needed, and save the fields you want to override.
|
|
434
434
|
|
|
435
|
-
Set `disabled: true` to hide a builtin from runtime discovery while keeping it visible in `/agents`. For bulk control, set `subagents.disableBuiltins: true` in settings. Overridden builtins show badges like `[builtin+user]` or `[builtin+project]`; disabled builtins show `off` badges in the manager.
|
|
435
|
+
Set `disabled: true` to hide a builtin from runtime discovery and agent-facing `subagent({ action: "list" })` output while keeping it visible in `/agents`. For bulk control, set `subagents.disableBuiltins: true` in settings. Overridden builtins show badges like `[builtin+user]` or `[builtin+project]`; disabled builtins show `off` badges in the manager.
|
|
436
436
|
|
|
437
437
|
### Prompt assembly
|
|
438
438
|
|
|
@@ -833,6 +833,18 @@ Session directory precedence is: `params.sessionDir`, then `config.defaultSessio
|
|
|
833
833
|
|
|
834
834
|
Controls nested delegation when no inherited `PI_SUBAGENT_MAX_DEPTH` is already in effect. Per-agent `maxSubagentDepth` can tighten the limit for that agent’s child runs, but cannot relax an inherited stricter limit.
|
|
835
835
|
|
|
836
|
+
### `agentManager.newShortcut`
|
|
837
|
+
|
|
838
|
+
```json
|
|
839
|
+
{
|
|
840
|
+
"agentManager": {
|
|
841
|
+
"newShortcut": "shift+ctrl+n"
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
Sets the `/agents` list shortcut for opening the new agent/chain template picker. The default is `shift+ctrl+n`; use Pi key names such as `ctrl+n` if your terminal cannot distinguish Shift for control chords.
|
|
847
|
+
|
|
836
848
|
### `intercomBridge`
|
|
837
849
|
|
|
838
850
|
```json
|
package/agent-management.ts
CHANGED
|
@@ -378,16 +378,12 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
378
378
|
const d = discoverAgentsAll(ctx.cwd);
|
|
379
379
|
const scopedAgents = allAgents(d).filter((a) => scope === "both" || a.source === "builtin" || a.source === scope).sort((a, b) => a.name.localeCompare(b.name));
|
|
380
380
|
const agents = scopedAgents.filter((a) => !a.disabled);
|
|
381
|
-
const disabledBuiltins = scopedAgents.filter((a) => a.source === "builtin" && a.disabled);
|
|
382
381
|
const chains = d.chains.filter((c) => scope === "both" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
|
|
383
382
|
const lines = [
|
|
384
383
|
"Executable agents:",
|
|
385
384
|
...(agents.length
|
|
386
385
|
? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}): ${a.description}`)
|
|
387
386
|
: ["- (none)"]),
|
|
388
|
-
...(disabledBuiltins.length
|
|
389
|
-
? ["", "Disabled builtins:", ...disabledBuiltins.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}, disabled): ${a.description}`)]
|
|
390
|
-
: []),
|
|
391
387
|
"",
|
|
392
388
|
"Chains:",
|
|
393
389
|
...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
|
package/agent-manager-list.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface ListState {
|
|
|
22
22
|
selected: string[];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface ListShortcuts {
|
|
26
|
+
newShortcut: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_AGENT_MANAGER_NEW_SHORTCUT = "shift+ctrl+n";
|
|
30
|
+
|
|
25
31
|
export type ListAction =
|
|
26
32
|
| { type: "open-detail"; id: string }
|
|
27
33
|
| { type: "clone"; id: string }
|
|
@@ -57,7 +63,7 @@ function clampCursor(state: ListState, filtered: ListAgent[]): void {
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
export function handleListInput(state: ListState, agents: ListAgent[], data: string): ListAction | undefined {
|
|
66
|
+
export function handleListInput(state: ListState, agents: ListAgent[], data: string, shortcuts: ListShortcuts = { newShortcut: DEFAULT_AGENT_MANAGER_NEW_SHORTCUT }): ListAction | undefined {
|
|
61
67
|
const filtered = fuzzyFilter(agents, state.filterQuery);
|
|
62
68
|
|
|
63
69
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
@@ -98,7 +104,7 @@ export function handleListInput(state: ListState, agents: ListAgent[], data: str
|
|
|
98
104
|
return;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
if (matchesKey(data,
|
|
107
|
+
if (matchesKey(data, shortcuts.newShortcut)) {
|
|
102
108
|
return { type: "new" };
|
|
103
109
|
}
|
|
104
110
|
|
|
@@ -160,6 +166,7 @@ export function renderList(
|
|
|
160
166
|
width: number,
|
|
161
167
|
theme: Theme,
|
|
162
168
|
statusMessage?: { text: string; type: "error" | "info" },
|
|
169
|
+
shortcuts: ListShortcuts = { newShortcut: DEFAULT_AGENT_MANAGER_NEW_SHORTCUT },
|
|
163
170
|
): string[] {
|
|
164
171
|
const lines: string[] = [];
|
|
165
172
|
const filtered = fuzzyFilter(agents, state.filterQuery);
|
|
@@ -269,7 +276,7 @@ export function renderList(
|
|
|
269
276
|
? ` [ctrl+r] chain [ctrl+p] parallel [tab] add [shift+tab] remove [esc] clear (${selCount}) `
|
|
270
277
|
: selCount === 1
|
|
271
278
|
? " [ctrl+r] run [ctrl+p] parallel [tab] add more [shift+tab] remove [esc] clear "
|
|
272
|
-
:
|
|
279
|
+
: ` [enter] view [ctrl+r] run [tab] select [${shortcuts.newShortcut}] new [esc] close `;
|
|
273
280
|
lines.push(renderFooter(footerText, width, theme));
|
|
274
281
|
|
|
275
282
|
return lines;
|
package/agent-manager.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { serializeAgent } from "./agent-serializer.ts";
|
|
19
19
|
import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "./agent-templates.ts";
|
|
20
20
|
import { parseChain, serializeChain } from "./chain-serializer.ts";
|
|
21
|
-
import { renderList, handleListInput, type ListAgent, type ListState, type ListAction } from "./agent-manager-list.ts";
|
|
21
|
+
import { DEFAULT_AGENT_MANAGER_NEW_SHORTCUT, renderList, handleListInput, type ListAgent, type ListShortcuts, type ListState, type ListAction } from "./agent-manager-list.ts";
|
|
22
22
|
import { createParallelState, handleParallelInput, renderParallel, formatParallelTitle, type ParallelState, type AgentOption } from "./agent-manager-parallel.ts";
|
|
23
23
|
import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction, type LaunchToggleState } from "./agent-manager-detail.ts";
|
|
24
24
|
import { renderChainDetail, handleChainDetailInput, type ChainDetailAction, type ChainDetailState } from "./agent-manager-chain-detail.ts";
|
|
@@ -43,6 +43,7 @@ interface ChainEntry { id: string; kind: "chain"; config: ChainConfig; }
|
|
|
43
43
|
interface NameInputState { mode: "new-agent" | "clone-agent" | "clone-chain" | "new-chain"; editor: TextEditorState; scope: "user" | "project"; allowProject: boolean; sourceId?: string; template?: AgentTemplate; error?: string; }
|
|
44
44
|
interface StatusMessage { text: string; type: "error" | "info"; }
|
|
45
45
|
interface OverrideScopeState { selectedScope: "user" | "project"; allowProject: boolean; }
|
|
46
|
+
export interface AgentManagerOptions { newShortcut?: string; }
|
|
46
47
|
|
|
47
48
|
const BUILTIN_OVERRIDE_FIELDS: EditField[] = ["model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "defaultContext", "disabled", "tools", "skills", "prompt"];
|
|
48
49
|
|
|
@@ -131,14 +132,16 @@ export class AgentManagerComponent implements Component {
|
|
|
131
132
|
private models: ModelInfo[];
|
|
132
133
|
private skills: SkillInfo[];
|
|
133
134
|
private done: (result: ManagerResult) => void;
|
|
135
|
+
private shortcuts: ListShortcuts;
|
|
134
136
|
|
|
135
|
-
constructor(tui: TUI, theme: Theme, agentData: AgentData, models: ModelInfo[], skills: SkillInfo[], done: (result: ManagerResult) => void) {
|
|
137
|
+
constructor(tui: TUI, theme: Theme, agentData: AgentData, models: ModelInfo[], skills: SkillInfo[], done: (result: ManagerResult) => void, options: AgentManagerOptions = {}) {
|
|
136
138
|
this.tui = tui;
|
|
137
139
|
this.theme = theme;
|
|
138
140
|
this.agentData = agentData;
|
|
139
141
|
this.models = models;
|
|
140
142
|
this.skills = skills;
|
|
141
143
|
this.done = done;
|
|
144
|
+
this.shortcuts = { newShortcut: options.newShortcut?.trim() || DEFAULT_AGENT_MANAGER_NEW_SHORTCUT };
|
|
142
145
|
this.loadEntries();
|
|
143
146
|
}
|
|
144
147
|
|
|
@@ -518,7 +521,7 @@ export class AgentManagerComponent implements Component {
|
|
|
518
521
|
if (this.screen === "list" && this.statusMessage) this.clearStatus();
|
|
519
522
|
if (this.screen.startsWith("edit") && this.editState?.error) this.editState.error = undefined;
|
|
520
523
|
switch (this.screen) {
|
|
521
|
-
case "list": { const action = handleListInput(this.listState, this.listAgents(), data); if (action) this.handleListAction(action); this.tui.requestRender(); return; }
|
|
524
|
+
case "list": { const action = handleListInput(this.listState, this.listAgents(), data, this.shortcuts); if (action) this.handleListAction(action); this.tui.requestRender(); return; }
|
|
522
525
|
case "template-select": this.handleTemplateSelectInput(data); return;
|
|
523
526
|
case "override-scope": this.handleOverrideScopeInput(data); return;
|
|
524
527
|
case "detail": {
|
|
@@ -671,14 +674,14 @@ export class AgentManagerComponent implements Component {
|
|
|
671
674
|
render(width: number): string[] {
|
|
672
675
|
this.overlayWidth = width; const w = this.overlayWidth;
|
|
673
676
|
switch (this.screen) {
|
|
674
|
-
case "list": return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage);
|
|
677
|
+
case "list": return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage, this.shortcuts);
|
|
675
678
|
case "template-select": return this.renderTemplateSelect(w);
|
|
676
679
|
case "override-scope": return this.renderOverrideScope(w);
|
|
677
|
-
case "detail": { const entry = this.getAgentEntry(this.currentAgentId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage); return renderDetail(this.detailState, entry.config, this.agentData.cwd, w, this.theme); }
|
|
678
|
-
case "chain-detail": { const entry = this.getChainEntry(this.currentChainId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage); return renderChainDetail(this.chainDetailState, entry.config, w, this.theme); }
|
|
680
|
+
case "detail": { const entry = this.getAgentEntry(this.currentAgentId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage, this.shortcuts); return renderDetail(this.detailState, entry.config, this.agentData.cwd, w, this.theme); }
|
|
681
|
+
case "chain-detail": { const entry = this.getChainEntry(this.currentChainId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage, this.shortcuts); return renderChainDetail(this.chainDetailState, entry.config, w, this.theme); }
|
|
679
682
|
case "edit": case "edit-field": case "edit-prompt": return this.editState ? renderEdit(this.screen as EditScreen, this.editState, w, this.theme) : [];
|
|
680
683
|
case "parallel-builder": {
|
|
681
|
-
if (!this.parallelState) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage);
|
|
684
|
+
if (!this.parallelState) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage, this.shortcuts);
|
|
682
685
|
const agentOptions: AgentOption[] = this.agents.map((e) => ({ name: e.config.name, description: e.config.description, model: e.config.model }));
|
|
683
686
|
return renderParallel(this.parallelState, agentOptions, w, this.theme);
|
|
684
687
|
}
|
package/execution.ts
CHANGED
|
@@ -232,6 +232,8 @@ async function runSingleAttempt(
|
|
|
232
232
|
const HARD_KILL_MS = 3000;
|
|
233
233
|
let childExited = false;
|
|
234
234
|
let forcedTerminationSignal = false;
|
|
235
|
+
let finalAssistantDelivered = false;
|
|
236
|
+
let finalDrainCleanupWarning: string | undefined;
|
|
235
237
|
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
236
238
|
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
237
239
|
const clearFinalDrainTimers = () => {
|
|
@@ -251,8 +253,8 @@ async function runSingleAttempt(
|
|
|
251
253
|
const termSent = trySignalChild(proc, "SIGTERM");
|
|
252
254
|
if (!termSent) return;
|
|
253
255
|
forcedTerminationSignal = true;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
+
finalDrainCleanupWarning = `Subagent process did not exit within ${FINAL_DRAIN_MS}ms after its final message. Forcing termination.`;
|
|
257
|
+
if (!finalAssistantDelivered) result.error = result.error ?? finalDrainCleanupWarning;
|
|
256
258
|
finalHardKillTimer = setTimeout(() => {
|
|
257
259
|
if (settled || processClosed || detached) return;
|
|
258
260
|
forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
|
|
@@ -462,7 +464,9 @@ async function runSingleAttempt(
|
|
|
462
464
|
const stopReason = (evt.message as { stopReason?: string }).stopReason;
|
|
463
465
|
const hasToolCall = Array.isArray(evt.message.content)
|
|
464
466
|
&& evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
467
|
+
const finalText = extractTextFromContent(evt.message.content).trim();
|
|
465
468
|
if (stopReason === "stop" && !hasToolCall) {
|
|
469
|
+
finalAssistantDelivered ||= !evt.message.errorMessage && finalText.length > 0;
|
|
466
470
|
startFinalDrain();
|
|
467
471
|
}
|
|
468
472
|
}
|
|
@@ -541,10 +545,14 @@ async function runSingleAttempt(
|
|
|
541
545
|
}
|
|
542
546
|
processClosed = true;
|
|
543
547
|
if (buf.trim()) processLine(buf);
|
|
544
|
-
|
|
548
|
+
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && finalAssistantDelivered;
|
|
549
|
+
if (code !== 0 && stderrBuf.trim() && !result.error && !forcedDrainAfterFinalSuccess) {
|
|
545
550
|
result.error = stderrBuf.trim();
|
|
546
551
|
}
|
|
547
|
-
|
|
552
|
+
if (forcedDrainAfterFinalSuccess && finalDrainCleanupWarning) {
|
|
553
|
+
appendRecentOutput(progress, [`Warning: ${finalDrainCleanupWarning}`]);
|
|
554
|
+
}
|
|
555
|
+
const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
|
|
548
556
|
finish(finalCode);
|
|
549
557
|
});
|
|
550
558
|
proc.on("error", (error) => {
|
package/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Toggle: async parameter (default: false, configurable via config.json)
|
|
10
10
|
*
|
|
11
11
|
* Config file: ~/.pi/agent/extensions/subagent/config.json
|
|
12
|
-
* { "asyncByDefault": true, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
12
|
+
* { "asyncByDefault": true, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "agentManager": { "newShortcut": "shift+ctrl+n" }, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as fs from "node:fs";
|
|
@@ -418,7 +418,7 @@ CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
|
418
418
|
Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", task:"Plan based on {previous}"}] }
|
|
419
419
|
|
|
420
420
|
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
421
|
-
• { action: "list" } - discover executable agents/chains
|
|
421
|
+
• { action: "list" } - discover executable agents/chains
|
|
422
422
|
• { action: "get", agent: "name" } - full detail
|
|
423
423
|
• { action: "create", config: { name, systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
|
|
424
424
|
• { action: "update", agent: "name", config: { ... } } - merge
|
|
@@ -475,7 +475,7 @@ DIAGNOSTICS:
|
|
|
475
475
|
};
|
|
476
476
|
|
|
477
477
|
pi.registerTool(tool);
|
|
478
|
-
registerSlashCommands(pi, state);
|
|
478
|
+
registerSlashCommands(pi, state, config);
|
|
479
479
|
|
|
480
480
|
const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
|
|
481
481
|
const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
|
package/intercom-bridge.ts
CHANGED
|
@@ -4,9 +4,18 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { AgentConfig } from "./agents.ts";
|
|
5
5
|
import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "./types.ts";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
function defaultIntercomExtensionDir(): string {
|
|
8
|
+
return path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function defaultIntercomConfigPath(): string {
|
|
12
|
+
return path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function defaultSubagentConfigDir(): string {
|
|
16
|
+
return path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
11
20
|
export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
12
21
|
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the orchestrator in normal assistant text.
|
|
@@ -135,9 +144,9 @@ ${instruction}`;
|
|
|
135
144
|
export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeDiagnostic {
|
|
136
145
|
const config = resolveIntercomBridgeConfig(input.config);
|
|
137
146
|
const mode = config.mode;
|
|
138
|
-
const extensionDir = path.resolve(input.extensionDir ??
|
|
147
|
+
const extensionDir = path.resolve(input.extensionDir ?? defaultIntercomExtensionDir());
|
|
139
148
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
140
|
-
const configPath = path.resolve(input.configPath ??
|
|
149
|
+
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath());
|
|
141
150
|
const wantsIntercom = mode !== "off" && !(mode === "fork-only" && input.context !== "fork");
|
|
142
151
|
const piIntercomAvailable = fs.existsSync(extensionDir);
|
|
143
152
|
let configStatus: ReturnType<typeof intercomConfigStatus> | undefined;
|
|
@@ -173,9 +182,9 @@ export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): Inter
|
|
|
173
182
|
export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
|
|
174
183
|
const config = resolveIntercomBridgeConfig(input.config);
|
|
175
184
|
const mode = config.mode;
|
|
176
|
-
const extensionDir = path.resolve(input.extensionDir ??
|
|
185
|
+
const extensionDir = path.resolve(input.extensionDir ?? defaultIntercomExtensionDir());
|
|
177
186
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
178
|
-
const settingsDir = path.resolve(input.settingsDir ??
|
|
187
|
+
const settingsDir = path.resolve(input.settingsDir ?? defaultSubagentConfigDir());
|
|
179
188
|
const defaultInstruction = buildIntercomBridgeInstruction(
|
|
180
189
|
orchestratorTarget || "{orchestratorTarget}",
|
|
181
190
|
DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
|
|
@@ -194,7 +203,7 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
|
|
|
194
203
|
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
const configPath = path.resolve(input.configPath ??
|
|
206
|
+
const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath());
|
|
198
207
|
const intercomStatus = intercomConfigStatus(configPath);
|
|
199
208
|
if (intercomStatus.error) console.warn(`Failed to parse intercom config at '${configPath}'. Assuming enabled.`, intercomStatus.error);
|
|
200
209
|
if (!intercomStatus.enabled) {
|
package/package.json
CHANGED
package/result-watcher.ts
CHANGED
|
@@ -13,11 +13,36 @@ import {
|
|
|
13
13
|
resolveSubagentResultStatus,
|
|
14
14
|
} from "./result-intercom.ts";
|
|
15
15
|
|
|
16
|
+
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
17
|
+
const POLL_INTERVAL_MS = 3000;
|
|
18
|
+
|
|
19
|
+
type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "watch">;
|
|
20
|
+
|
|
21
|
+
type ResultWatcherTimers = {
|
|
22
|
+
setTimeout: typeof setTimeout;
|
|
23
|
+
clearTimeout: typeof clearTimeout;
|
|
24
|
+
setInterval: typeof setInterval;
|
|
25
|
+
clearInterval: typeof clearInterval;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ResultWatcherDeps = {
|
|
29
|
+
fs?: ResultWatcherFs;
|
|
30
|
+
timers?: ResultWatcherTimers;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function getErrorCode(error: unknown): string | undefined {
|
|
34
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
35
|
+
? (error as NodeJS.ErrnoException).code
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
function isNotFoundError(error: unknown): boolean {
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
return getErrorCode(error) === "ENOENT";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldFallBackToPolling(error: unknown): boolean {
|
|
44
|
+
const code = getErrorCode(error);
|
|
45
|
+
return code === "EMFILE" || code === "ENOSPC";
|
|
21
46
|
}
|
|
22
47
|
|
|
23
48
|
export function createResultWatcher(
|
|
@@ -25,16 +50,20 @@ export function createResultWatcher(
|
|
|
25
50
|
state: SubagentState,
|
|
26
51
|
resultsDir: string,
|
|
27
52
|
completionTtlMs: number,
|
|
53
|
+
deps: ResultWatcherDeps = {},
|
|
28
54
|
): {
|
|
29
55
|
startResultWatcher: () => void;
|
|
30
56
|
primeExistingResults: () => void;
|
|
31
57
|
stopResultWatcher: () => void;
|
|
32
58
|
} {
|
|
59
|
+
const fsApi = deps.fs ?? fs;
|
|
60
|
+
const timers = deps.timers ?? { setTimeout, clearTimeout, setInterval, clearInterval };
|
|
61
|
+
|
|
33
62
|
const handleResult = async (file: string) => {
|
|
34
63
|
const resultPath = path.join(resultsDir, file);
|
|
35
|
-
if (!
|
|
64
|
+
if (!fsApi.existsSync(resultPath)) return;
|
|
36
65
|
try {
|
|
37
|
-
const data = JSON.parse(
|
|
66
|
+
const data = JSON.parse(fsApi.readFileSync(resultPath, "utf-8")) as {
|
|
38
67
|
id?: string;
|
|
39
68
|
runId?: string;
|
|
40
69
|
agent?: string;
|
|
@@ -62,7 +91,7 @@ export function createResultWatcher(
|
|
|
62
91
|
const now = Date.now();
|
|
63
92
|
const completionKey = buildCompletionKey(data, `result:${file}`);
|
|
64
93
|
if (markSeenWithTtl(state.completionSeen, completionKey, now, completionTtlMs)) {
|
|
65
|
-
|
|
94
|
+
fsApi.unlinkSync(resultPath);
|
|
66
95
|
return;
|
|
67
96
|
}
|
|
68
97
|
|
|
@@ -114,7 +143,7 @@ export function createResultWatcher(
|
|
|
114
143
|
}
|
|
115
144
|
|
|
116
145
|
pi.events.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, data);
|
|
117
|
-
|
|
146
|
+
fsApi.unlinkSync(resultPath);
|
|
118
147
|
} catch (error) {
|
|
119
148
|
if (isNotFoundError(error)) return;
|
|
120
149
|
console.error(`Failed to process subagent result file '${resultPath}':`, error);
|
|
@@ -125,50 +154,91 @@ export function createResultWatcher(
|
|
|
125
154
|
void handleResult(file);
|
|
126
155
|
}, 50);
|
|
127
156
|
|
|
157
|
+
const primeExistingResults = () => {
|
|
158
|
+
try {
|
|
159
|
+
fsApi.readdirSync(resultsDir)
|
|
160
|
+
.filter((f) => f.endsWith(".json"))
|
|
161
|
+
.forEach((file) => state.resultFileCoalescer.schedule(file, 0));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (isNotFoundError(error)) return;
|
|
164
|
+
console.error(`Failed to scan subagent result directory '${resultsDir}':`, error);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const startPollingFallback = (reason: unknown) => {
|
|
169
|
+
state.watcher?.close();
|
|
170
|
+
state.watcher = null;
|
|
171
|
+
if (state.watcherRestartTimer) return;
|
|
172
|
+
|
|
173
|
+
console.error(
|
|
174
|
+
`Subagent result watcher for '${resultsDir}' fell back to polling because native fs.watch is unavailable (${getErrorCode(reason) ?? "unknown error"}).`,
|
|
175
|
+
);
|
|
176
|
+
primeExistingResults();
|
|
177
|
+
state.watcherRestartTimer = timers.setInterval(primeExistingResults, POLL_INTERVAL_MS);
|
|
178
|
+
state.watcherRestartTimer.unref?.();
|
|
179
|
+
};
|
|
180
|
+
|
|
128
181
|
const scheduleRestart = () => {
|
|
129
|
-
state.watcherRestartTimer
|
|
182
|
+
if (state.watcherRestartTimer) return;
|
|
183
|
+
state.watcherRestartTimer = timers.setTimeout(() => {
|
|
184
|
+
state.watcherRestartTimer = null;
|
|
130
185
|
try {
|
|
131
|
-
|
|
186
|
+
fsApi.mkdirSync(resultsDir, { recursive: true });
|
|
132
187
|
startResultWatcher();
|
|
133
188
|
} catch (error) {
|
|
189
|
+
if (shouldFallBackToPolling(error)) {
|
|
190
|
+
startPollingFallback(error);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
134
193
|
console.error(`Failed to restart subagent result watcher for '${resultsDir}':`, error);
|
|
194
|
+
scheduleRestart();
|
|
135
195
|
}
|
|
136
|
-
},
|
|
196
|
+
}, WATCHER_RESTART_DELAY_MS);
|
|
197
|
+
state.watcherRestartTimer.unref?.();
|
|
137
198
|
};
|
|
138
199
|
|
|
139
200
|
const startResultWatcher = () => {
|
|
140
|
-
state.
|
|
201
|
+
if (state.watcher) return;
|
|
202
|
+
if (state.watcherRestartTimer) {
|
|
203
|
+
timers.clearTimeout(state.watcherRestartTimer);
|
|
204
|
+
timers.clearInterval(state.watcherRestartTimer);
|
|
205
|
+
state.watcherRestartTimer = null;
|
|
206
|
+
}
|
|
141
207
|
try {
|
|
142
|
-
state.watcher =
|
|
208
|
+
state.watcher = fsApi.watch(resultsDir, (ev, file) => {
|
|
143
209
|
if (ev !== "rename" || !file) return;
|
|
144
210
|
const fileName = file.toString();
|
|
145
211
|
if (!fileName.endsWith(".json")) return;
|
|
146
212
|
state.resultFileCoalescer.schedule(fileName);
|
|
147
213
|
});
|
|
148
214
|
state.watcher.on("error", (error) => {
|
|
215
|
+
if (shouldFallBackToPolling(error)) {
|
|
216
|
+
startPollingFallback(error);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
149
219
|
console.error(`Subagent result watcher failed for '${resultsDir}':`, error);
|
|
220
|
+
state.watcher?.close();
|
|
150
221
|
state.watcher = null;
|
|
151
222
|
scheduleRestart();
|
|
152
223
|
});
|
|
153
224
|
state.watcher.unref?.();
|
|
154
225
|
} catch (error) {
|
|
226
|
+
if (shouldFallBackToPolling(error)) {
|
|
227
|
+
startPollingFallback(error);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
155
230
|
console.error(`Failed to start subagent result watcher for '${resultsDir}':`, error);
|
|
156
231
|
state.watcher = null;
|
|
157
232
|
scheduleRestart();
|
|
158
233
|
}
|
|
159
234
|
};
|
|
160
235
|
|
|
161
|
-
const primeExistingResults = () => {
|
|
162
|
-
fs.readdirSync(resultsDir)
|
|
163
|
-
.filter((f) => f.endsWith(".json"))
|
|
164
|
-
.forEach((file) => state.resultFileCoalescer.schedule(file, 0));
|
|
165
|
-
};
|
|
166
|
-
|
|
167
236
|
const stopResultWatcher = () => {
|
|
168
237
|
state.watcher?.close();
|
|
169
238
|
state.watcher = null;
|
|
170
239
|
if (state.watcherRestartTimer) {
|
|
171
|
-
clearTimeout(state.watcherRestartTimer);
|
|
240
|
+
timers.clearTimeout(state.watcherRestartTimer);
|
|
241
|
+
timers.clearInterval(state.watcherRestartTimer);
|
|
172
242
|
}
|
|
173
243
|
state.watcherRestartTimer = null;
|
|
174
244
|
state.resultFileCoalescer.clear();
|
|
@@ -44,6 +44,7 @@ Packaged prompt shortcuts are also available for repeatable workflows:
|
|
|
44
44
|
- `/parallel-review` — fresh-context reviewers with distinct review angles, then synthesis
|
|
45
45
|
- `/parallel-research` — combine `researcher` and `scout` for external evidence plus local code context
|
|
46
46
|
- `/gather-context-and-clarify` — scout/research first, then ask the user clarifying questions with `interview`
|
|
47
|
+
- `/parallel-cleanup` — two fresh-context reviewers (deslop + verbosity passes) for an adversarial cleanup review of the current diff
|
|
47
48
|
|
|
48
49
|
## Builtin Agents
|
|
49
50
|
|
|
@@ -439,8 +440,9 @@ copying a full builtin file.
|
|
|
439
440
|
## Prompt Template Integration
|
|
440
441
|
|
|
441
442
|
The package includes prompt shortcuts for common workflows: `/parallel-review`,
|
|
442
|
-
`/parallel-research`,
|
|
443
|
-
wants repeatable review, research,
|
|
443
|
+
`/parallel-research`, `/gather-context-and-clarify`, and `/parallel-cleanup`.
|
|
444
|
+
Use them when the user wants repeatable review, research, clarification, or
|
|
445
|
+
cleanup-review patterns.
|
|
444
446
|
|
|
445
447
|
If `pi-prompt-template-model` is installed, additional user prompt templates can delegate into
|
|
446
448
|
`pi-subagents`. This is useful when a slash command should always run through a
|
package/slash-commands.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
24
24
|
SLASH_SUBAGENT_STARTED_EVENT,
|
|
25
25
|
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
26
|
+
type ExtensionConfig,
|
|
26
27
|
type SingleResult,
|
|
27
28
|
type SubagentState,
|
|
28
29
|
} from "./types.ts";
|
|
@@ -324,6 +325,7 @@ async function runSlashSubagent(
|
|
|
324
325
|
async function openAgentManager(
|
|
325
326
|
pi: ExtensionAPI,
|
|
326
327
|
ctx: ExtensionContext,
|
|
328
|
+
config: ExtensionConfig = {},
|
|
327
329
|
): Promise<void> {
|
|
328
330
|
const agentData = { ...discoverAgentsAll(ctx.cwd), cwd: ctx.cwd };
|
|
329
331
|
const models = ctx.modelRegistry.getAvailable().map((m) => ({
|
|
@@ -334,7 +336,7 @@ async function openAgentManager(
|
|
|
334
336
|
const skills = discoverAvailableSkills(ctx.cwd);
|
|
335
337
|
|
|
336
338
|
const result = await ctx.ui.custom<ManagerResult>(
|
|
337
|
-
(tui, theme, _kb, done) => new AgentManagerComponent(tui, theme, agentData, models, skills, done),
|
|
339
|
+
(tui, theme, _kb, done) => new AgentManagerComponent(tui, theme, agentData, models, skills, done, { newShortcut: config.agentManager?.newShortcut }),
|
|
338
340
|
{ overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
|
|
339
341
|
);
|
|
340
342
|
if (!result) return;
|
|
@@ -448,11 +450,12 @@ const parseAgentArgs = (
|
|
|
448
450
|
export function registerSlashCommands(
|
|
449
451
|
pi: ExtensionAPI,
|
|
450
452
|
state: SubagentState,
|
|
453
|
+
config: ExtensionConfig = {},
|
|
451
454
|
): void {
|
|
452
455
|
pi.registerCommand("agents", {
|
|
453
456
|
description: "Open the Agents Manager",
|
|
454
457
|
handler: async (_args, ctx) => {
|
|
455
|
-
await openAgentManager(pi, ctx);
|
|
458
|
+
await openAgentManager(pi, ctx, config);
|
|
456
459
|
},
|
|
457
460
|
});
|
|
458
461
|
|
|
@@ -578,7 +581,7 @@ export function registerSlashCommands(
|
|
|
578
581
|
|
|
579
582
|
pi.registerShortcut("ctrl+shift+a", {
|
|
580
583
|
handler: async (ctx) => {
|
|
581
|
-
await openAgentManager(pi, ctx);
|
|
584
|
+
await openAgentManager(pi, ctx, config);
|
|
582
585
|
},
|
|
583
586
|
});
|
|
584
587
|
}
|
package/subagent-runner.ts
CHANGED
|
@@ -282,7 +282,11 @@ function runPiStreaming(
|
|
|
282
282
|
const stopReason = (event.message as { stopReason?: string }).stopReason;
|
|
283
283
|
const hasToolCall = Array.isArray(event.message.content)
|
|
284
284
|
&& event.message.content.some((part) => (part as { type?: string }).type === "toolCall");
|
|
285
|
-
|
|
285
|
+
const finalText = extractTextFromContent(event.message.content).trim();
|
|
286
|
+
if (stopReason === "stop" && !hasToolCall) {
|
|
287
|
+
finalAssistantDelivered ||= !event.message.errorMessage && finalText.length > 0;
|
|
288
|
+
startFinalDrain();
|
|
289
|
+
}
|
|
286
290
|
}
|
|
287
291
|
};
|
|
288
292
|
|
|
@@ -305,6 +309,8 @@ function runPiStreaming(
|
|
|
305
309
|
const HARD_KILL_MS = 3000;
|
|
306
310
|
let childExited = false;
|
|
307
311
|
let forcedTerminationSignal = false;
|
|
312
|
+
let finalAssistantDelivered = false;
|
|
313
|
+
let finalDrainCleanupWarning: string | undefined;
|
|
308
314
|
let finalDrainTimer: NodeJS.Timeout | undefined;
|
|
309
315
|
let finalHardKillTimer: NodeJS.Timeout | undefined;
|
|
310
316
|
let settled = false;
|
|
@@ -346,8 +352,9 @@ function runPiStreaming(
|
|
|
346
352
|
const termSent = trySignalChild(child, "SIGTERM");
|
|
347
353
|
if (!termSent) return;
|
|
348
354
|
forcedTerminationSignal = true;
|
|
349
|
-
|
|
350
|
-
|
|
355
|
+
finalDrainCleanupWarning = `Subagent process did not exit within ${FINAL_DRAIN_MS}ms after its final message. Forcing termination.`;
|
|
356
|
+
if (!finalAssistantDelivered && !error) {
|
|
357
|
+
error = finalDrainCleanupWarning;
|
|
351
358
|
}
|
|
352
359
|
finalHardKillTimer = setTimeout(() => {
|
|
353
360
|
if (settled) return;
|
|
@@ -370,13 +377,14 @@ function runPiStreaming(
|
|
|
370
377
|
if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
|
|
371
378
|
outputStream.end();
|
|
372
379
|
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
380
|
+
const forcedDrainAfterFinalSuccess = forcedTerminationSignal && finalAssistantDelivered;
|
|
373
381
|
resolve({
|
|
374
382
|
stderr,
|
|
375
|
-
exitCode: interrupted ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
383
|
+
exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
|
|
376
384
|
messages,
|
|
377
385
|
usage,
|
|
378
386
|
model,
|
|
379
|
-
error: interrupted ? undefined : error,
|
|
387
|
+
error: interrupted || forcedDrainAfterFinalSuccess ? undefined : error,
|
|
380
388
|
finalOutput,
|
|
381
389
|
interrupted,
|
|
382
390
|
observedMutationAttempt,
|
package/types.ts
CHANGED
|
@@ -445,6 +445,10 @@ interface TopLevelParallelConfig {
|
|
|
445
445
|
concurrency?: number;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
interface AgentManagerConfig {
|
|
449
|
+
newShortcut?: string;
|
|
450
|
+
}
|
|
451
|
+
|
|
448
452
|
export interface ExtensionConfig {
|
|
449
453
|
asyncByDefault?: boolean;
|
|
450
454
|
forceTopLevelAsync?: boolean;
|
|
@@ -452,6 +456,7 @@ export interface ExtensionConfig {
|
|
|
452
456
|
maxSubagentDepth?: number;
|
|
453
457
|
control?: ControlConfig;
|
|
454
458
|
parallel?: TopLevelParallelConfig;
|
|
459
|
+
agentManager?: AgentManagerConfig;
|
|
455
460
|
worktreeSetupHook?: string;
|
|
456
461
|
worktreeSetupHookTimeoutMs?: number;
|
|
457
462
|
intercomBridge?: IntercomBridgeConfig;
|