pi-subagents 0.40.0 → 0.41.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 +76 -0
- package/README.md +246 -525
- package/agents/oracle.md +1 -0
- package/package.json +12 -4
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +6 -6
- package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
- package/skills/pi-subagents/references/execution-controls.md +98 -97
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
- package/src/agents/agent-management.ts +155 -65
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +154 -71
- package/src/agents/chain-serializer.ts +10 -7
- package/src/agents/frontmatter.ts +5 -3
- package/src/agents/identity.ts +1 -1
- package/src/agents/proactive-skills.ts +13 -10
- package/src/agents/skills.ts +23 -6
- package/src/api/control-channel.ts +4 -0
- package/src/api/delegation.ts +26 -194
- package/src/api/external-runs.ts +129 -0
- package/src/api/intercom-bridge.ts +3 -0
- package/src/api/pi-args.ts +5 -0
- package/src/api/preflight.ts +3 -3
- package/src/api/shared-types.ts +19 -0
- package/src/extension/config.ts +10 -0
- package/src/extension/control-notices.ts +5 -39
- package/src/extension/doctor.ts +10 -9
- package/src/extension/fanout-child.ts +7 -4
- package/src/extension/index.ts +232 -68
- package/src/extension/rpc.ts +18 -12
- package/src/extension/schemas.ts +48 -37
- package/src/extension/tool-description.ts +36 -86
- package/src/inspectors/herdr/actions.ts +229 -0
- package/src/inspectors/herdr/client.ts +130 -0
- package/src/inspectors/herdr/inspector-runner.ts +141 -0
- package/src/inspectors/herdr/project-panes.ts +154 -0
- package/src/integrations/herdr-status.ts +330 -0
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/result-intercom.ts +5 -1
- package/src/missions/actions.ts +372 -0
- package/src/missions/lifecycle.ts +314 -0
- package/src/missions/store.ts +442 -0
- package/src/missions/types.ts +135 -0
- package/src/policy/authority.ts +46 -0
- package/src/profiles/profiles.ts +29 -3
- package/src/runs/background/async-execution.ts +98 -49
- package/src/runs/background/async-job-tracker.ts +10 -2
- package/src/runs/background/async-resume.ts +6 -6
- package/src/runs/background/async-status.ts +29 -1
- package/src/runs/background/auto-drain.ts +3 -3
- package/src/runs/background/chain-append.ts +3 -2
- package/src/runs/background/control-channel.ts +9 -7
- package/src/runs/background/fleet-view.ts +3 -4
- package/src/runs/background/notify.ts +2 -1
- package/src/runs/background/process-terminal.ts +5 -5
- package/src/runs/background/result-watcher.ts +13 -5
- package/src/runs/background/run-id-resolver.ts +3 -3
- package/src/runs/background/run-status.ts +35 -8
- package/src/runs/background/scheduled-runs.ts +602 -375
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +608 -445
- package/src/runs/background/subagent-wait.ts +50 -9
- package/src/runs/background/wait-subscriptions.ts +253 -0
- package/src/runs/background/wait-tool.ts +12 -4
- package/src/runs/foreground/async-steering-action.ts +3 -3
- package/src/runs/foreground/chain-clarify.ts +8 -4
- package/src/runs/foreground/chain-execution.ts +56 -30
- package/src/runs/foreground/execution.ts +15 -2
- package/src/runs/foreground/subagent-executor.ts +1023 -273
- package/src/runs/shared/acceptance.ts +28 -6
- package/src/runs/shared/child-protocol.ts +302 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/external-cli-runner.ts +130 -0
- package/src/runs/shared/long-running-guard.ts +42 -1
- package/src/runs/shared/nested-events.ts +59 -5
- package/src/runs/shared/nested-render.ts +9 -4
- package/src/runs/shared/parallel-handoff.ts +86 -2
- package/src/runs/shared/parallel-utils.ts +11 -2
- package/src/runs/shared/permissions.ts +95 -0
- package/src/runs/shared/pi-args.ts +11 -1
- package/src/runs/shared/pi-spawn.ts +11 -1
- package/src/runs/shared/run-history.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
- package/src/runs/shared/subagent-startup-retry.ts +5 -2
- package/src/runs/shared/turn-budget.ts +6 -6
- package/src/runs/shared/worktree.ts +122 -12
- package/src/shared/accessible-dir.ts +29 -7
- package/src/shared/artifacts.ts +18 -1
- package/src/shared/fork-context.ts +3 -2
- package/src/shared/launch-contract.ts +1 -0
- package/src/shared/settings.ts +10 -0
- package/src/shared/types.ts +156 -14
- package/src/shared/utils.ts +8 -6
- package/src/slash/delegation-adapters.ts +32 -194
- package/src/slash/delegation-request.ts +43 -126
- package/src/slash/prompt-template-bridge.ts +158 -205
- package/src/slash/prompt-workflows.ts +21 -57
- package/src/slash/slash-bridge.ts +14 -0
- package/src/slash/slash-commands.ts +31 -632
- package/src/slash/subagents-admin.ts +18 -14
- package/src/tui/fleet-status.ts +156 -21
- package/src/tui/fleet-transcript.ts +110 -5
- package/src/tui/fleet.ts +56 -24
- package/src/tui/render.ts +291 -109
- package/src/types/pi-runtime-compat.d.ts +14 -0
- package/src/watchdog/lsp-diagnostics.ts +12 -7
- package/src/watchdog/model-selection.ts +2 -2
- package/src/watchdog/permission-arbiter.ts +145 -0
- package/src/watchdog/register-child.ts +1 -1
- package/src/watchdog/register-main.ts +1 -1
- package/src/watchdog/review.ts +4 -1
- package/src/watchdog/runtime.ts +3 -2
- package/src/workflows/chat-progress.ts +140 -0
- package/src/workflows/scripted-workflow.ts +415 -0
- package/agents/advisor.md +0 -73
- package/src/extension/chain-validation.ts +0 -181
package/src/extension/index.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* - Sync (default): Streams output, renders markdown, tracks usage
|
|
6
6
|
* - Async: Background execution, emits events when done
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* Toggle: async parameter (default: false
|
|
8
|
+
* Public execution modes: single (agent + task) and workflow (workflowScript)
|
|
9
|
+
* Toggle: async parameter (default: true; set asyncByDefault:false in config.json to opt out)
|
|
10
10
|
*
|
|
11
11
|
* Config file: ~/.pi/agent/extensions/subagent/config.json
|
|
12
12
|
* { "asyncByDefault": true, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
@@ -24,11 +24,10 @@ import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
|
|
|
24
24
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
25
25
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
26
26
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
27
|
-
import { clearLegacyResultAnimationTimer, renderSubagentResult } from "../tui/render.ts";
|
|
27
|
+
import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
|
|
28
28
|
import { openSubagentFleet } from "../tui/fleet.ts";
|
|
29
29
|
import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-status.ts";
|
|
30
30
|
import { SubagentParams } from "./schemas.ts";
|
|
31
|
-
import { validateChainInput } from "./chain-validation.ts";
|
|
32
31
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
33
32
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
34
33
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
@@ -38,25 +37,27 @@ import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template
|
|
|
38
37
|
import { registerMainWatchdog } from "../watchdog/register-main.ts";
|
|
39
38
|
import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
|
|
40
39
|
import { createNativeSupervisorChannel } from "../intercom/native-supervisor-channel.ts";
|
|
40
|
+
import { registerHerdrStatusBridge } from "../integrations/herdr-status.ts";
|
|
41
41
|
import { registerSubagentRpcBridge } from "./rpc.ts";
|
|
42
42
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
|
|
43
43
|
import { inspectSubagentStatus } from "../runs/background/run-status.ts";
|
|
44
44
|
import { resolveWaitToolConfig } from "../runs/background/subagent-wait.ts";
|
|
45
45
|
import { registerWaitTool } from "../runs/background/wait-tool.ts";
|
|
46
|
+
import { createWaitSubscriptionManager } from "../runs/background/wait-subscriptions.ts";
|
|
46
47
|
import { drainOutstandingWork } from "../runs/background/auto-drain.ts";
|
|
47
48
|
import registerSubagentNotify, { parseSubagentNotifyContent, type SubagentNotifyDetails } from "../runs/background/notify.ts";
|
|
48
49
|
import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_MESSAGE_TYPE, type SubagentSteeringMessageDetails } from "./steering-notices.ts";
|
|
49
50
|
import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
|
|
50
51
|
import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
51
52
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
52
|
-
import { loadConfig } from "./config.ts";
|
|
53
|
+
import { loadConfig, resolveAsyncByDefault } from "./config.ts";
|
|
53
54
|
import { buildSubagentToolDescription } from "./tool-description.ts";
|
|
55
|
+
import { syncMissionFromAsyncCompletion } from "../missions/lifecycle.ts";
|
|
54
56
|
import {
|
|
55
57
|
type Details,
|
|
56
58
|
type SubagentState,
|
|
57
|
-
|
|
59
|
+
DIRS,
|
|
58
60
|
DEFAULT_ARTIFACT_CONFIG,
|
|
59
|
-
RESULTS_DIR,
|
|
60
61
|
SLASH_RESULT_TYPE,
|
|
61
62
|
SLASH_TEXT_RESULT_TYPE,
|
|
62
63
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
@@ -67,14 +68,146 @@ import {
|
|
|
67
68
|
resolveMaxSubagentSpawnsPerSession,
|
|
68
69
|
} from "../shared/types.ts";
|
|
69
70
|
import {
|
|
70
|
-
clearPendingForegroundControlNotices,
|
|
71
71
|
formatSubagentControlNotice,
|
|
72
72
|
handleSubagentControlNotice,
|
|
73
73
|
SUBAGENT_CONTROL_MESSAGE_TYPE,
|
|
74
74
|
type SubagentControlMessageDetails,
|
|
75
75
|
} from "./control-notices.ts";
|
|
76
76
|
|
|
77
|
-
export { loadConfig } from "./config.ts";
|
|
77
|
+
export { loadConfig, resolveAsyncByDefault } from "./config.ts";
|
|
78
|
+
|
|
79
|
+
function workflowLaneKeys(script: string): string[] {
|
|
80
|
+
const keys: string[] = [];
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
const add = (key: string): void => {
|
|
83
|
+
if (!seen.has(key)) {
|
|
84
|
+
seen.add(key);
|
|
85
|
+
keys.push(key);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const isIdentifier = (char: string | undefined): boolean => char !== undefined && /[\w$]/.test(char);
|
|
89
|
+
const skipTrivia = (start: number): number => {
|
|
90
|
+
let index = start;
|
|
91
|
+
while (index < script.length) {
|
|
92
|
+
if (/\s/.test(script[index]!)) index += 1;
|
|
93
|
+
else if (script.startsWith("//", index)) {
|
|
94
|
+
const end = script.indexOf("\n", index + 2);
|
|
95
|
+
index = end === -1 ? script.length : end + 1;
|
|
96
|
+
} else if (script.startsWith("/*", index)) {
|
|
97
|
+
const end = script.indexOf("*/", index + 2);
|
|
98
|
+
index = end === -1 ? script.length : end + 2;
|
|
99
|
+
} else break;
|
|
100
|
+
}
|
|
101
|
+
return index;
|
|
102
|
+
};
|
|
103
|
+
const readLiteral = (start: number): { key?: string; end: number } | undefined => {
|
|
104
|
+
const quote = script[start];
|
|
105
|
+
if (quote !== "'" && quote !== '"' && quote !== "`") return undefined;
|
|
106
|
+
let index = start + 1;
|
|
107
|
+
let dynamicTemplate = false;
|
|
108
|
+
while (index < script.length) {
|
|
109
|
+
if (script[index] === "\\") {
|
|
110
|
+
index += 2;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (quote === "`" && script.startsWith("${", index)) dynamicTemplate = true;
|
|
114
|
+
if (script[index] === quote) return { key: dynamicTemplate ? undefined : script.slice(start + 1, index), end: index + 1 };
|
|
115
|
+
if (quote !== "`" && /[\r\n]/.test(script[index]!)) return { end: index + 1 };
|
|
116
|
+
index += 1;
|
|
117
|
+
}
|
|
118
|
+
return { end: script.length };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const collectRunsAllKeys = (start: number): number => {
|
|
122
|
+
let index = skipTrivia(start);
|
|
123
|
+
if (script[index] !== "(") return start;
|
|
124
|
+
index = skipTrivia(index + 1);
|
|
125
|
+
if (script[index] !== "[") return start;
|
|
126
|
+
let arrayDepth = 1;
|
|
127
|
+
let objectDepth = 0;
|
|
128
|
+
let directChildObject = false;
|
|
129
|
+
let expectingElement = true;
|
|
130
|
+
for (index += 1; index < script.length; index += 1) {
|
|
131
|
+
index = skipTrivia(index);
|
|
132
|
+
const literal = readLiteral(index);
|
|
133
|
+
if (literal) {
|
|
134
|
+
index = literal.end - 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (script[index] === "[") {
|
|
138
|
+
arrayDepth += 1;
|
|
139
|
+
expectingElement = false;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (script[index] === "]") {
|
|
143
|
+
arrayDepth -= 1;
|
|
144
|
+
if (arrayDepth === 0) return index + 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (script[index] === "{") {
|
|
148
|
+
objectDepth += 1;
|
|
149
|
+
if (objectDepth === 1) directChildObject = arrayDepth === 1 && expectingElement;
|
|
150
|
+
expectingElement = false;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (script[index] === "}") {
|
|
154
|
+
objectDepth -= 1;
|
|
155
|
+
if (objectDepth === 0) directChildObject = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (script[index] === "," && arrayDepth === 1 && objectDepth === 0) {
|
|
159
|
+
expectingElement = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (directChildObject && objectDepth === 1 && !isIdentifier(script[index - 1]) && script.startsWith("key", index) && !isIdentifier(script[index + 3])) {
|
|
163
|
+
const colon = skipTrivia(index + 3);
|
|
164
|
+
const key = script[colon] === ":" ? readLiteral(skipTrivia(colon + 1)) : undefined;
|
|
165
|
+
if (key) {
|
|
166
|
+
const next = skipTrivia(key.end);
|
|
167
|
+
if (key.key !== undefined && (script[next] === "," || script[next] === "}")) add(key.key);
|
|
168
|
+
index = key.end - 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return index;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
for (let index = 0; index < script.length;) {
|
|
176
|
+
index = skipTrivia(index);
|
|
177
|
+
const literal = readLiteral(index);
|
|
178
|
+
if (literal) {
|
|
179
|
+
index = literal.end;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!isIdentifier(script[index - 1]) && script.startsWith("runs.run", index) && !isIdentifier(script[index + 8])) {
|
|
183
|
+
const open = skipTrivia(index + 8);
|
|
184
|
+
const key = script[open] === "(" ? readLiteral(skipTrivia(open + 1)) : undefined;
|
|
185
|
+
if (key) {
|
|
186
|
+
const next = skipTrivia(key.end);
|
|
187
|
+
if (key.key !== undefined && (script[next] === "," || script[next] === ")")) add(key.key);
|
|
188
|
+
index = key.end;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!isIdentifier(script[index - 1]) && script.startsWith("runs.all", index) && !isIdentifier(script[index + 8])) {
|
|
193
|
+
index = collectRunsAllKeys(index + 8);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
index += 1;
|
|
197
|
+
}
|
|
198
|
+
return keys;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatWorkflowManifest(script: string, async: unknown, clarify: unknown): string {
|
|
202
|
+
if (clarify === true) return "workflow script · rejected: clarify UI unsupported";
|
|
203
|
+
const keys = workflowLaneKeys(script);
|
|
204
|
+
// The workflow executor starts background work unless callers pass async:false.
|
|
205
|
+
const mode = async === false ? "foreground" : "background";
|
|
206
|
+
if (keys.length === 0) return `workflow script · ${mode}`;
|
|
207
|
+
const visibleKeys = keys.slice(0, 4).join(", ");
|
|
208
|
+
const remainder = keys.length > 4 ? `, +${keys.length - 4}` : "";
|
|
209
|
+
return `workflow · ${mode} · ${keys.length} lane${keys.length === 1 ? "" : "s"}: ${visibleKeys}${remainder}`;
|
|
210
|
+
}
|
|
78
211
|
|
|
79
212
|
/**
|
|
80
213
|
* Derive subagent session base directory from parent session file.
|
|
@@ -188,16 +321,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
188
321
|
}
|
|
189
322
|
}
|
|
190
323
|
|
|
191
|
-
ensureAccessibleDir(
|
|
192
|
-
ensureAccessibleDir(
|
|
324
|
+
DIRS.results = ensureAccessibleDir(DIRS.results);
|
|
325
|
+
DIRS.async = ensureAccessibleDir(DIRS.async);
|
|
193
326
|
cleanupOldChainDirs();
|
|
194
327
|
|
|
195
328
|
const config = loadConfig();
|
|
196
329
|
const waitToolConfig = resolveWaitToolConfig(config.waitTool);
|
|
197
|
-
const asyncByDefault = config
|
|
330
|
+
const asyncByDefault = resolveAsyncByDefault(config);
|
|
198
331
|
const fleetViewEnabled = config.fleetView !== false;
|
|
199
332
|
const fleetViewPlacement = resolveFleetViewPlacement(config.fleetViewPlacement);
|
|
200
|
-
const asyncWidgetEnabled = config.asyncWidget
|
|
333
|
+
const asyncWidgetEnabled = config.asyncWidget !== false;
|
|
334
|
+
const summaryInlineToolDisplay = config.inlineToolDisplay === "summary";
|
|
201
335
|
const tempArtifactsDir = getArtifactsDir(null);
|
|
202
336
|
cleanupAllArtifactDirs(DEFAULT_ARTIFACT_CONFIG.cleanupDays);
|
|
203
337
|
|
|
@@ -205,6 +339,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
205
339
|
baseCwd: "",
|
|
206
340
|
currentSessionId: null,
|
|
207
341
|
artifactDirPreference: config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
|
|
342
|
+
...(config.authorityPolicy ? { authorityPolicy: config.authorityPolicy } : {}),
|
|
343
|
+
...(config.missions ? { missionStoreConfig: config.missions } : {}),
|
|
208
344
|
parentSessionFile: null,
|
|
209
345
|
subagentInProgress: false,
|
|
210
346
|
subagentSpawns: {
|
|
@@ -219,7 +355,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
219
355
|
foregroundRuns: new Map(),
|
|
220
356
|
foregroundControls: new Map(),
|
|
221
357
|
lastForegroundControlId: null,
|
|
222
|
-
pendingForegroundControlNotices: new Map(),
|
|
223
358
|
cleanupTimers: new Map(),
|
|
224
359
|
lastUiContext: null,
|
|
225
360
|
poller: null,
|
|
@@ -233,23 +368,40 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
233
368
|
};
|
|
234
369
|
|
|
235
370
|
const supervisorChannel = createNativeSupervisorChannel(pi, state);
|
|
371
|
+
const waitSubscriptionManager = createWaitSubscriptionManager(pi, state);
|
|
236
372
|
const mainWatchdog = registerMainWatchdog(pi);
|
|
237
373
|
const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch });
|
|
238
374
|
const fleetStatus = fleetViewEnabled
|
|
239
375
|
? new SubagentFleetStatus(state, async (itemKey) => {
|
|
240
376
|
const ctx = state.lastUiContext;
|
|
241
377
|
if (!ctx?.hasUI) return;
|
|
242
|
-
await openSubagentFleet(ctx, state, { initialKey: itemKey });
|
|
378
|
+
await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results });
|
|
243
379
|
}, { placement: fleetViewPlacement })
|
|
244
380
|
: undefined;
|
|
381
|
+
let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
|
|
382
|
+
const scheduledRunManager = createScheduledRunManager({
|
|
383
|
+
config,
|
|
384
|
+
launch: (params, ctx, signal) => {
|
|
385
|
+
if (!executorScheduled) {
|
|
386
|
+
return Promise.resolve({
|
|
387
|
+
content: [{ type: "text", text: "Scheduled subagent launch is unavailable (executor not ready)." }],
|
|
388
|
+
isError: true,
|
|
389
|
+
details: { mode: "management" as const, results: [] },
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return executorScheduled(randomUUID(), params, signal, ctx);
|
|
393
|
+
},
|
|
394
|
+
resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
|
|
395
|
+
});
|
|
245
396
|
const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
|
|
246
397
|
pi,
|
|
247
398
|
state,
|
|
248
|
-
|
|
399
|
+
DIRS.results,
|
|
249
400
|
10 * 60 * 1000,
|
|
250
401
|
{
|
|
251
402
|
notifier: completionNotifier,
|
|
252
|
-
|
|
403
|
+
observeCompletion: (result) => scheduledRunManager.handleAsyncCompletion(result),
|
|
404
|
+
deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
|
|
253
405
|
},
|
|
254
406
|
);
|
|
255
407
|
|
|
@@ -260,8 +412,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
260
412
|
mainWatchdog.dispose();
|
|
261
413
|
scheduledRunManager.stop();
|
|
262
414
|
supervisorChannel.dispose();
|
|
415
|
+
waitSubscriptionManager.dispose();
|
|
263
416
|
fleetStatus?.dispose();
|
|
264
|
-
clearPendingForegroundControlNotices(state);
|
|
265
417
|
if (state.poller) {
|
|
266
418
|
clearInterval(state.poller);
|
|
267
419
|
state.poller = null;
|
|
@@ -269,24 +421,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
269
421
|
};
|
|
270
422
|
globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
|
|
271
423
|
|
|
272
|
-
const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs } = createAsyncJobTracker(pi, state,
|
|
424
|
+
const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs } = createAsyncJobTracker(pi, state, DIRS.async, {
|
|
273
425
|
widgetEnabled: asyncWidgetEnabled,
|
|
274
426
|
});
|
|
275
|
-
let executorExecute: ((id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
|
|
276
|
-
const scheduledRunManager = createScheduledRunManager({
|
|
277
|
-
config,
|
|
278
|
-
launch: (params, ctx, signal) => {
|
|
279
|
-
if (!executorExecute) {
|
|
280
|
-
return Promise.resolve({
|
|
281
|
-
content: [{ type: "text", text: "Scheduled subagent launch is unavailable (executor not ready)." }],
|
|
282
|
-
isError: true,
|
|
283
|
-
details: { mode: "management" as const, results: [] },
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
return executorExecute(randomUUID(), params, signal, undefined, ctx);
|
|
287
|
-
},
|
|
288
|
-
resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
|
|
289
|
-
});
|
|
290
427
|
const executor = createSubagentExecutor({
|
|
291
428
|
pi,
|
|
292
429
|
state,
|
|
@@ -300,7 +437,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
300
437
|
expandTilde,
|
|
301
438
|
discoverAgents,
|
|
302
439
|
});
|
|
303
|
-
|
|
440
|
+
executorScheduled = executor.executeScheduled;
|
|
304
441
|
|
|
305
442
|
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
306
443
|
const details = resolveSlashMessageDetails(message.details);
|
|
@@ -379,7 +516,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
379
516
|
getContext: () => state.lastUiContext,
|
|
380
517
|
execute: (requestId, params, signal, ctx, onUpdate) =>
|
|
381
518
|
executeSubagentCollapsed(requestId, params, signal, onUpdate, ctx),
|
|
382
|
-
|
|
519
|
+
executeStructured: (requestId, params, signal, ctx, onUpdate) => {
|
|
383
520
|
if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
|
|
384
521
|
return executor.executeDelegated(requestId, params, signal, onUpdate, ctx);
|
|
385
522
|
},
|
|
@@ -392,13 +529,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
392
529
|
state,
|
|
393
530
|
});
|
|
394
531
|
|
|
395
|
-
function effectiveParallelTaskCount(tasks: Array<{ count?: unknown }> | undefined): number {
|
|
396
|
-
if (!tasks || tasks.length === 0) return 0;
|
|
397
|
-
return tasks.reduce((total, task) => {
|
|
398
|
-
const count = typeof task.count === "number" && Number.isInteger(task.count) && task.count >= 1 ? task.count : 1;
|
|
399
|
-
return total + count;
|
|
400
|
-
}, 0);
|
|
401
|
-
}
|
|
402
532
|
|
|
403
533
|
const tool: ToolDefinition<typeof SubagentParams, Details> = {
|
|
404
534
|
name: "subagent",
|
|
@@ -406,16 +536,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
406
536
|
description: buildSubagentToolDescription(config),
|
|
407
537
|
parameters: SubagentParams,
|
|
408
538
|
|
|
409
|
-
prepareArguments(args) {
|
|
410
|
-
// Run friendly chain validation before pi-ai's raw TypeBox schema check
|
|
411
|
-
// so the model sees which property is disallowed, what is allowed, and a
|
|
412
|
-
// valid example instead of `chain.N: must not have additional properties`.
|
|
413
|
-
validateChainInput(args);
|
|
414
|
-
return args as never;
|
|
415
|
-
},
|
|
416
|
-
|
|
417
539
|
execute(id, params, signal, onUpdate, ctx) {
|
|
418
|
-
|
|
540
|
+
const input = params as SubagentParamsLike;
|
|
541
|
+
if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
|
|
542
|
+
return Promise.resolve({ content: [{ type: "text", text: "Legacy top-level chain and parallel inputs were removed; use workflowScript." }], isError: true, details: { mode: "management", results: [] } });
|
|
543
|
+
}
|
|
544
|
+
return executeSubagentCollapsed(id, input, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
419
545
|
},
|
|
420
546
|
|
|
421
547
|
renderCall(args, theme) {
|
|
@@ -426,21 +552,13 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
426
552
|
0, 0,
|
|
427
553
|
);
|
|
428
554
|
}
|
|
429
|
-
|
|
430
|
-
const parallelCount = effectiveParallelTaskCount(args.tasks as Array<{ count?: unknown }> | undefined);
|
|
431
|
-
const asyncLabel = args.async === true && args.clarify !== true ? theme.fg("warning", " [async]") : "";
|
|
432
|
-
if (args.chain?.length)
|
|
433
|
-
return new Text(
|
|
434
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}chain (${args.chain.length})${asyncLabel}`,
|
|
435
|
-
0,
|
|
436
|
-
0,
|
|
437
|
-
);
|
|
438
|
-
if (isParallel)
|
|
555
|
+
if (args.workflowScript)
|
|
439
556
|
return new Text(
|
|
440
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}
|
|
557
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${formatWorkflowManifest(args.workflowScript, args.async, args.clarify)}`,
|
|
441
558
|
0,
|
|
442
559
|
0,
|
|
443
560
|
);
|
|
561
|
+
const asyncLabel = args.async === true && args.clarify !== true ? theme.fg("warning", " [async]") : "";
|
|
444
562
|
return new Text(
|
|
445
563
|
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
|
|
446
564
|
0,
|
|
@@ -450,14 +568,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
450
568
|
|
|
451
569
|
renderResult(result, options, theme, context) {
|
|
452
570
|
clearLegacyResultAnimationTimer(context);
|
|
453
|
-
|
|
571
|
+
const renderedResult = { ...result, isError: context.isError };
|
|
572
|
+
return summaryInlineToolDisplay
|
|
573
|
+
? renderSubagentSummary(renderedResult, options, theme)
|
|
574
|
+
: renderSubagentResult(renderedResult, options, theme);
|
|
454
575
|
},
|
|
455
576
|
|
|
456
577
|
};
|
|
457
578
|
|
|
458
579
|
pi.registerTool(tool);
|
|
459
580
|
|
|
460
|
-
registerWaitTool(pi, state, waitToolConfig.enabled);
|
|
581
|
+
registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager);
|
|
461
582
|
|
|
462
583
|
pi.on("agent_end", async (_event, ctx) => {
|
|
463
584
|
if (ctx.hasUI) return;
|
|
@@ -482,6 +603,20 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
482
603
|
const existingVisibleControlNotices = globalStore[controlNoticeSeenStoreKey];
|
|
483
604
|
const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
|
|
484
605
|
globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
|
|
606
|
+
const activeHerdrRuns = () => [...state.asyncJobs.values()]
|
|
607
|
+
.filter((job) => job.status === "queued" || job.status === "running")
|
|
608
|
+
.map((job) => ({
|
|
609
|
+
id: job.asyncId,
|
|
610
|
+
agents: job.agents,
|
|
611
|
+
needsAttention: job.activityState === "needs_attention",
|
|
612
|
+
}));
|
|
613
|
+
const herdrStatusBridge = registerHerdrStatusBridge({
|
|
614
|
+
events: pi.events,
|
|
615
|
+
getRuns: activeHerdrRuns,
|
|
616
|
+
async runHerdr(args) {
|
|
617
|
+
await pi.exec(process.env.HERDR_BIN || "herdr", [...args], { timeout: 5_000 });
|
|
618
|
+
},
|
|
619
|
+
});
|
|
485
620
|
const controlEventHandler = (payload: unknown) => {
|
|
486
621
|
handleSubagentControlNotice({
|
|
487
622
|
pi,
|
|
@@ -499,6 +634,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
499
634
|
};
|
|
500
635
|
const asyncCompleteHandler = (payload: unknown) => {
|
|
501
636
|
handleComplete(payload);
|
|
637
|
+
scheduledRunManager.handleAsyncCompletion(payload);
|
|
638
|
+
try {
|
|
639
|
+
syncMissionFromAsyncCompletion(payload);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
console.error("Failed to update mission from async completion:", error);
|
|
642
|
+
}
|
|
502
643
|
fleetStatus?.refresh();
|
|
503
644
|
};
|
|
504
645
|
const eventUnsubscribes = [
|
|
@@ -506,6 +647,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
506
647
|
pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
|
|
507
648
|
pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
|
|
508
649
|
pi.events.on(SUBAGENT_STEERING_NOTICE_EVENT, steeringNoticeHandler),
|
|
650
|
+
herdrStatusBridge.dispose,
|
|
509
651
|
rpcBridge.dispose,
|
|
510
652
|
];
|
|
511
653
|
globalStore[eventUnsubscribeStoreKey] = eventUnsubscribes;
|
|
@@ -514,6 +656,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
514
656
|
if (event.toolName !== "subagent") return;
|
|
515
657
|
if (!ctx.hasUI) return;
|
|
516
658
|
state.lastUiContext = ctx;
|
|
659
|
+
restoreActiveJobs(ctx);
|
|
517
660
|
fleetStatus?.setContext(ctx);
|
|
518
661
|
fleetStatus?.refresh();
|
|
519
662
|
if (state.asyncJobs.size > 0) {
|
|
@@ -557,26 +700,47 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
557
700
|
}
|
|
558
701
|
state.lastUiContext = ctx;
|
|
559
702
|
cleanupSessionArtifacts(ctx);
|
|
560
|
-
clearPendingForegroundControlNotices(state);
|
|
561
703
|
state.foregroundControls.clear();
|
|
562
704
|
state.lastForegroundControlId = null;
|
|
563
705
|
resetJobs(ctx);
|
|
564
706
|
restoreActiveJobs(ctx);
|
|
565
707
|
scheduledRunManager.bindSession(ctx);
|
|
566
708
|
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
709
|
+
waitSubscriptionManager.restore();
|
|
567
710
|
startResultWatcher();
|
|
568
711
|
primeExistingResults({ triggerTurn: !recovering });
|
|
569
712
|
fleetStatus?.setContext(ctx);
|
|
570
713
|
};
|
|
571
714
|
|
|
715
|
+
pi.on("agent_start", () => {
|
|
716
|
+
herdrStatusBridge.agentStarted();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
pi.on("session_compact", () => {
|
|
720
|
+
const hasActiveAsyncWork = [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
|
|
721
|
+
if (!hasActiveAsyncWork || state.lastUiContext?.hasUI !== true) return;
|
|
722
|
+
pi.sendMessage(
|
|
723
|
+
{
|
|
724
|
+
customType: "subagent-compaction-resume",
|
|
725
|
+
content: "Compaction is complete. Resume the parent task now; background subagent results will arrive separately when ready.",
|
|
726
|
+
display: false,
|
|
727
|
+
},
|
|
728
|
+
{ triggerTurn: true },
|
|
729
|
+
);
|
|
730
|
+
});
|
|
731
|
+
|
|
572
732
|
pi.on("session_start", (event, ctx) => {
|
|
573
733
|
const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
|
|
574
734
|
resetSessionState(ctx, recovering);
|
|
735
|
+
herdrStatusBridge.sessionStarted({
|
|
736
|
+
hasUI: ctx.hasUI === true,
|
|
737
|
+
runs: activeHerdrRuns(),
|
|
738
|
+
});
|
|
575
739
|
rpcBridge.emitReady(ctx);
|
|
576
740
|
supervisorChannel.start();
|
|
577
741
|
});
|
|
578
742
|
|
|
579
|
-
pi.on("session_shutdown", () => {
|
|
743
|
+
pi.on("session_shutdown", async () => {
|
|
580
744
|
stopResultWatcher();
|
|
581
745
|
state.currentSessionId = null;
|
|
582
746
|
state.parentSessionFile = null;
|
|
@@ -595,7 +759,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
595
759
|
scheduledRunManager.stop();
|
|
596
760
|
if (state.poller) clearInterval(state.poller);
|
|
597
761
|
state.poller = null;
|
|
598
|
-
clearPendingForegroundControlNotices(state);
|
|
599
762
|
for (const timer of state.cleanupTimers.values()) {
|
|
600
763
|
clearTimeout(timer);
|
|
601
764
|
}
|
|
@@ -618,5 +781,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
618
781
|
} catch (error) {
|
|
619
782
|
if (!isStaleExtensionContextError(error)) throw error;
|
|
620
783
|
}
|
|
784
|
+
await herdrStatusBridge.flush();
|
|
621
785
|
});
|
|
622
786
|
}
|
package/src/extension/rpc.ts
CHANGED
|
@@ -11,15 +11,14 @@ import {
|
|
|
11
11
|
type AsyncJobStep,
|
|
12
12
|
type Details,
|
|
13
13
|
type SubagentState,
|
|
14
|
-
|
|
15
|
-
RESULTS_DIR,
|
|
14
|
+
DIRS,
|
|
16
15
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
17
16
|
SUBAGENT_PROCESS_TERMINAL_EVENT,
|
|
18
17
|
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
19
18
|
} from "../shared/types.ts";
|
|
20
19
|
import { readStatus } from "../shared/utils.ts";
|
|
21
20
|
import { SubagentParams } from "./schemas.ts";
|
|
22
|
-
import {
|
|
21
|
+
import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
|
|
23
22
|
|
|
24
23
|
export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
|
|
25
24
|
export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
|
|
@@ -193,6 +192,17 @@ function buildFleetStatus(
|
|
|
193
192
|
for (const job of state.asyncJobs.values()) {
|
|
194
193
|
if (job.sessionId !== authoritativeSessionId || !activeState(job.status)) continue;
|
|
195
194
|
const startedAt = job.startedAt ?? job.updatedAt;
|
|
195
|
+
if (job.mode === "workflow") {
|
|
196
|
+
const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
|
|
197
|
+
addCandidate({
|
|
198
|
+
internalKey: `async:${job.asyncId}`,
|
|
199
|
+
agent: "workflow",
|
|
200
|
+
startedAt,
|
|
201
|
+
tokens: job.totalTokens,
|
|
202
|
+
goal: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
|
|
203
|
+
});
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
196
206
|
const steps: AsyncJobStep[] | undefined = job.steps?.length
|
|
197
207
|
? job.steps
|
|
198
208
|
: job.agents?.map((agent, index) => ({
|
|
@@ -318,13 +328,6 @@ function assertRecordParams(params: unknown, method: SubagentRpcMethod): Record<
|
|
|
318
328
|
}
|
|
319
329
|
|
|
320
330
|
function assertSubagentParams(params: SubagentParamsLike, label: string): void {
|
|
321
|
-
// Friendly chain validation first: name the disallowed property, list allowed
|
|
322
|
-
// ones, and show a valid example instead of raw TypeBox diagnostics.
|
|
323
|
-
try {
|
|
324
|
-
validateChainInput(params);
|
|
325
|
-
} catch (error) {
|
|
326
|
-
throw new SubagentRpcError("invalid_params", `${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
327
|
-
}
|
|
328
331
|
if (subagentParamsValidator.Check(params)) return;
|
|
329
332
|
const messages = [...subagentParamsValidator.Errors(params)]
|
|
330
333
|
.slice(0, 4)
|
|
@@ -417,6 +420,9 @@ async function executeChecked(
|
|
|
417
420
|
|
|
418
421
|
function spawnParams(params: unknown): SubagentParamsLike {
|
|
419
422
|
const input = assertRecordParams(params, "spawn");
|
|
423
|
+
if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
|
|
424
|
+
throw new SubagentRpcError("invalid_params", "RPC spawn no longer accepts top-level chain or parallel inputs; use workflowScript.");
|
|
425
|
+
}
|
|
420
426
|
if (input.action !== undefined) {
|
|
421
427
|
throw new SubagentRpcError("invalid_params", "RPC spawn does not accept management/control actions. Use status or interrupt RPC methods instead.");
|
|
422
428
|
}
|
|
@@ -468,8 +474,8 @@ function stopAsyncRun(
|
|
|
468
474
|
): { runId: string; asyncDir: string; previousState: string; state: "stopping"; message: string } {
|
|
469
475
|
const target = normalizeTargetParams(params, "stop");
|
|
470
476
|
assertSubagentParams({ action: "status", ...target }, "RPC stop target params");
|
|
471
|
-
const asyncDirRoot = options.asyncDirRoot ??
|
|
472
|
-
const resultsDir = options.resultsDir ??
|
|
477
|
+
const asyncDirRoot = options.asyncDirRoot ?? DIRS.async;
|
|
478
|
+
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
473
479
|
let location;
|
|
474
480
|
try {
|
|
475
481
|
location = resolveAsyncRunLocation(target, asyncDirRoot, resultsDir);
|