pi-subagents 0.21.4 → 0.22.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/CHANGELOG.md +23 -0
- package/README.md +12 -11
- package/agents/context-builder.md +4 -5
- package/agents/delegate.md +2 -1
- package/agents/oracle.md +5 -8
- package/agents/planner.md +2 -3
- package/agents/researcher.md +2 -3
- package/agents/reviewer.md +4 -21
- package/agents/scout.md +2 -3
- package/agents/worker.md +7 -7
- package/package.json +3 -2
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +2 -2
- package/skills/pi-subagents/SKILL.md +30 -27
- package/src/extension/index.ts +4 -1
- package/src/intercom/intercom-bridge.ts +10 -8
- package/src/intercom/result-intercom.ts +4 -3
- package/src/manager-ui/agent-manager-edit.ts +43 -19
- package/src/manager-ui/agent-manager.ts +5 -2
- package/src/runs/background/async-execution.ts +16 -10
- package/src/runs/background/async-job-tracker.ts +12 -19
- package/src/runs/background/async-resume.ts +1 -0
- package/src/runs/background/async-status.ts +35 -49
- package/src/runs/background/parallel-groups.ts +45 -0
- package/src/runs/background/result-watcher.ts +2 -2
- package/src/runs/background/run-status.ts +26 -7
- package/src/runs/background/stale-run-reconciler.ts +15 -2
- package/src/runs/background/subagent-runner.ts +12 -3
- package/src/runs/foreground/chain-clarify.ts +35 -30
- package/src/runs/foreground/chain-execution.ts +9 -6
- package/src/runs/foreground/execution.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +18 -30
- package/src/runs/shared/model-fallback.ts +2 -5
- package/src/runs/shared/pi-args.ts +20 -0
- package/src/shared/model-info.ts +68 -0
- package/src/shared/session-identity.ts +10 -0
- package/src/shared/types.ts +14 -5
- package/src/slash/slash-commands.ts +8 -7
- package/src/tui/render.ts +67 -2
- package/src/tui/subagents-status.ts +129 -15
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type SubagentResultIntercomChild,
|
|
7
7
|
type SubagentResultIntercomPayload,
|
|
8
8
|
type SubagentResultStatus,
|
|
9
|
+
type SubagentRunMode,
|
|
9
10
|
SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
|
|
10
11
|
SUBAGENT_RESULT_INTERCOM_EVENT,
|
|
11
12
|
} from "../shared/types.ts";
|
|
@@ -61,7 +62,7 @@ function resolveGroupedStatus(children: SubagentResultIntercomChild[]): Subagent
|
|
|
61
62
|
interface GroupedResultIntercomMessageInput {
|
|
62
63
|
to: string;
|
|
63
64
|
runId: string;
|
|
64
|
-
mode:
|
|
65
|
+
mode: SubagentRunMode;
|
|
65
66
|
source: "foreground" | "async";
|
|
66
67
|
children: SubagentResultIntercomChild[];
|
|
67
68
|
asyncId?: string;
|
|
@@ -84,7 +85,7 @@ function asyncResumeGuidance(input: {
|
|
|
84
85
|
|
|
85
86
|
function formatSubagentResultIntercomMessage(input: {
|
|
86
87
|
runId: string;
|
|
87
|
-
mode:
|
|
88
|
+
mode: SubagentRunMode;
|
|
88
89
|
status: SubagentResultStatus;
|
|
89
90
|
source: "foreground" | "async";
|
|
90
91
|
children: SubagentResultIntercomChild[];
|
|
@@ -218,7 +219,7 @@ export function stripDetailsOutputsForIntercomReceipt(details: Details): Details
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
export function formatSubagentResultReceipt(input: {
|
|
221
|
-
mode:
|
|
222
|
+
mode: SubagentRunMode;
|
|
222
223
|
runId: string;
|
|
223
224
|
payload: SubagentResultIntercomPayload;
|
|
224
225
|
}): string {
|
|
@@ -4,30 +4,31 @@ import { buildRuntimeName, defaultSystemPromptMode, frontmatterNameForConfig, pa
|
|
|
4
4
|
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../tui/text-editor.ts";
|
|
5
5
|
import type { TextEditorState } from "../tui/text-editor.ts";
|
|
6
6
|
import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "../tui/render-helpers.ts";
|
|
7
|
+
import { findModelInfo, getSupportedThinkingLevels, type ModelInfo, type ThinkingLevel } from "../shared/model-info.ts";
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
+
export type { ModelInfo };
|
|
9
10
|
export interface SkillInfo { name: string; source: string; description?: string; }
|
|
10
11
|
export type EditScreen = "edit" | "edit-field" | "edit-prompt";
|
|
11
12
|
export type EditField = typeof FIELD_ORDER[number];
|
|
12
13
|
|
|
13
14
|
export interface EditState {
|
|
14
15
|
draft: AgentConfig; isNew: boolean; fieldIndex: number; fieldMode: "text" | "model" | "thinking" | "skills" | null;
|
|
15
|
-
fieldEditor: TextEditorState; promptEditor: TextEditorState; modelSearchQuery: string; modelCursor: number; filteredModels: ModelInfo[];
|
|
16
|
+
fieldEditor: TextEditorState; promptEditor: TextEditorState; modelSearchQuery: string; modelCursor: number; models: ModelInfo[]; filteredModels: ModelInfo[];
|
|
16
17
|
thinkingCursor: number; skillSearchQuery: string; skillCursor: number; filteredSkills: SkillInfo[]; skillSelected: Set<string>; error?: string;
|
|
17
18
|
fields: EditField[];
|
|
18
19
|
title?: string;
|
|
19
20
|
overrideBase?: BuiltinAgentOverrideBase;
|
|
21
|
+
preferredProvider?: string;
|
|
20
22
|
}
|
|
21
23
|
interface EditInputResult { action?: "save" | "discard" | "delete"; nextScreen?: EditScreen; }
|
|
22
24
|
interface CreateEditStateOptions {
|
|
23
25
|
fields?: EditField[];
|
|
24
26
|
title?: string;
|
|
25
27
|
overrideBase?: BuiltinAgentOverrideBase;
|
|
28
|
+
preferredProvider?: string;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
29
31
|
const FIELD_ORDER = ["name", "package", "description", "model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "defaultContext", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
30
|
-
type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
31
32
|
const PROMPT_VIEWPORT_HEIGHT = 16;
|
|
32
33
|
const MODEL_SELECTOR_HEIGHT = 10;
|
|
33
34
|
const SKILL_SELECTOR_HEIGHT = 10;
|
|
@@ -86,8 +87,8 @@ export function createEditState(draft: AgentConfig, isNew: boolean, models: Mode
|
|
|
86
87
|
return {
|
|
87
88
|
draft: { ...draft, tools: draft.tools ? [...draft.tools] : undefined, mcpDirectTools: draft.mcpDirectTools ? [...draft.mcpDirectTools] : undefined, skills: draft.skills ? [...draft.skills] : undefined, fallbackModels: draft.fallbackModels ? [...draft.fallbackModels] : undefined, extensions: draft.extensions ? [...draft.extensions] : draft.extensions, defaultReads: draft.defaultReads ? [...draft.defaultReads] : undefined, extraFields: draft.extraFields ? { ...draft.extraFields } : undefined },
|
|
88
89
|
isNew, fieldIndex: 0, fieldMode: null, fieldEditor: createEditorState(), promptEditor: createEditorState(draft.systemPrompt ?? ""),
|
|
89
|
-
modelSearchQuery: "", modelCursor: 0, filteredModels: [...models], thinkingCursor: 0, skillSearchQuery: "", skillCursor: 0, filteredSkills: [...skills], skillSelected: new Set(draft.skills ?? []),
|
|
90
|
-
fields: options.fields ?? [...FIELD_ORDER], title: options.title, overrideBase: options.overrideBase,
|
|
90
|
+
modelSearchQuery: "", modelCursor: 0, models: [...models], filteredModels: [...models], thinkingCursor: 0, skillSearchQuery: "", skillCursor: 0, filteredSkills: [...skills], skillSelected: new Set(draft.skills ?? []),
|
|
91
|
+
fields: options.fields ?? [...FIELD_ORDER], title: options.title, overrideBase: options.overrideBase, preferredProvider: options.preferredProvider,
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -180,9 +181,16 @@ function openModelPicker(state: EditState, models: ModelInfo[]): void {
|
|
|
180
181
|
state.fieldIndex = state.fields.indexOf("model"); state.fieldMode = "model"; state.modelSearchQuery = ""; state.filteredModels = [...models];
|
|
181
182
|
const idx = state.filteredModels.findIndex((m) => m.fullId === state.draft.model || m.id === state.draft.model); state.modelCursor = idx >= 0 ? idx : 0;
|
|
182
183
|
}
|
|
184
|
+
function getDraftThinkingLevels(state: EditState): ThinkingLevel[] {
|
|
185
|
+
return getSupportedThinkingLevels(findModelInfo(state.draft.model, state.models, state.preferredProvider));
|
|
186
|
+
}
|
|
187
|
+
|
|
183
188
|
function openThinkingPicker(state: EditState): void {
|
|
184
189
|
state.fieldIndex = state.fields.indexOf("thinking"); state.fieldMode = "thinking";
|
|
185
|
-
const
|
|
190
|
+
const levels = getDraftThinkingLevels(state);
|
|
191
|
+
const currentLevel = state.draft.thinking ?? "off";
|
|
192
|
+
const idx = levels.findIndex((level) => level === currentLevel);
|
|
193
|
+
state.thinkingCursor = idx >= 0 ? idx : Math.max(0, levels.indexOf("off"));
|
|
186
194
|
}
|
|
187
195
|
function openSkillPicker(state: EditState, skills: SkillInfo[]): void {
|
|
188
196
|
state.fieldIndex = state.fields.indexOf("skills"); state.fieldMode = "skills"; state.skillSearchQuery = ""; state.filteredSkills = [...skills]; state.skillSelected = new Set(state.draft.skills ?? []); state.skillCursor = 0;
|
|
@@ -230,16 +238,22 @@ function renderThinkingPicker(state: EditState, width: number, theme: Theme): st
|
|
|
230
238
|
high: "Deep reasoning",
|
|
231
239
|
xhigh: "Maximum reasoning (ultrathink)",
|
|
232
240
|
};
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
const levels = getDraftThinkingLevels(state);
|
|
242
|
+
if (levels.length === 0) {
|
|
243
|
+
lines.push(row(` ${theme.fg("dim", "No supported thinking levels")}`, width, theme));
|
|
244
|
+
} else {
|
|
245
|
+
for (let i = 0; i < levels.length; i++) {
|
|
246
|
+
const level = levels[i]!;
|
|
247
|
+
const isSelected = i === state.thinkingCursor;
|
|
248
|
+
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
|
|
249
|
+
const levelText = isSelected ? theme.fg("accent", level) : level;
|
|
250
|
+
const desc = theme.fg("dim", ` - ${descriptions[level]}`);
|
|
251
|
+
lines.push(row(` ${prefix}${levelText}${desc}`, width, theme));
|
|
252
|
+
}
|
|
240
253
|
}
|
|
241
254
|
while (lines.length < 19) lines.push(row("", width, theme));
|
|
242
|
-
|
|
255
|
+
const footer = levels.length === 0 ? " [esc] cancel " : " [enter] select [esc] cancel [↑↓] navigate ";
|
|
256
|
+
lines.push(renderFooter(footer, width, theme));
|
|
243
257
|
return lines;
|
|
244
258
|
}
|
|
245
259
|
|
|
@@ -323,7 +337,15 @@ export function handleEditInput(screen: EditScreen, state: EditState, data: stri
|
|
|
323
337
|
if (screen === "edit-field") {
|
|
324
338
|
if (state.fieldMode === "model") {
|
|
325
339
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
326
|
-
if (matchesKey(data, "return")) {
|
|
340
|
+
if (matchesKey(data, "return")) {
|
|
341
|
+
const selected = state.filteredModels[state.modelCursor];
|
|
342
|
+
if (selected) {
|
|
343
|
+
state.draft.model = selected.fullId;
|
|
344
|
+
if (state.draft.thinking && !getDraftThinkingLevels(state).some((level) => level === state.draft.thinking)) state.draft.thinking = undefined;
|
|
345
|
+
}
|
|
346
|
+
state.fieldMode = null;
|
|
347
|
+
return { nextScreen: "edit" };
|
|
348
|
+
}
|
|
327
349
|
if (matchesKey(data, "up")) { if (state.filteredModels.length > 0) state.modelCursor = state.modelCursor === 0 ? state.filteredModels.length - 1 : state.modelCursor - 1; return; }
|
|
328
350
|
if (matchesKey(data, "down")) { if (state.filteredModels.length > 0) state.modelCursor = state.modelCursor === state.filteredModels.length - 1 ? 0 : state.modelCursor + 1; return; }
|
|
329
351
|
if (matchesKey(data, "backspace")) { if (state.modelSearchQuery.length > 0) state.modelSearchQuery = state.modelSearchQuery.slice(0, -1); }
|
|
@@ -334,10 +356,12 @@ export function handleEditInput(screen: EditScreen, state: EditState, data: stri
|
|
|
334
356
|
return;
|
|
335
357
|
}
|
|
336
358
|
if (state.fieldMode === "thinking") {
|
|
359
|
+
const levels = getDraftThinkingLevels(state);
|
|
337
360
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
338
|
-
if (
|
|
339
|
-
if (matchesKey(data, "
|
|
340
|
-
if (matchesKey(data, "
|
|
361
|
+
if (levels.length === 0) return;
|
|
362
|
+
if (matchesKey(data, "return")) { const selected = levels[state.thinkingCursor] ?? "off"; state.draft.thinking = selected === "off" ? undefined : selected; state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
363
|
+
if (matchesKey(data, "up")) { state.thinkingCursor = state.thinkingCursor === 0 ? levels.length - 1 : state.thinkingCursor - 1; return; }
|
|
364
|
+
if (matchesKey(data, "down")) { state.thinkingCursor = state.thinkingCursor === levels.length - 1 ? 0 : state.thinkingCursor + 1; return; }
|
|
341
365
|
return;
|
|
342
366
|
}
|
|
343
367
|
if (state.fieldMode === "skills") {
|
|
@@ -45,7 +45,7 @@ interface ChainEntry { id: string; kind: "chain"; config: ChainConfig; }
|
|
|
45
45
|
interface NameInputState { mode: "new-agent" | "clone-agent" | "clone-chain" | "new-chain"; editor: TextEditorState; scope: "user" | "project"; allowProject: boolean; sourceId?: string; template?: AgentTemplate; error?: string; }
|
|
46
46
|
interface StatusMessage { text: string; type: "error" | "info"; }
|
|
47
47
|
interface OverrideScopeState { selectedScope: "user" | "project"; allowProject: boolean; }
|
|
48
|
-
export interface AgentManagerOptions { newShortcut?: string; }
|
|
48
|
+
export interface AgentManagerOptions { newShortcut?: string; preferredModelProvider?: string; }
|
|
49
49
|
|
|
50
50
|
const BUILTIN_OVERRIDE_FIELDS: EditField[] = ["model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "defaultContext", "disabled", "tools", "skills", "prompt"];
|
|
51
51
|
|
|
@@ -135,6 +135,7 @@ export class AgentManagerComponent implements Component {
|
|
|
135
135
|
private skills: SkillInfo[];
|
|
136
136
|
private done: (result: ManagerResult) => void;
|
|
137
137
|
private shortcuts: ListShortcuts;
|
|
138
|
+
private preferredModelProvider: string | undefined;
|
|
138
139
|
|
|
139
140
|
constructor(tui: TUI, theme: Theme, agentData: AgentData, models: ModelInfo[], skills: SkillInfo[], done: (result: ManagerResult) => void, options: AgentManagerOptions = {}) {
|
|
140
141
|
this.tui = tui;
|
|
@@ -144,6 +145,7 @@ export class AgentManagerComponent implements Component {
|
|
|
144
145
|
this.skills = skills;
|
|
145
146
|
this.done = done;
|
|
146
147
|
this.shortcuts = { newShortcut: options.newShortcut?.trim() || DEFAULT_AGENT_MANAGER_NEW_SHORTCUT };
|
|
148
|
+
this.preferredModelProvider = options.preferredModelProvider;
|
|
147
149
|
this.loadEntries();
|
|
148
150
|
}
|
|
149
151
|
|
|
@@ -196,7 +198,7 @@ export class AgentManagerComponent implements Component {
|
|
|
196
198
|
|
|
197
199
|
private enterDetail(entry: AgentEntry): void { this.currentAgentId = entry.id; this.detailState = { resolved: true, scrollOffset: 0, recentRuns: loadRunsForAgent(entry.config.name).slice(0, 5) }; this.screen = "detail"; }
|
|
198
200
|
private enterChainDetail(entry: ChainEntry): void { this.currentChainId = entry.id; this.chainDetailState = { scrollOffset: 0 }; this.screen = "chain-detail"; }
|
|
199
|
-
private enterEdit(entry: AgentEntry): void { this.currentAgentId = entry.id; this.builtinOverrideScope = null; this.editState = createEditState(entry.config, entry.isNew, this.models, this.skills); this.screen = "edit"; }
|
|
201
|
+
private enterEdit(entry: AgentEntry): void { this.currentAgentId = entry.id; this.builtinOverrideScope = null; this.editState = createEditState(entry.config, entry.isNew, this.models, this.skills, { preferredProvider: this.preferredModelProvider }); this.screen = "edit"; }
|
|
200
202
|
private enterBuiltinOverrideScope(entry: AgentEntry): void {
|
|
201
203
|
this.currentAgentId = entry.id;
|
|
202
204
|
this.overrideScopeState = { selectedScope: this.agentData.projectSettingsPath ? "project" : "user", allowProject: Boolean(this.agentData.projectSettingsPath) };
|
|
@@ -209,6 +211,7 @@ export class AgentManagerComponent implements Component {
|
|
|
209
211
|
fields: BUILTIN_OVERRIDE_FIELDS,
|
|
210
212
|
title: `Builtin Override: ${entry.config.name} [${scope}]`,
|
|
211
213
|
overrideBase: this.resolveBuiltinOverrideBase(entry),
|
|
214
|
+
preferredProvider: this.preferredModelProvider,
|
|
212
215
|
});
|
|
213
216
|
this.screen = "edit";
|
|
214
217
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type Details,
|
|
25
25
|
type MaxOutputConfig,
|
|
26
26
|
type ResolvedControlConfig,
|
|
27
|
+
type SubagentRunMode,
|
|
27
28
|
ASYNC_DIR,
|
|
28
29
|
RESULTS_DIR,
|
|
29
30
|
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
@@ -64,7 +65,7 @@ interface AsyncExecutionContext {
|
|
|
64
65
|
|
|
65
66
|
interface AsyncChainParams {
|
|
66
67
|
chain: ChainStep[];
|
|
67
|
-
resultMode?:
|
|
68
|
+
resultMode?: Exclude<SubagentRunMode, "single">;
|
|
68
69
|
agents: AgentConfig[];
|
|
69
70
|
ctx: AsyncExecutionContext;
|
|
70
71
|
availableModels?: AvailableModelInfo[];
|
|
@@ -160,7 +161,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
|
|
|
160
161
|
return { pid: proc.pid };
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
function formatAsyncStartError(mode:
|
|
164
|
+
function formatAsyncStartError(mode: SubagentRunMode, message: string): AsyncExecutionResult {
|
|
164
165
|
return {
|
|
165
166
|
content: [{ type: "text", text: message }],
|
|
166
167
|
isError: true,
|
|
@@ -198,6 +199,7 @@ export function executeAsyncChain(
|
|
|
198
199
|
controlIntercomTarget,
|
|
199
200
|
childIntercomTarget,
|
|
200
201
|
} = params;
|
|
202
|
+
const resultMode = params.resultMode ?? "chain";
|
|
201
203
|
const chainSkills = params.chainSkills ?? [];
|
|
202
204
|
const availableModels = params.availableModels;
|
|
203
205
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
@@ -211,7 +213,7 @@ export function executeAsyncChain(
|
|
|
211
213
|
return {
|
|
212
214
|
content: [{ type: "text", text: `Unknown agent: ${agentName}` }],
|
|
213
215
|
isError: true,
|
|
214
|
-
details: { mode:
|
|
216
|
+
details: { mode: resultMode, results: [] },
|
|
215
217
|
};
|
|
216
218
|
}
|
|
217
219
|
}
|
|
@@ -225,7 +227,7 @@ export function executeAsyncChain(
|
|
|
225
227
|
return {
|
|
226
228
|
content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
|
|
227
229
|
isError: true,
|
|
228
|
-
details: { mode:
|
|
230
|
+
details: { mode: resultMode, results: [] },
|
|
229
231
|
};
|
|
230
232
|
}
|
|
231
233
|
|
|
@@ -329,7 +331,7 @@ export function executeAsyncChain(
|
|
|
329
331
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
330
332
|
});
|
|
331
333
|
} catch (error) {
|
|
332
|
-
if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return formatAsyncStartError(
|
|
334
|
+
if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return formatAsyncStartError(resultMode, error.message);
|
|
333
335
|
throw error;
|
|
334
336
|
}
|
|
335
337
|
let childTargetIndex = 0;
|
|
@@ -363,18 +365,18 @@ export function executeAsyncChain(
|
|
|
363
365
|
controlConfig,
|
|
364
366
|
controlIntercomTarget,
|
|
365
367
|
childIntercomTargets,
|
|
366
|
-
resultMode
|
|
368
|
+
resultMode,
|
|
367
369
|
},
|
|
368
370
|
id,
|
|
369
371
|
runnerCwd,
|
|
370
372
|
);
|
|
371
373
|
} catch (error) {
|
|
372
374
|
const message = error instanceof Error ? error.message : String(error);
|
|
373
|
-
return formatAsyncStartError(
|
|
375
|
+
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${message}`);
|
|
374
376
|
}
|
|
375
377
|
|
|
376
378
|
if (spawnResult.error) {
|
|
377
|
-
return formatAsyncStartError(
|
|
379
|
+
return formatAsyncStartError(resultMode, `Failed to start async ${resultMode} '${id}': ${spawnResult.error}`);
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
if (spawnResult.pid) {
|
|
@@ -399,6 +401,8 @@ export function executeAsyncChain(
|
|
|
399
401
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
400
402
|
id,
|
|
401
403
|
pid: spawnResult.pid,
|
|
404
|
+
sessionId: ctx.currentSessionId,
|
|
405
|
+
mode: resultMode,
|
|
402
406
|
agent: firstAgents[0],
|
|
403
407
|
agents: flatAgents,
|
|
404
408
|
task: isParallelStep(firstStep)
|
|
@@ -421,8 +425,8 @@ export function executeAsyncChain(
|
|
|
421
425
|
.join(" -> ");
|
|
422
426
|
|
|
423
427
|
return {
|
|
424
|
-
content: [{ type: "text", text: `Async
|
|
425
|
-
details: { mode:
|
|
428
|
+
content: [{ type: "text", text: `Async ${resultMode}: ${chainDesc} [${id}]` }],
|
|
429
|
+
details: { mode: resultMode, results: [], asyncId: id, asyncDir },
|
|
426
430
|
};
|
|
427
431
|
}
|
|
428
432
|
|
|
@@ -543,6 +547,8 @@ export function executeAsyncSingle(
|
|
|
543
547
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
544
548
|
id,
|
|
545
549
|
pid: spawnResult.pid,
|
|
550
|
+
sessionId: ctx.currentSessionId,
|
|
551
|
+
mode: "single",
|
|
546
552
|
agent,
|
|
547
553
|
task: task?.slice(0, 50),
|
|
548
554
|
cwd: runnerCwd,
|
|
@@ -5,7 +5,6 @@ import { renderWidget } from "../../tui/render.ts";
|
|
|
5
5
|
import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
|
|
6
6
|
import {
|
|
7
7
|
type AsyncJobState,
|
|
8
|
-
type AsyncParallelGroupStatus,
|
|
9
8
|
type AsyncStartedEvent,
|
|
10
9
|
type ControlEvent,
|
|
11
10
|
type SubagentState,
|
|
@@ -15,25 +14,9 @@ import {
|
|
|
15
14
|
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
16
15
|
} from "../../shared/types.ts";
|
|
17
16
|
import { readStatus } from "../../shared/utils.ts";
|
|
17
|
+
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
18
18
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
function isValidParallelGroup(group: AsyncParallelGroupStatus, stepCount: number, chainStepCount: number): boolean {
|
|
22
|
-
return Number.isInteger(group.start)
|
|
23
|
-
&& Number.isInteger(group.count)
|
|
24
|
-
&& Number.isInteger(group.stepIndex)
|
|
25
|
-
&& group.start >= 0
|
|
26
|
-
&& group.count > 0
|
|
27
|
-
&& group.stepIndex >= 0
|
|
28
|
-
&& group.stepIndex < chainStepCount
|
|
29
|
-
&& group.start + group.count <= stepCount;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeParallelGroups(groups: AsyncParallelGroupStatus[] | undefined, stepCount: number, chainStepCount: number): AsyncParallelGroupStatus[] {
|
|
33
|
-
if (!groups?.length) return [];
|
|
34
|
-
return groups.filter((group) => isValidParallelGroup(group, stepCount, chainStepCount));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
20
|
interface AsyncJobTrackerOptions {
|
|
38
21
|
completionRetentionMs?: number;
|
|
39
22
|
pollIntervalMs?: number;
|
|
@@ -145,8 +128,11 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
145
128
|
startedRun: {
|
|
146
129
|
runId: job.asyncId,
|
|
147
130
|
pid: job.pid,
|
|
131
|
+
sessionId: job.sessionId,
|
|
148
132
|
mode: job.mode,
|
|
149
133
|
agents: job.agents,
|
|
134
|
+
chainStepCount: job.chainStepCount,
|
|
135
|
+
parallelGroups: job.parallelGroups,
|
|
150
136
|
startedAt: job.startedAt,
|
|
151
137
|
sessionFile: job.sessionFile,
|
|
152
138
|
},
|
|
@@ -155,6 +141,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
155
141
|
if (status) {
|
|
156
142
|
const previousStatus = job.status;
|
|
157
143
|
job.status = status.state;
|
|
144
|
+
job.sessionId = status.sessionId ?? job.sessionId;
|
|
158
145
|
job.activityState = status.activityState;
|
|
159
146
|
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
160
147
|
job.currentTool = status.currentTool;
|
|
@@ -164,10 +151,12 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
164
151
|
job.toolCount = status.toolCount ?? job.toolCount;
|
|
165
152
|
job.mode = status.mode;
|
|
166
153
|
job.currentStep = status.currentStep ?? job.currentStep;
|
|
154
|
+
job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
|
|
167
155
|
job.startedAt = status.startedAt ?? job.startedAt;
|
|
168
156
|
job.updatedAt = status.lastUpdate ?? Date.now();
|
|
169
157
|
if (status.steps?.length) {
|
|
170
158
|
const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
|
|
159
|
+
job.parallelGroups = groups.length ? groups : job.parallelGroups;
|
|
171
160
|
job.hasParallelGroups = groups.length > 0 || job.hasParallelGroups;
|
|
172
161
|
const activeGroup = status.currentStep !== undefined
|
|
173
162
|
? groups.find((group) => status.currentStep! >= group.start && status.currentStep! < group.start + group.count)
|
|
@@ -177,6 +166,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
177
166
|
: status.steps;
|
|
178
167
|
job.activeParallelGroup = Boolean(activeGroup);
|
|
179
168
|
job.agents = visibleSteps.map((step) => step.agent);
|
|
169
|
+
job.steps = visibleSteps;
|
|
180
170
|
job.stepsTotal = visibleSteps.length;
|
|
181
171
|
job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
|
|
182
172
|
job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
@@ -225,8 +215,11 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
225
215
|
asyncDir,
|
|
226
216
|
status: "queued",
|
|
227
217
|
pid: typeof info.pid === "number" ? info.pid : undefined,
|
|
228
|
-
|
|
218
|
+
...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
|
|
219
|
+
mode: info.mode ?? (info.chain ? "chain" : "single"),
|
|
229
220
|
agents,
|
|
221
|
+
chainStepCount: info.chainStepCount,
|
|
222
|
+
parallelGroups: validParallelGroups,
|
|
230
223
|
stepsTotal: firstGroupCount ?? agents?.length,
|
|
231
224
|
hasParallelGroups: validParallelGroups.length > 0,
|
|
232
225
|
activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
|
|
@@ -202,6 +202,7 @@ function resultState(result: AsyncResultFile): AsyncStatus["state"] {
|
|
|
202
202
|
function validateStatusForResume(status: AsyncStatus | null, source: string): void {
|
|
203
203
|
if (!status) return;
|
|
204
204
|
if (typeof status.runId !== "string") throw new Error(`Invalid async status '${source}': runId must be a string.`);
|
|
205
|
+
if (status.sessionId !== undefined && typeof status.sessionId !== "string") throw new Error(`Invalid async status '${source}': sessionId must be a string.`);
|
|
205
206
|
if (status.cwd !== undefined && typeof status.cwd !== "string") throw new Error(`Invalid async status '${source}': cwd must be a string.`);
|
|
206
207
|
if (status.sessionFile !== undefined && typeof status.sessionFile !== "string") throw new Error(`Invalid async status '${source}': sessionFile must be a string.`);
|
|
207
208
|
if (status.steps !== undefined) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
|
-
import { type ActivityState, type AsyncParallelGroupStatus, type AsyncStatus, type TokenUsage } from "../../shared/types.ts";
|
|
4
|
+
import { type ActivityState, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
|
|
5
5
|
import { readStatus } from "../../shared/utils.ts";
|
|
6
|
+
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
6
7
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
7
8
|
|
|
8
9
|
interface AsyncRunStepSummary {
|
|
@@ -27,6 +28,7 @@ interface AsyncRunStepSummary {
|
|
|
27
28
|
export interface AsyncRunSummary {
|
|
28
29
|
id: string;
|
|
29
30
|
asyncDir: string;
|
|
31
|
+
sessionId?: string;
|
|
30
32
|
state: "queued" | "running" | "complete" | "failed" | "paused";
|
|
31
33
|
activityState?: ActivityState;
|
|
32
34
|
lastActivityAt?: number;
|
|
@@ -35,7 +37,7 @@ export interface AsyncRunSummary {
|
|
|
35
37
|
currentPath?: string;
|
|
36
38
|
turnCount?: number;
|
|
37
39
|
toolCount?: number;
|
|
38
|
-
mode:
|
|
40
|
+
mode: SubagentRunMode;
|
|
39
41
|
cwd?: string;
|
|
40
42
|
startedAt: number;
|
|
41
43
|
lastUpdate?: number;
|
|
@@ -50,45 +52,9 @@ export interface AsyncRunSummary {
|
|
|
50
52
|
sessionFile?: string;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
function isValidParallelGroup(group: AsyncParallelGroupStatus, stepCount: number, chainStepCount: number): boolean {
|
|
54
|
-
return Number.isInteger(group.start)
|
|
55
|
-
&& Number.isInteger(group.count)
|
|
56
|
-
&& Number.isInteger(group.stepIndex)
|
|
57
|
-
&& group.start >= 0
|
|
58
|
-
&& group.count > 0
|
|
59
|
-
&& group.stepIndex >= 0
|
|
60
|
-
&& group.stepIndex < chainStepCount
|
|
61
|
-
&& group.start + group.count <= stepCount;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function normalizeParallelGroups(groups: AsyncParallelGroupStatus[] | undefined, stepCount: number, chainStepCount: number): AsyncParallelGroupStatus[] {
|
|
65
|
-
if (!groups?.length) return [];
|
|
66
|
-
return groups.filter((group) => isValidParallelGroup(group, stepCount, chainStepCount));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function flatToLogicalStepIndex(flatIndex: number, chainStepCount: number, parallelGroups: AsyncParallelGroupStatus[]): number {
|
|
70
|
-
let logicalIndex = 0;
|
|
71
|
-
let cursor = 0;
|
|
72
|
-
for (const group of parallelGroups) {
|
|
73
|
-
while (logicalIndex < chainStepCount && cursor < group.start) {
|
|
74
|
-
if (flatIndex === cursor) return logicalIndex;
|
|
75
|
-
logicalIndex++;
|
|
76
|
-
cursor++;
|
|
77
|
-
}
|
|
78
|
-
if (flatIndex >= group.start && flatIndex < group.start + group.count) return group.stepIndex;
|
|
79
|
-
logicalIndex = Math.max(logicalIndex, group.stepIndex + 1);
|
|
80
|
-
cursor = group.start + group.count;
|
|
81
|
-
}
|
|
82
|
-
while (logicalIndex < chainStepCount) {
|
|
83
|
-
if (flatIndex === cursor) return logicalIndex;
|
|
84
|
-
logicalIndex++;
|
|
85
|
-
cursor++;
|
|
86
|
-
}
|
|
87
|
-
return Math.max(0, chainStepCount - 1);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
55
|
interface AsyncRunListOptions {
|
|
91
56
|
states?: Array<AsyncRunSummary["state"]>;
|
|
57
|
+
sessionId?: string;
|
|
92
58
|
limit?: number;
|
|
93
59
|
resultsDir?: string;
|
|
94
60
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
@@ -101,6 +67,11 @@ export interface AsyncRunOverlayData {
|
|
|
101
67
|
recent: AsyncRunSummary[];
|
|
102
68
|
}
|
|
103
69
|
|
|
70
|
+
export interface AsyncRunOverlayOptions {
|
|
71
|
+
recentLimit?: number;
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
104
75
|
function getErrorMessage(error: unknown): string {
|
|
105
76
|
return error instanceof Error ? error.message : String(error);
|
|
106
77
|
}
|
|
@@ -147,6 +118,9 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
|
|
|
147
118
|
}
|
|
148
119
|
|
|
149
120
|
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
|
|
121
|
+
if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
|
|
122
|
+
throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
|
|
123
|
+
}
|
|
150
124
|
const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
|
|
151
125
|
const steps = status.steps ?? [];
|
|
152
126
|
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
@@ -154,6 +128,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
154
128
|
return {
|
|
155
129
|
id: status.runId || path.basename(asyncDir),
|
|
156
130
|
asyncDir,
|
|
131
|
+
...(status.sessionId ? { sessionId: status.sessionId } : {}),
|
|
157
132
|
state: status.state,
|
|
158
133
|
activityState,
|
|
159
134
|
lastActivityAt,
|
|
@@ -240,6 +215,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
240
215
|
if (!status) continue;
|
|
241
216
|
const summary = statusToSummary(asyncDir, status);
|
|
242
217
|
if (allowedStates && !allowedStates.has(summary.state)) continue;
|
|
218
|
+
if (options.sessionId && summary.sessionId !== options.sessionId) continue;
|
|
243
219
|
runs.push(summary);
|
|
244
220
|
}
|
|
245
221
|
|
|
@@ -247,8 +223,9 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
247
223
|
return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
|
|
248
224
|
}
|
|
249
225
|
|
|
250
|
-
export function listAsyncRunsForOverlay(asyncDirRoot: string,
|
|
251
|
-
const
|
|
226
|
+
export function listAsyncRunsForOverlay(asyncDirRoot: string, options: AsyncRunOverlayOptions = {}): AsyncRunOverlayData {
|
|
227
|
+
const recentLimit = options.recentLimit ?? 5;
|
|
228
|
+
const all = listAsyncRuns(asyncDirRoot, { sessionId: options.sessionId });
|
|
252
229
|
const recent = all
|
|
253
230
|
.filter((run) => run.state === "complete" || run.state === "failed" || run.state === "paused")
|
|
254
231
|
.sort((a, b) => (b.lastUpdate ?? b.endedAt ?? b.startedAt) - (a.lastUpdate ?? a.endedAt ?? a.startedAt))
|
|
@@ -287,6 +264,18 @@ function formatStepLine(step: AsyncRunStepSummary): string {
|
|
|
287
264
|
return parts.join(" | ");
|
|
288
265
|
}
|
|
289
266
|
|
|
267
|
+
function formatParallelProgress(steps: Pick<AsyncRunStepSummary, "status">[], total: number, showRunning: boolean): string {
|
|
268
|
+
const running = steps.filter((step) => step.status === "running").length;
|
|
269
|
+
const done = steps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
270
|
+
const failed = steps.filter((step) => step.status === "failed").length;
|
|
271
|
+
const paused = steps.filter((step) => step.status === "paused").length;
|
|
272
|
+
const parts = [`${done}/${total} done`];
|
|
273
|
+
if (showRunning) parts.unshift(running === 1 ? "1 agent running" : `${running} agents running`);
|
|
274
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
275
|
+
if (paused > 0) parts.push(`${paused} paused`);
|
|
276
|
+
return parts.join(" · ");
|
|
277
|
+
}
|
|
278
|
+
|
|
290
279
|
export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" | "state" | "currentStep" | "chainStepCount" | "parallelGroups" | "steps">): string {
|
|
291
280
|
const stepCount = run.steps.length || 1;
|
|
292
281
|
const chainStepCount = run.chainStepCount ?? stepCount;
|
|
@@ -294,16 +283,13 @@ export function formatAsyncRunProgressLabel(run: Pick<AsyncRunSummary, "mode" |
|
|
|
294
283
|
const activeGroup = run.currentStep !== undefined
|
|
295
284
|
? groups.find((group) => run.currentStep! >= group.start && run.currentStep! < group.start + group.count)
|
|
296
285
|
: undefined;
|
|
297
|
-
if (
|
|
286
|
+
if (activeGroup) {
|
|
298
287
|
const groupSteps = run.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count);
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const groupLabel = run.state === "running"
|
|
303
|
-
? `parallel group: ${runningLabel} · ${done}/${activeGroup.count} done`
|
|
304
|
-
: `parallel group: ${done}/${activeGroup.count} done`;
|
|
305
|
-
return `step ${activeGroup.stepIndex + 1}/${chainStepCount} · ${groupLabel}`;
|
|
288
|
+
const groupLabel = formatParallelProgress(groupSteps, activeGroup.count, run.state === "running");
|
|
289
|
+
if (run.mode === "parallel") return groupLabel;
|
|
290
|
+
return `step ${activeGroup.stepIndex + 1}/${chainStepCount} · parallel group: ${groupLabel}`;
|
|
306
291
|
}
|
|
292
|
+
if (run.mode === "parallel") return formatParallelProgress(run.steps, stepCount, run.state === "running");
|
|
307
293
|
if (run.mode === "chain" && run.currentStep !== undefined && groups.length > 0) {
|
|
308
294
|
const logicalStep = flatToLogicalStepIndex(run.currentStep, chainStepCount, groups);
|
|
309
295
|
return `step ${logicalStep + 1}/${chainStepCount}`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AsyncParallelGroupStatus } from "../../shared/types.ts";
|
|
2
|
+
|
|
3
|
+
function isValidParallelGroup(group: unknown, stepCount: number, chainStepCount: number): group is AsyncParallelGroupStatus {
|
|
4
|
+
if (typeof group !== "object" || group === null) return false;
|
|
5
|
+
const { start, count, stepIndex } = group as Partial<AsyncParallelGroupStatus>;
|
|
6
|
+
return typeof start === "number"
|
|
7
|
+
&& typeof count === "number"
|
|
8
|
+
&& typeof stepIndex === "number"
|
|
9
|
+
&& Number.isInteger(start)
|
|
10
|
+
&& Number.isInteger(count)
|
|
11
|
+
&& Number.isInteger(stepIndex)
|
|
12
|
+
&& start >= 0
|
|
13
|
+
&& count > 0
|
|
14
|
+
&& stepIndex >= 0
|
|
15
|
+
&& stepIndex < chainStepCount
|
|
16
|
+
&& start + count <= stepCount;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeParallelGroups(groups: unknown, stepCount: number, chainStepCount: number): AsyncParallelGroupStatus[] {
|
|
20
|
+
if (!Array.isArray(groups)) return [];
|
|
21
|
+
return groups
|
|
22
|
+
.filter((group): group is AsyncParallelGroupStatus => isValidParallelGroup(group, stepCount, chainStepCount))
|
|
23
|
+
.sort((left, right) => left.stepIndex - right.stepIndex || left.start - right.start);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function flatToLogicalStepIndex(flatIndex: number, chainStepCount: number, groups: AsyncParallelGroupStatus[]): number {
|
|
27
|
+
let logicalIndex = 0;
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
for (const group of groups) {
|
|
30
|
+
while (cursor < group.start && logicalIndex < chainStepCount) {
|
|
31
|
+
if (cursor === flatIndex) return logicalIndex;
|
|
32
|
+
cursor++;
|
|
33
|
+
logicalIndex++;
|
|
34
|
+
}
|
|
35
|
+
if (flatIndex >= group.start && flatIndex < group.start + group.count) return group.stepIndex;
|
|
36
|
+
cursor = group.start + group.count;
|
|
37
|
+
logicalIndex = group.stepIndex + 1;
|
|
38
|
+
}
|
|
39
|
+
while (cursor <= flatIndex && logicalIndex < chainStepCount) {
|
|
40
|
+
if (cursor === flatIndex) return logicalIndex;
|
|
41
|
+
cursor++;
|
|
42
|
+
logicalIndex++;
|
|
43
|
+
}
|
|
44
|
+
return Math.max(0, chainStepCount - 1);
|
|
45
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
3
|
import { buildCompletionKey, markSeenWithTtl } from "./completion-dedupe.ts";
|
|
5
4
|
import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
6
5
|
import {
|
|
7
6
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
7
|
+
type IntercomEventBus,
|
|
8
8
|
type SubagentState,
|
|
9
9
|
} from "../../shared/types.ts";
|
|
10
10
|
import {
|
|
@@ -46,7 +46,7 @@ function shouldFallBackToPolling(error: unknown): boolean {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export function createResultWatcher(
|
|
49
|
-
pi:
|
|
49
|
+
pi: { events: IntercomEventBus },
|
|
50
50
|
state: SubagentState,
|
|
51
51
|
resultsDir: string,
|
|
52
52
|
completionTtlMs: number,
|