pi-subagents 0.47.1 → 0.49.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 +51 -0
- package/README.md +2 -0
- package/docs/agents.md +1 -0
- package/docs/configuration.md +74 -7
- package/docs/missions.md +4 -2
- package/docs/observability.md +27 -3
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agents.ts +36 -15
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +26 -0
- package/src/extension/doctor.ts +40 -0
- package/src/extension/index.ts +39 -8
- package/src/extension/public-execution.ts +35 -4
- package/src/extension/rpc.ts +3 -9
- package/src/extension/schemas.ts +10 -9
- package/src/extension/tool-description.ts +10 -8
- package/src/inspectors/herdr/actions.ts +11 -2
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/intercom/intercom-bridge.ts +7 -2
- package/src/missions/lifecycle.ts +4 -7
- package/src/missions/store.ts +12 -7
- package/src/missions/workflow-state.ts +6 -2
- package/src/runs/background/active-async-capacity.ts +431 -0
- package/src/runs/background/active-run-index.ts +9 -5
- package/src/runs/background/async-execution.ts +141 -65
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +18 -2
- package/src/runs/background/async-status.ts +11 -5
- package/src/runs/background/chain-append.ts +33 -15
- package/src/runs/background/fleet-view.ts +18 -5
- package/src/runs/background/owned-process-tree.ts +104 -0
- package/src/runs/background/process-terminal.ts +17 -3
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +14 -6
- package/src/runs/background/run-status.ts +101 -5
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +60 -32
- package/src/runs/foreground/chain-execution.ts +37 -2
- package/src/runs/foreground/execution.ts +90 -19
- package/src/runs/foreground/foreground-control.ts +12 -0
- package/src/runs/foreground/prompt-audit.ts +172 -0
- package/src/runs/foreground/subagent-executor.ts +829 -203
- package/src/runs/shared/acceptance.ts +13 -4
- package/src/runs/shared/completion-guard.ts +17 -1
- package/src/runs/shared/llm-intent-arbiter.ts +302 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/pi-args.ts +44 -1
- package/src/runs/shared/run-fanout-budget.ts +280 -0
- package/src/runs/shared/single-output.ts +4 -2
- package/src/runs/shared/task-intent.ts +19 -3
- package/src/runs/shared/worktree.ts +17 -5
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +115 -4
- package/src/shared/utils.ts +3 -1
- package/src/tui/fleet-status.ts +7 -5
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +228 -13
- package/src/tui/render.ts +86 -39
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +8 -2
- package/src/workflows/scripted-workflow.ts +66 -10
package/src/extension/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-sta
|
|
|
30
30
|
import { createSubagentParamsSchema } from "./schemas.ts";
|
|
31
31
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
32
32
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
33
|
+
import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
33
34
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
34
35
|
import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
|
|
35
36
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
@@ -58,6 +59,7 @@ import { resolveMissionStoreLocation } from "../missions/store.ts";
|
|
|
58
59
|
import { listRetainedChildren } from "../runs/background/retained-children.ts";
|
|
59
60
|
import {
|
|
60
61
|
type Details,
|
|
62
|
+
type MainWindowRendererConfig,
|
|
61
63
|
type SubagentState,
|
|
62
64
|
DIRS,
|
|
63
65
|
DEFAULT_ARTIFACT_CONFIG,
|
|
@@ -65,6 +67,7 @@ import {
|
|
|
65
67
|
SLASH_TEXT_RESULT_TYPE,
|
|
66
68
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
67
69
|
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
70
|
+
SUBAGENT_PROCESS_TERMINAL_EVENT,
|
|
68
71
|
SUBAGENT_CONTROL_EVENT,
|
|
69
72
|
SUBAGENT_STEERING_NOTICE_EVENT,
|
|
70
73
|
WIDGET_KEY,
|
|
@@ -251,12 +254,13 @@ function rebuildSlashResultContainer(
|
|
|
251
254
|
result: AgentToolResult<Details>,
|
|
252
255
|
options: { expanded: boolean },
|
|
253
256
|
theme: ExtensionContext["ui"]["theme"],
|
|
257
|
+
rendererConfig?: MainWindowRendererConfig,
|
|
254
258
|
): void {
|
|
255
259
|
container.clear();
|
|
256
260
|
container.addChild(new Spacer(1));
|
|
257
261
|
const boxTheme = isSlashResultRunning(result) ? "toolPendingBg" : isSlashResultError(result) ? "toolErrorBg" : "toolSuccessBg";
|
|
258
262
|
const box = new Box(1, 1, (text: string) => theme.bg(boxTheme, text));
|
|
259
|
-
box.addChild(renderSubagentResult(result, options, theme));
|
|
263
|
+
box.addChild(renderSubagentResult(result, options, theme, undefined, rendererConfig));
|
|
260
264
|
container.addChild(box);
|
|
261
265
|
}
|
|
262
266
|
|
|
@@ -264,6 +268,7 @@ function createSlashResultComponent(
|
|
|
264
268
|
details: SlashMessageDetails,
|
|
265
269
|
options: { expanded: boolean },
|
|
266
270
|
theme: ExtensionContext["ui"]["theme"],
|
|
271
|
+
rendererConfig?: MainWindowRendererConfig,
|
|
267
272
|
): Container {
|
|
268
273
|
const container = new Container();
|
|
269
274
|
let lastVersion = -1;
|
|
@@ -271,7 +276,7 @@ function createSlashResultComponent(
|
|
|
271
276
|
const snapshot = getSlashRenderableSnapshot(details);
|
|
272
277
|
if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
|
|
273
278
|
lastVersion = snapshot.version;
|
|
274
|
-
rebuildSlashResultContainer(container, snapshot.result, options, theme);
|
|
279
|
+
rebuildSlashResultContainer(container, snapshot.result, options, theme, rendererConfig);
|
|
275
280
|
}
|
|
276
281
|
return Container.prototype.render.call(container, width);
|
|
277
282
|
};
|
|
@@ -376,6 +381,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
376
381
|
...(config.authorityPolicy ? { authorityPolicy: config.authorityPolicy } : {}),
|
|
377
382
|
...(config.missions ? { missionStoreConfig: config.missions } : {}),
|
|
378
383
|
parentSessionFile: null,
|
|
384
|
+
trustedSessionRoots: [],
|
|
379
385
|
subagentInProgress: false,
|
|
380
386
|
subagentSpawns: {
|
|
381
387
|
sessionId: null,
|
|
@@ -384,6 +390,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
384
390
|
granted: 0,
|
|
385
391
|
grantHistory: [],
|
|
386
392
|
},
|
|
393
|
+
activeAsyncCapacity: { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 },
|
|
387
394
|
asyncJobs: new Map(),
|
|
388
395
|
fleetJobs: new Map(),
|
|
389
396
|
foregroundRuns: new Map(),
|
|
@@ -478,7 +485,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
478
485
|
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
479
486
|
const details = resolveSlashMessageDetails(message.details);
|
|
480
487
|
if (!details) return undefined;
|
|
481
|
-
return createSlashResultComponent(details, options, theme);
|
|
488
|
+
return createSlashResultComponent(details, options, theme, config.mainWindowRenderer);
|
|
482
489
|
});
|
|
483
490
|
|
|
484
491
|
pi.registerMessageRenderer<undefined>(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
|
|
@@ -578,22 +585,24 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
578
585
|
},
|
|
579
586
|
|
|
580
587
|
renderCall(args, theme) {
|
|
588
|
+
const gap = " ".repeat(config.mainWindowRenderer?.horizontalSpacing ?? 1);
|
|
589
|
+
const title = theme.fg("toolTitle", theme.bold("subagent"));
|
|
581
590
|
if (args.action) {
|
|
582
591
|
const target = args.agent || args.chainName || "";
|
|
583
592
|
return new Text(
|
|
584
|
-
`${
|
|
593
|
+
`${title}${gap}${args.action}${target ? `${gap}${theme.fg("accent", target)}` : ""}`,
|
|
585
594
|
0, 0,
|
|
586
595
|
);
|
|
587
596
|
}
|
|
588
597
|
if (args.workflowScript)
|
|
589
598
|
return new Text(
|
|
590
|
-
`${
|
|
599
|
+
`${title}${gap}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
|
|
591
600
|
0,
|
|
592
601
|
0,
|
|
593
602
|
);
|
|
594
|
-
const asyncLabel = args.async === true ? theme.fg("warning", "
|
|
603
|
+
const asyncLabel = args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : "";
|
|
595
604
|
return new Text(
|
|
596
|
-
`${
|
|
605
|
+
`${title}${gap}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
|
|
597
606
|
0,
|
|
598
607
|
0,
|
|
599
608
|
);
|
|
@@ -604,7 +613,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
604
613
|
const renderedResult = { ...result, isError: context.isError };
|
|
605
614
|
return summaryInlineToolDisplay
|
|
606
615
|
? renderSubagentSummary(renderedResult, options, theme)
|
|
607
|
-
: renderSubagentResult(renderedResult, options, theme);
|
|
616
|
+
: renderSubagentResult(renderedResult, options, theme, undefined, config.mainWindowRenderer);
|
|
608
617
|
},
|
|
609
618
|
|
|
610
619
|
};
|
|
@@ -677,12 +686,17 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
677
686
|
};
|
|
678
687
|
const asyncCompleteHandler = (payload: unknown) => {
|
|
679
688
|
handleComplete(payload);
|
|
689
|
+
refreshActiveAsyncCapacity();
|
|
680
690
|
scheduledRunManager.handleAsyncCompletion(payload);
|
|
681
691
|
fleetStatus?.refresh();
|
|
682
692
|
};
|
|
683
693
|
const eventUnsubscribes = [
|
|
684
694
|
pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
|
|
685
695
|
pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
|
|
696
|
+
pi.events.on(SUBAGENT_PROCESS_TERMINAL_EVENT, () => {
|
|
697
|
+
refreshActiveAsyncCapacity();
|
|
698
|
+
fleetStatus?.refresh();
|
|
699
|
+
}),
|
|
686
700
|
pi.events.on(SUBAGENT_CONTROL_EVENT, controlEventHandler),
|
|
687
701
|
pi.events.on(SUBAGENT_STEERING_NOTICE_EVENT, steeringNoticeHandler),
|
|
688
702
|
herdrStatusBridge.dispose,
|
|
@@ -728,12 +742,28 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
728
742
|
fleetStatus?.refresh();
|
|
729
743
|
};
|
|
730
744
|
|
|
745
|
+
const refreshActiveAsyncCapacity = () => {
|
|
746
|
+
if (!state.currentSessionId) {
|
|
747
|
+
state.activeAsyncCapacity = { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 };
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
|
|
751
|
+
state.currentSessionId,
|
|
752
|
+
resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession),
|
|
753
|
+
{ liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []) },
|
|
754
|
+
);
|
|
755
|
+
};
|
|
756
|
+
|
|
731
757
|
const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
|
|
732
758
|
state.widgetsSuspended = false;
|
|
733
759
|
state.baseCwd = ctx.cwd;
|
|
734
760
|
goalTurnId = 0;
|
|
735
761
|
state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
736
762
|
state.parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
763
|
+
state.trustedSessionRoots = [...new Set([
|
|
764
|
+
...(config.defaultSessionDir ? [path.resolve(expandTilde(config.defaultSessionDir))] : []),
|
|
765
|
+
...(state.parentSessionFile ? [getSubagentSessionRoot(state.parentSessionFile)] : []),
|
|
766
|
+
])];
|
|
737
767
|
state.subagentSpawns = {
|
|
738
768
|
sessionId: state.currentSessionId,
|
|
739
769
|
count: 0,
|
|
@@ -753,6 +783,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
753
783
|
}
|
|
754
784
|
}
|
|
755
785
|
state.lastUiContext = ctx;
|
|
786
|
+
refreshActiveAsyncCapacity();
|
|
756
787
|
cleanupSessionArtifacts(ctx);
|
|
757
788
|
state.foregroundControls.clear();
|
|
758
789
|
state.lastForegroundControlId = null;
|
|
@@ -11,6 +11,8 @@ export interface PublicSubagentExecutionParams {
|
|
|
11
11
|
workflowScript?: unknown;
|
|
12
12
|
resume?: unknown;
|
|
13
13
|
clarify?: unknown;
|
|
14
|
+
runFanoutBudget?: unknown;
|
|
15
|
+
runFanoutAdmitted?: unknown;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export type PublicSubagentExecutionMode = "workflow" | "management";
|
|
@@ -24,6 +26,9 @@ export type PublicSubagentExecutionNormalization<T> =
|
|
|
24
26
|
* Internal runs.run children and structured owned delegation bypass this boundary.
|
|
25
27
|
*/
|
|
26
28
|
export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
|
|
29
|
+
if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
|
|
30
|
+
return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
|
|
31
|
+
}
|
|
27
32
|
const action = params.action;
|
|
28
33
|
if (action !== undefined && (typeof action !== "string" || !action.trim())) {
|
|
29
34
|
return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
|
|
@@ -42,7 +47,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
42
47
|
if (normalizedAction !== undefined) {
|
|
43
48
|
const legacyAction = normalizedAction.toLowerCase();
|
|
44
49
|
if (legacyAction === "single") {
|
|
45
|
-
return { ok: false, error: "
|
|
50
|
+
return { ok: false, error: "action='single' is not supported. Omit action and pass { agent, task } for one child.", mode: "workflow" };
|
|
46
51
|
}
|
|
47
52
|
if (legacyAction === "parallel" || legacyAction === "tasks" || legacyAction === "chain") {
|
|
48
53
|
return { ok: false, error: "Legacy top-level chain and parallel inputs were removed; use workflowScript.", mode: "workflow" };
|
|
@@ -59,13 +64,39 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
59
64
|
if (params.workflowScript !== undefined) {
|
|
60
65
|
return { ok: false, error: "workflowScript execution must omit action; only schedule.create accepts action with workflowScript.", mode: "management" };
|
|
61
66
|
}
|
|
67
|
+
if (params.task !== undefined) {
|
|
68
|
+
return { ok: false, error: "Structured single-child task cannot be combined with a management/control action.", mode: "management" };
|
|
69
|
+
}
|
|
62
70
|
return { ok: true, params: { ...params, action: normalizedAction } };
|
|
63
71
|
}
|
|
64
|
-
if (params.
|
|
65
|
-
return { ok: false, error: "
|
|
72
|
+
if (params.step !== undefined) {
|
|
73
|
+
return { ok: false, error: "step is only available with action='append-step'; it is not an execution mode.", mode: "workflow" };
|
|
74
|
+
}
|
|
75
|
+
if (params.workflowScript !== undefined && (params.agent !== undefined || params.task !== undefined)) {
|
|
76
|
+
return { ok: false, error: "Structured single-child execution cannot be combined with workflowScript.", mode: "workflow" };
|
|
77
|
+
}
|
|
78
|
+
if (params.agent !== undefined || params.task !== undefined) {
|
|
79
|
+
if (typeof params.agent !== "string" || !params.agent.trim()) {
|
|
80
|
+
return { ok: false, error: "Structured single-child execution requires agent to be a non-empty string.", mode: "workflow" };
|
|
81
|
+
}
|
|
82
|
+
if (params.task !== undefined && typeof params.task !== "string") {
|
|
83
|
+
return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
|
|
84
|
+
}
|
|
85
|
+
const { agent: _agent, task: _task, ...workflowDefaults } = params;
|
|
86
|
+
const child = {
|
|
87
|
+
agent: params.agent.trim(),
|
|
88
|
+
...(params.task !== undefined ? { task: params.task } : {}),
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
params: {
|
|
93
|
+
...workflowDefaults,
|
|
94
|
+
workflowScript: `console.info("Converted structured single-child request to workflow runs.run('main', ...)."); return runs.run("main", ${JSON.stringify(child)})`,
|
|
95
|
+
} as T,
|
|
96
|
+
};
|
|
66
97
|
}
|
|
67
98
|
if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
|
|
68
|
-
return { ok: false, error: "Execution requires
|
|
99
|
+
return { ok: false, error: "Execution requires either { agent, task? } for one child or a non-empty workflowScript for orchestration.", mode: "workflow" };
|
|
69
100
|
}
|
|
70
101
|
return { ok: true, params };
|
|
71
102
|
}
|
package/src/extension/rpc.ts
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
20
20
|
import { readStatus } from "../shared/utils.ts";
|
|
21
21
|
import { SubagentParams } from "./schemas.ts";
|
|
22
|
-
import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
|
|
23
22
|
import { normalizePublicSubagentExecution } from "./public-execution.ts";
|
|
24
23
|
|
|
25
24
|
export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
|
|
@@ -91,6 +90,7 @@ export interface SubagentRpcFleetStatus {
|
|
|
91
90
|
entries: SubagentRpcFleetEntry[];
|
|
92
91
|
/** Total active children before the bounded entries window. */
|
|
93
92
|
totalActive: number;
|
|
93
|
+
topLevelAsyncCapacity: { used: number; limit: number };
|
|
94
94
|
omitted: number;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -154,7 +154,7 @@ function buildFleetStatus(
|
|
|
154
154
|
}
|
|
155
155
|
if (!state || !authoritativeSessionId || state.currentSessionId !== authoritativeSessionId) {
|
|
156
156
|
keyState.keys.clear();
|
|
157
|
-
return { version: 1, entries: [], totalActive: 0, omitted: 0 };
|
|
157
|
+
return { version: 1, entries: [], totalActive: 0, topLevelAsyncCapacity: { used: 0, limit: 0 }, omitted: 0 };
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
let totalActive = 0;
|
|
@@ -173,7 +173,6 @@ function buildFleetStatus(
|
|
|
173
173
|
effort: child.thinking,
|
|
174
174
|
startedAt: child.startedAt,
|
|
175
175
|
tokens: { input: child.inputTokens ?? 0, output: child.outputTokens ?? 0, total: child.tokens ?? 0 },
|
|
176
|
-
goal: child.description ?? control.description,
|
|
177
176
|
});
|
|
178
177
|
} else {
|
|
179
178
|
addCandidate({
|
|
@@ -183,7 +182,6 @@ function buildFleetStatus(
|
|
|
183
182
|
effort: control.thinking,
|
|
184
183
|
startedAt: control.startedAt,
|
|
185
184
|
tokens: { input: control.inputTokens ?? 0, output: control.outputTokens ?? 0, total: control.tokens ?? 0 },
|
|
186
|
-
goal: control.description,
|
|
187
185
|
});
|
|
188
186
|
}
|
|
189
187
|
}
|
|
@@ -191,13 +189,11 @@ function buildFleetStatus(
|
|
|
191
189
|
if (job.sessionId !== authoritativeSessionId || !activeState(job.status)) continue;
|
|
192
190
|
const startedAt = job.startedAt ?? job.updatedAt;
|
|
193
191
|
if (job.mode === "workflow") {
|
|
194
|
-
const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
|
|
195
192
|
addCandidate({
|
|
196
193
|
internalKey: `async:${job.asyncId}`,
|
|
197
194
|
agent: "workflow",
|
|
198
195
|
startedAt,
|
|
199
196
|
tokens: job.totalTokens,
|
|
200
|
-
goal: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
|
|
201
197
|
});
|
|
202
198
|
continue;
|
|
203
199
|
}
|
|
@@ -214,7 +210,6 @@ function buildFleetStatus(
|
|
|
214
210
|
agent: job.mode ?? "subagent",
|
|
215
211
|
startedAt,
|
|
216
212
|
tokens: job.totalTokens,
|
|
217
|
-
goal: job.description,
|
|
218
213
|
});
|
|
219
214
|
continue;
|
|
220
215
|
}
|
|
@@ -230,7 +225,6 @@ function buildFleetStatus(
|
|
|
230
225
|
effort: step.thinking,
|
|
231
226
|
startedAt: step.startedAt ?? startedAt,
|
|
232
227
|
tokens: step.tokens ?? (steps.length === 1 ? job.totalTokens : undefined),
|
|
233
|
-
goal: job.description,
|
|
234
228
|
});
|
|
235
229
|
}
|
|
236
230
|
}
|
|
@@ -271,7 +265,7 @@ function buildFleetStatus(
|
|
|
271
265
|
if (!activeKeys.has(internalKey)) keyState.keys.delete(internalKey);
|
|
272
266
|
}
|
|
273
267
|
const omitted = Math.max(0, totalActive - entries.length);
|
|
274
|
-
return { version: 1, entries, totalActive, omitted };
|
|
268
|
+
return { version: 1, entries, totalActive, topLevelAsyncCapacity: state.activeAsyncCapacity ?? { used: 0, limit: 0 }, omitted };
|
|
275
269
|
}
|
|
276
270
|
|
|
277
271
|
interface RegisterSubagentRpcBridgeOptions {
|
package/src/extension/schemas.ts
CHANGED
|
@@ -255,21 +255,22 @@ const ControlOverrides = Type.Object({
|
|
|
255
255
|
});
|
|
256
256
|
|
|
257
257
|
const SubagentParamProperties = {
|
|
258
|
-
agent: Type.Optional(Type.String({ description: "Agent
|
|
258
|
+
agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
|
|
259
|
+
task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript." })),
|
|
259
260
|
resume: Type.Optional(Type.String({ description: "Retained child run id for a workflowScript runs.run/runs.all item. Mutually exclusive with agent; task supplies the follow-up." })),
|
|
260
261
|
// Management action (when present, tool operates in management mode)
|
|
261
262
|
action: Type.Optional(Type.String({ minLength: 1,
|
|
262
|
-
description: "Optional management/control action. Omit this field for workflowScript execution; use it only for management/control actions."
|
|
263
|
+
description: "Optional management/control action. Omit this field for structured single-child or workflowScript execution; use it only for management/control actions."
|
|
263
264
|
})),
|
|
264
265
|
name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
|
|
265
266
|
id: Type.Optional(Type.String({
|
|
266
|
-
description: "Run id
|
|
267
|
+
description: "Run id/prefix for status/debug.run, interrupt, steer, append-step, approve-checkpoint, reject-checkpoint, or mission."
|
|
267
268
|
})),
|
|
268
269
|
runId: Type.Optional(Type.String({
|
|
269
|
-
description: "Target run ID for
|
|
270
|
+
description: "Target run ID for debug.run, interrupt, steer, append-step, or mission.attach-run. Prefer id."
|
|
270
271
|
})),
|
|
271
272
|
dir: Type.Optional(Type.String({
|
|
272
|
-
description: "Async run directory for
|
|
273
|
+
description: "Async run directory for status/debug.run, stop, resume, or steer."
|
|
273
274
|
})),
|
|
274
275
|
handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
|
|
275
276
|
index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
|
|
@@ -324,8 +325,8 @@ const SubagentParamProperties = {
|
|
|
324
325
|
description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.",
|
|
325
326
|
})),
|
|
326
327
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
327
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "
|
|
328
|
-
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground
|
|
328
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
|
|
329
|
+
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline." })),
|
|
329
330
|
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
330
331
|
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
331
332
|
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
@@ -359,10 +360,10 @@ const { step: _legacyChainStep, ...subagentParamPropertiesWithoutStep } = Subage
|
|
|
359
360
|
const trimmedSubagentParamProperties = {
|
|
360
361
|
...subagentParamPropertiesWithoutStep,
|
|
361
362
|
id: Type.Optional(Type.String({
|
|
362
|
-
description: "Run id
|
|
363
|
+
description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
|
|
363
364
|
})),
|
|
364
365
|
runId: Type.Optional(Type.String({
|
|
365
|
-
description: "Target run ID for
|
|
366
|
+
description: "Target run ID for debug.run, interrupt, steer, or mission.attach-run. Prefer id."
|
|
366
367
|
})),
|
|
367
368
|
};
|
|
368
369
|
const SubagentParamsSchema = Type.Object(SubagentParamProperties);
|
|
@@ -8,24 +8,25 @@ const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
|
8
8
|
|
|
9
9
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
10
|
• Use { action: "list" } before execution and only run executable/non-disabled agents.
|
|
11
|
-
• Keep execution and management separate: omit action for workflowScript execution; use action only for management/control.
|
|
12
|
-
• Async/background runs are the default. Use async:false only when a blocking foreground result is needed. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
11
|
+
• Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
|
|
12
|
+
• Async/background runs are the default. Use async:false only when a blocking foreground result is needed. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
13
13
|
• Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
|
|
14
14
|
• Keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers for independent review, then have the parent synthesize and apply fixes.
|
|
15
|
-
• Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs,
|
|
15
|
+
• Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, status via { action: "status", id }, and lifecycle diagnostics via { action: "debug.run", id }. Include output paths and residual risks when reporting results.`;
|
|
16
16
|
|
|
17
|
-
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run
|
|
17
|
+
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for orchestration. Omit action for execution. Use action only for management/control actions.
|
|
18
18
|
|
|
19
19
|
EXECUTION:
|
|
20
20
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
21
|
-
•
|
|
21
|
+
• SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one child through the workflow runtime. Workflow-level fields such as model, context, cwd, worktree, output, budgets, acceptance, and async remain defaults for that child. Do not combine agent/task with action or workflowScript.
|
|
22
|
+
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable plain task text, then pass the result explicitly as task. Use {action:"children.list"} to list up to 10 completed retained children from this parent session, then continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); resume and agent are mutually exclusive, resume keeps the stored agent/model/tool contract, workflow resumes wait for completed output, and loops must continue from each latest returned runId. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, prompts.render, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
22
23
|
• Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
|
|
23
24
|
• Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
|
|
24
25
|
• Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
25
26
|
• Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work. A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
26
27
|
|
|
27
28
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
28
|
-
• list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
29
|
+
• list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
29
30
|
• status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
|
|
30
31
|
• { action: "append-step", id: "...", step: {agent:"agent-c", task:"Use {previous}"} } appends one step to an already-running durable legacy chain. step is control-only, not an execution mode.
|
|
31
32
|
• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
|
|
@@ -33,10 +34,11 @@ MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
|
33
34
|
|
|
34
35
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
35
36
|
|
|
36
|
-
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run
|
|
37
|
+
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for orchestration. Omit action for execution. Use action only for management/control actions.
|
|
37
38
|
|
|
38
39
|
EXECUTE:
|
|
39
40
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
41
|
+
• SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
|
|
40
42
|
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. Use await prompts.render("package:name" | "user:name" | "project:name", vars?) for reusable task text and pass it explicitly to runs.run. Use {action:"children.list"} for the last 10 retained children in this parent session, then runs.run(key,{resume:"run-id",task:"follow-up"}) to continue one with its stored contract; workflow resumes wait for completion and loops continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off.
|
|
41
43
|
• Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
|
|
42
44
|
• context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
|
|
@@ -47,7 +49,7 @@ MANAGE / CONTROL:
|
|
|
47
49
|
• A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
48
50
|
|
|
49
51
|
ASYNC / SAFETY:
|
|
50
|
-
• Omitted async detaches background work. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
52
|
+
• Omitted async detaches background work. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
51
53
|
• Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
|
|
52
54
|
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
|
|
53
55
|
|
|
@@ -10,6 +10,7 @@ import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
|
10
10
|
import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
|
|
11
11
|
import { readStatus } from "../../shared/utils.ts";
|
|
12
12
|
import { resolveSubagentRunId } from "../../runs/background/run-id-resolver.ts";
|
|
13
|
+
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
13
14
|
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
|
|
14
15
|
|
|
15
16
|
export const HERDR_INSPECTOR_ACTIONS = ["inspector.open", "inspector.status", "inspector.close"] as const;
|
|
@@ -45,6 +46,7 @@ interface InspectorDeps {
|
|
|
45
46
|
client?: HerdrClient;
|
|
46
47
|
missions?: MissionStoreConfig;
|
|
47
48
|
authorityPolicy?: AuthorityPolicyConfig;
|
|
49
|
+
sessionRoots?: string[];
|
|
48
50
|
cwd: string;
|
|
49
51
|
signal?: AbortSignal;
|
|
50
52
|
now?: () => Date;
|
|
@@ -89,8 +91,8 @@ function shellQuote(value: string): string {
|
|
|
89
91
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean }): string {
|
|
93
|
-
const args = [
|
|
94
|
+
function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean; sessionRoots: string[] }): string {
|
|
95
|
+
const args = [resolveNodeExecutable(), input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop), "--session-roots", JSON.stringify(input.sessionRoots)];
|
|
94
96
|
if (input.index !== undefined) args.push("--index", String(input.index));
|
|
95
97
|
if (input.missionPath) args.push("--mission-path", input.missionPath);
|
|
96
98
|
return `${process.platform === "win32" ? "& " : ""}${args.map(shellQuote).join(" ")}`;
|
|
@@ -114,6 +116,12 @@ function pathWithin(base: string, candidate: string): boolean {
|
|
|
114
116
|
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
function herdrSessionRoots(target: { runId: string }, deps: InspectorDeps): string[] {
|
|
120
|
+
const roots = deps.sessionRoots ?? deps.state?.trustedSessionRoots ?? [];
|
|
121
|
+
const job = deps.state?.asyncJobs.get(target.runId) ?? deps.state?.fleetJobs?.get(target.runId);
|
|
122
|
+
return [...new Set([...roots, ...(job?.sessionRoot ? [job.sessionRoot] : [])])];
|
|
123
|
+
}
|
|
124
|
+
|
|
117
125
|
function isTrustedAsyncDir(asyncDir: string, deps: InspectorDeps): boolean {
|
|
118
126
|
try {
|
|
119
127
|
if (fs.lstatSync(asyncDir).isSymbolicLink() || !fs.statSync(asyncDir).isDirectory()) return false;
|
|
@@ -204,6 +212,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
|
|
|
204
212
|
missionPath: mission?.path,
|
|
205
213
|
allowSteer: resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto",
|
|
206
214
|
allowStop: resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto",
|
|
215
|
+
sessionRoots: herdrSessionRoots(target, deps),
|
|
207
216
|
});
|
|
208
217
|
const started = await client.run(["pane", "run", paneId, command], { timeoutMs: 15_000, signal: deps.signal });
|
|
209
218
|
if (started.ok === false) {
|
|
@@ -17,6 +17,7 @@ export interface RunnerOptions {
|
|
|
17
17
|
refreshMs: number;
|
|
18
18
|
allowSteer?: boolean;
|
|
19
19
|
allowStop?: boolean;
|
|
20
|
+
sessionRoots: string[];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function readMission(filePath: string | undefined): MissionRecord | undefined {
|
|
@@ -24,7 +25,7 @@ function readMission(filePath: string | undefined): MissionRecord | undefined {
|
|
|
24
25
|
try { return parseMissionRecord(JSON.parse(fs.readFileSync(filePath, "utf-8")), filePath); } catch { return undefined; }
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean }): string {
|
|
28
|
+
export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir: string; index?: number; mission?: MissionRecord; allowSteer?: boolean; allowStop?: boolean; sessionRoots?: string[] }): string {
|
|
28
29
|
const { status, asyncDir, mission } = input;
|
|
29
30
|
const lines = [
|
|
30
31
|
`pi-subagents inspector for ${status.runId}`,
|
|
@@ -37,7 +38,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
|
|
|
37
38
|
if (open.length) lines.push(`Open decisions: ${open.map((decision) => `${decision.id}: ${decision.title}`).join(" | ")}`);
|
|
38
39
|
lines.push("");
|
|
39
40
|
}
|
|
40
|
-
lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60 }));
|
|
41
|
+
lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
|
|
41
42
|
const controls = [input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
|
|
42
43
|
lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
|
|
43
44
|
return lines.join("\n");
|
|
@@ -57,6 +58,17 @@ function parseArgs(argv: string[]): RunnerOptions {
|
|
|
57
58
|
const indexRaw = values.get("--index");
|
|
58
59
|
const childIndex = indexRaw === undefined ? undefined : Number(indexRaw);
|
|
59
60
|
if (childIndex !== undefined && (!Number.isInteger(childIndex) || childIndex < 0)) throw new Error("--index must be a non-negative integer.");
|
|
61
|
+
const sessionRootsRaw = values.get("--session-roots");
|
|
62
|
+
let sessionRoots: string[] = [];
|
|
63
|
+
if (sessionRootsRaw !== undefined) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(sessionRootsRaw) as unknown;
|
|
66
|
+
if (!Array.isArray(parsed) || parsed.some((root) => typeof root !== "string")) throw new Error();
|
|
67
|
+
sessionRoots = parsed;
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error("--session-roots must be a JSON array of strings.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
60
72
|
const refreshRaw = values.get("--refresh-ms");
|
|
61
73
|
const refreshMs = refreshRaw === undefined ? 1_500 : Number(refreshRaw);
|
|
62
74
|
if (!Number.isInteger(refreshMs) || refreshMs < 250) throw new Error("--refresh-ms must be an integer >= 250.");
|
|
@@ -65,6 +77,7 @@ function parseArgs(argv: string[]): RunnerOptions {
|
|
|
65
77
|
runId,
|
|
66
78
|
...(childIndex !== undefined ? { index: childIndex } : {}),
|
|
67
79
|
...(values.get("--mission-path") ? { missionPath: path.resolve(values.get("--mission-path")!) } : {}),
|
|
80
|
+
sessionRoots,
|
|
68
81
|
refreshMs,
|
|
69
82
|
allowSteer: values.get("--allow-steer") !== "false",
|
|
70
83
|
allowStop: values.get("--allow-stop") !== "false",
|
|
@@ -115,7 +128,7 @@ export function runInspector(argv = process.argv.slice(2)): void {
|
|
|
115
128
|
process.stdout.write(`\x1b[2J\x1b[Hpi-subagents inspector\n\nLifecycle status for ${options.runId} is unavailable.\n`);
|
|
116
129
|
return;
|
|
117
130
|
}
|
|
118
|
-
process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop })}${notice ? `\n\n${notice}` : ""}\n> `);
|
|
131
|
+
process.stdout.write(`\x1b[2J\x1b[H${formatInspectorDashboard({ status, asyncDir: options.asyncDir, index: options.index, mission: readMission(options.missionPath), allowSteer: options.allowSteer, allowStop: options.allowStop, sessionRoots: options.sessionRoots })}${notice ? `\n\n${notice}` : ""}\n> `);
|
|
119
132
|
if (isTerminal(status) && timer) {
|
|
120
133
|
clearInterval(timer);
|
|
121
134
|
timer = undefined;
|
|
@@ -51,6 +51,8 @@ export interface IntercomBridgeDiagnostic {
|
|
|
51
51
|
|
|
52
52
|
interface ResolveIntercomBridgeInput {
|
|
53
53
|
config: ExtensionConfig["intercomBridge"];
|
|
54
|
+
/** Per-run config replaces the global config when supplied. */
|
|
55
|
+
override?: IntercomBridgeConfig;
|
|
54
56
|
context: "fresh" | "fork" | undefined;
|
|
55
57
|
orchestratorTarget?: string;
|
|
56
58
|
cwd?: string;
|
|
@@ -63,7 +65,10 @@ export function resolveIntercomSessionTarget(sessionName: string | undefined, se
|
|
|
63
65
|
if (trimmedName) return trimmedName;
|
|
64
66
|
const fallbackSessionId = intercomSessionId?.trim() || sessionId;
|
|
65
67
|
const normalizedSessionId = fallbackSessionId.startsWith("session-") ? fallbackSessionId.slice("session-".length) : fallbackSessionId;
|
|
66
|
-
|
|
68
|
+
// NOTE: keep slice length in sync with pi-intercom's resolveIntercomPresenceName
|
|
69
|
+
// (index.ts: DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX + slice(0, 18)); mismatched lengths
|
|
70
|
+
// make fallback orchestrator targets unresolvable ("Session not found").
|
|
71
|
+
return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 18)}`;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
function sanitizeIntercomTargetPart(value: string): string {
|
|
@@ -140,7 +145,7 @@ export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): Inter
|
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
|
|
143
|
-
const config = resolveIntercomBridgeConfig(input.config);
|
|
148
|
+
const config = resolveIntercomBridgeConfig(input.override !== undefined ? input.override : input.config);
|
|
144
149
|
const mode = config.mode;
|
|
145
150
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
146
151
|
const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
4
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
5
|
+
import { PROMPT_REDACTED } from "../shared/utils.ts";
|
|
5
6
|
import type { Details, SubagentRunMode } from "../shared/types.ts";
|
|
6
7
|
import { validateMissionLaunch } from "./actions.ts";
|
|
7
8
|
import type { MissionArtifact, MissionRecord, MissionRunLink, MissionRunMode, MissionStatus, MissionStoreConfig, MissionStoreLocation } from "./types.ts";
|
|
@@ -47,11 +48,6 @@ function workflowObjective(params: MissionLaunchParams): string | undefined {
|
|
|
47
48
|
return undefined;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function conciseTitle(objective: string): string {
|
|
51
|
-
const firstLine = objective.split(/\r?\n/, 1)[0]?.trim() || objective.trim();
|
|
52
|
-
return firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
51
|
export function prepareMissionLaunch(input: {
|
|
56
52
|
params: MissionLaunchParams;
|
|
57
53
|
projectRoot: string;
|
|
@@ -73,10 +69,11 @@ export function prepareMissionLaunch(input: {
|
|
|
73
69
|
return { missionId, location, autoCreated: false, announceInContent: true };
|
|
74
70
|
}
|
|
75
71
|
const mission = input.params.mission !== undefined ? validateMissionLaunch(input.params.mission) : undefined;
|
|
76
|
-
const
|
|
72
|
+
const promptDerivedObjective = mission?.objective ?? (mission ? mission.title : objective ? PROMPT_REDACTED : undefined);
|
|
73
|
+
const title = mission?.title || PROMPT_REDACTED;
|
|
77
74
|
const record = createMission(location, {
|
|
78
75
|
title,
|
|
79
|
-
objective:
|
|
76
|
+
objective: promptDerivedObjective || title,
|
|
80
77
|
...(mission?.goal === true ? { goal: true as const } : {}),
|
|
81
78
|
...(mission?.budget ? { budget: mission.budget } : {}),
|
|
82
79
|
status: "active",
|