pi-subagents 0.49.0 → 0.50.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 +35 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/docs/agents.md +2 -0
- package/docs/configuration.md +47 -3
- package/docs/extension-api.md +36 -0
- package/docs/missions.md +1 -1
- package/docs/observability.md +16 -0
- package/docs/tool-reference.md +19 -2
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +10 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +12 -6
- package/src/extension/config.ts +33 -0
- package/src/extension/index.ts +48 -5
- package/src/extension/public-execution.ts +3 -1
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +4 -2
- package/src/inspectors/herdr/actions.ts +3 -7
- package/src/inspectors/herdr/project-panes.ts +2 -6
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +19 -42
- package/src/missions/goal-driver.ts +3 -1
- package/src/runs/background/active-run-index.ts +71 -1
- package/src/runs/background/async-execution.ts +49 -7
- package/src/runs/background/async-job-tracker.ts +1 -0
- package/src/runs/background/async-resume.ts +3 -5
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/result-files.ts +437 -0
- package/src/runs/background/result-watcher.ts +188 -41
- package/src/runs/background/retained-children.ts +69 -20
- package/src/runs/background/run-id-resolver.ts +30 -24
- package/src/runs/background/run-status.ts +7 -2
- package/src/runs/background/scheduled-runs.ts +54 -28
- package/src/runs/background/stale-run-reconciler.ts +27 -13
- package/src/runs/background/subagent-runner.ts +294 -32
- package/src/runs/background/subagent-wait.ts +2 -0
- package/src/runs/background/wait-completions.ts +5 -2
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/chain-execution.ts +16 -0
- package/src/runs/foreground/execution.ts +219 -15
- package/src/runs/foreground/subagent-executor.ts +114 -28
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/model-fallback.ts +16 -2
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +375 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/subagent-control.ts +15 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/shared/types.ts +29 -2
- package/src/slash/slash-commands.ts +34 -25
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +125 -15
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +6 -4
- package/src/workflows/scripted-workflow.ts +227 -70
|
@@ -18,6 +18,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
18
18
|
"defaultContext",
|
|
19
19
|
"async",
|
|
20
20
|
"timeoutMs",
|
|
21
|
+
"toolTimeoutMs",
|
|
21
22
|
"turnBudget",
|
|
22
23
|
"acceptance",
|
|
23
24
|
"acceptanceRole",
|
|
@@ -86,6 +87,7 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
|
|
|
86
87
|
}
|
|
87
88
|
if (config.defaultAsync !== undefined || preserve("async")) lines.push(`async: ${config.defaultAsync === undefined ? "" : config.defaultAsync ? "true" : "false"}`);
|
|
88
89
|
if (config.defaultTimeoutMs !== undefined || preserve("timeoutMs")) lines.push(`timeoutMs: ${config.defaultTimeoutMs ?? ""}`);
|
|
90
|
+
if (config.defaultToolTimeoutMs !== undefined || preserve("toolTimeoutMs")) lines.push(`toolTimeoutMs: ${config.defaultToolTimeoutMs ?? ""}`);
|
|
89
91
|
if (config.defaultTurnBudget || preserve("turnBudget")) lines.push(`turnBudget: ${config.defaultTurnBudget ? JSON.stringify(config.defaultTurnBudget) : ""}`);
|
|
90
92
|
if (config.defaultAcceptance !== undefined || preserve("acceptance")) {
|
|
91
93
|
lines.push(`acceptance: ${config.defaultAcceptance === undefined
|
package/src/agents/agents.ts
CHANGED
|
@@ -130,6 +130,7 @@ export interface AgentConfig {
|
|
|
130
130
|
defaultContext?: AgentDefaultContext;
|
|
131
131
|
defaultAsync?: boolean;
|
|
132
132
|
defaultTimeoutMs?: number;
|
|
133
|
+
defaultToolTimeoutMs?: number;
|
|
133
134
|
defaultTurnBudget?: TurnBudgetConfig;
|
|
134
135
|
defaultAcceptance?: AcceptanceInput;
|
|
135
136
|
acceptanceRole?: AcceptanceRole;
|
|
@@ -1582,6 +1583,14 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1582
1583
|
}
|
|
1583
1584
|
defaultTimeoutMs = parsed;
|
|
1584
1585
|
}
|
|
1586
|
+
let defaultToolTimeoutMs: number | undefined;
|
|
1587
|
+
if (frontmatter.toolTimeoutMs !== undefined) {
|
|
1588
|
+
const parsed = Number(frontmatter.toolTimeoutMs);
|
|
1589
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 2_147_483_647) {
|
|
1590
|
+
throw new Error(`Agent '${localName}' has invalid toolTimeoutMs frontmatter; expected a positive integer no larger than 2147483647.`);
|
|
1591
|
+
}
|
|
1592
|
+
defaultToolTimeoutMs = parsed;
|
|
1593
|
+
}
|
|
1585
1594
|
let defaultTurnBudget: TurnBudgetConfig | undefined;
|
|
1586
1595
|
if (frontmatter.turnBudget !== undefined && frontmatter.turnBudget.trim()) {
|
|
1587
1596
|
const parsed = JSON.parse(frontmatter.turnBudget) as unknown;
|
|
@@ -1648,6 +1657,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1648
1657
|
...(defaultContext !== undefined ? { defaultContext } : {}),
|
|
1649
1658
|
...(defaultAsync !== undefined ? { defaultAsync } : {}),
|
|
1650
1659
|
...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
|
|
1660
|
+
...(defaultToolTimeoutMs !== undefined ? { defaultToolTimeoutMs } : {}),
|
|
1651
1661
|
...(defaultTurnBudget !== undefined ? { defaultTurnBudget } : {}),
|
|
1652
1662
|
...(defaultAcceptance !== undefined ? { defaultAcceptance } : {}),
|
|
1653
1663
|
...(acceptanceRole !== undefined ? { acceptanceRole } : {}),
|
package/src/api/external-runs.ts
CHANGED
|
@@ -1,129 +1,219 @@
|
|
|
1
|
-
|
|
2
|
-
export const EXTERNAL_RUN_REGISTRY_KEY = "pi-subagents.external-runs.v1";
|
|
1
|
+
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
3
2
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const MAX_TEXT_LENGTH = 4_096;
|
|
3
|
+
export const EXTERNAL_RUN_REGISTRY_VERSION = 2;
|
|
4
|
+
export const EXTERNAL_RUN_REGISTRY_KEY = "pi-subagents.external-runs.v2";
|
|
7
5
|
|
|
8
|
-
export
|
|
9
|
-
|
|
6
|
+
export const EXTERNAL_RUN_LIMITS = {
|
|
7
|
+
maxCachedRuns: 100,
|
|
8
|
+
maxSnapshotRuns: 20,
|
|
9
|
+
maxIdentityLength: 160,
|
|
10
|
+
/**
|
|
11
|
+
* A Pi session id is the session file path, which routinely exceeds a short
|
|
12
|
+
* identity budget in nested worktrees, so it is bounded like the other paths.
|
|
13
|
+
*/
|
|
14
|
+
maxSessionIdLength: 4_096,
|
|
15
|
+
maxTextLength: 160,
|
|
16
|
+
maxPreviewLength: 4_096,
|
|
17
|
+
maxPathLength: 4_096,
|
|
18
|
+
maxSerializedBytes: 32 * 1_024,
|
|
19
|
+
} as const;
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
export type ExternalRunState = "queued" | "running" | "completed" | "failed" | "stopped";
|
|
22
|
+
|
|
23
|
+
/** A display-only record for work owned by another extension or runtime. */
|
|
12
24
|
export interface ExternalRun {
|
|
13
25
|
id: string;
|
|
14
26
|
sessionId: string;
|
|
15
27
|
source: string;
|
|
28
|
+
label: string;
|
|
16
29
|
state: ExternalRunState;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
startedAt: number;
|
|
31
|
+
updatedAt?: number;
|
|
32
|
+
endedAt?: number;
|
|
33
|
+
currentAction?: string;
|
|
34
|
+
preview?: string;
|
|
22
35
|
reportPath?: string;
|
|
23
|
-
|
|
24
|
-
completedAt?: number;
|
|
25
|
-
exitCode?: number | null;
|
|
26
|
-
residualRisks?: string[];
|
|
36
|
+
transcriptPath?: string;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
export
|
|
30
|
-
name: string;
|
|
31
|
-
listExternalRuns(): readonly ExternalRun[];
|
|
32
|
-
}
|
|
39
|
+
export type ExternalRunUpdate = Partial<Omit<ExternalRun, "id" | "sessionId" | "source">>;
|
|
33
40
|
|
|
34
|
-
export interface
|
|
35
|
-
|
|
41
|
+
export interface ExternalRunSnapshotOptions {
|
|
42
|
+
onMalformedRecord?: (message: string) => void;
|
|
43
|
+
ignoreMalformed?: boolean;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
interface ExternalRunRegistry {
|
|
39
47
|
version: typeof EXTERNAL_RUN_REGISTRY_VERSION;
|
|
40
|
-
|
|
48
|
+
runs: Map<string, ExternalRun>;
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
const RUN_FIELDS = new Set([
|
|
52
|
+
"id",
|
|
53
|
+
"sessionId",
|
|
54
|
+
"source",
|
|
55
|
+
"label",
|
|
56
|
+
"state",
|
|
57
|
+
"startedAt",
|
|
58
|
+
"updatedAt",
|
|
59
|
+
"endedAt",
|
|
60
|
+
"currentAction",
|
|
61
|
+
"preview",
|
|
62
|
+
"reportPath",
|
|
63
|
+
"transcriptPath",
|
|
64
|
+
]);
|
|
65
|
+
const UPDATE_FIELDS = new Set([...RUN_FIELDS].filter((field) => field !== "id" && field !== "sessionId" && field !== "source"));
|
|
66
|
+
|
|
43
67
|
function registry(): ExternalRunRegistry {
|
|
44
68
|
const key = Symbol.for(EXTERNAL_RUN_REGISTRY_KEY);
|
|
45
69
|
const target = globalThis as Record<PropertyKey, unknown>;
|
|
46
70
|
const existing = target[key];
|
|
47
71
|
if (existing === undefined) {
|
|
48
|
-
const created: ExternalRunRegistry = { version: EXTERNAL_RUN_REGISTRY_VERSION,
|
|
72
|
+
const created: ExternalRunRegistry = { version: EXTERNAL_RUN_REGISTRY_VERSION, runs: new Map() };
|
|
49
73
|
target[key] = created;
|
|
50
74
|
return created;
|
|
51
75
|
}
|
|
52
76
|
if (!existing || typeof existing !== "object" || Array.isArray(existing)) throw new Error(`Malformed external-run registry at Symbol.for("${EXTERNAL_RUN_REGISTRY_KEY}").`);
|
|
53
77
|
const candidate = existing as Partial<ExternalRunRegistry>;
|
|
54
|
-
if (candidate.version !== EXTERNAL_RUN_REGISTRY_VERSION || !(candidate.
|
|
78
|
+
if (candidate.version !== EXTERNAL_RUN_REGISTRY_VERSION || !(candidate.runs instanceof Map)) throw new Error(`Unsupported external-run registry at Symbol.for("${EXTERNAL_RUN_REGISTRY_KEY}").`);
|
|
55
79
|
return candidate as ExternalRunRegistry;
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
function
|
|
59
|
-
if (value
|
|
82
|
+
function inputObject(value: unknown, field: string, allowed: Set<string>): Record<string, unknown> {
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} must be an object.`);
|
|
84
|
+
const input = value as Record<string, unknown>;
|
|
85
|
+
const unknown = Object.keys(input).filter((key) => !allowed.has(key));
|
|
86
|
+
if (unknown.length) throw new Error(`${field} has unknown fields: ${unknown.join(", ")}.`);
|
|
87
|
+
return input;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function identity(value: unknown, field: string, maxLength: number = EXTERNAL_RUN_LIMITS.maxIdentityLength): string {
|
|
60
91
|
if (typeof value !== "string" || value.length === 0 || value.trim() !== value || value.includes("\0")) throw new Error(`${field} must be a non-empty trimmed string without NUL characters.`);
|
|
61
|
-
if (value.length >
|
|
92
|
+
if (value.length > maxLength) throw new Error(`${field} must be at most ${maxLength} characters.`);
|
|
93
|
+
const safe = sanitizeDisplayText(value);
|
|
94
|
+
if (!safe || safe !== value) throw new Error(`${field} must contain only display-safe text.`);
|
|
62
95
|
return value;
|
|
63
96
|
}
|
|
64
97
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
function displayText(value: unknown, field: string, maxLength: number): string | undefined;
|
|
99
|
+
function displayText(value: unknown, field: string, maxLength: number, required: true): string;
|
|
100
|
+
function displayText(value: unknown, field: string, maxLength: number, required = false): string | undefined {
|
|
101
|
+
if (value === undefined && !required) return undefined;
|
|
102
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`${field} must be a non-empty string.`);
|
|
103
|
+
const safe = sanitizeDisplayText(value.slice(0, Math.max(maxLength * 4, maxLength)));
|
|
104
|
+
if (!safe) throw new Error(`${field} must contain displayable text.`);
|
|
105
|
+
return truncateDisplayText(safe, maxLength);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function timestamp(value: unknown, field: string): number | undefined;
|
|
109
|
+
function timestamp(value: unknown, field: string, required: true): number;
|
|
110
|
+
function timestamp(value: unknown, field: string, required = false): number | undefined {
|
|
111
|
+
if (value === undefined && !required) return undefined;
|
|
112
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 8_640_000_000_000_000) throw new Error(`${field} must be a non-negative safe timestamp.`);
|
|
68
113
|
return value;
|
|
69
114
|
}
|
|
70
115
|
|
|
71
|
-
function
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
116
|
+
function state(value: unknown, field: string): ExternalRunState {
|
|
117
|
+
if (value === "queued" || value === "running" || value === "completed" || value === "failed" || value === "stopped") return value;
|
|
118
|
+
throw new Error(`${field} is invalid.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateRun(value: unknown): ExternalRun {
|
|
122
|
+
const run = inputObject(value, "External run", RUN_FIELDS);
|
|
123
|
+
const updatedAt = timestamp(run.updatedAt, "External run updatedAt");
|
|
124
|
+
const endedAt = timestamp(run.endedAt, "External run endedAt");
|
|
125
|
+
const currentAction = displayText(run.currentAction, "External run currentAction", EXTERNAL_RUN_LIMITS.maxTextLength);
|
|
126
|
+
const preview = displayText(run.preview, "External run preview", EXTERNAL_RUN_LIMITS.maxPreviewLength);
|
|
127
|
+
const reportPath = displayText(run.reportPath, "External run reportPath", EXTERNAL_RUN_LIMITS.maxPathLength);
|
|
128
|
+
const transcriptPath = displayText(run.transcriptPath, "External run transcriptPath", EXTERNAL_RUN_LIMITS.maxPathLength);
|
|
81
129
|
return {
|
|
82
|
-
id:
|
|
83
|
-
sessionId:
|
|
84
|
-
source:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
...(
|
|
89
|
-
...(
|
|
90
|
-
...(
|
|
91
|
-
...(
|
|
92
|
-
...(
|
|
93
|
-
...(
|
|
94
|
-
...(run.exitCode !== undefined ? { exitCode: run.exitCode as number | null } : {}),
|
|
95
|
-
...(run.residualRisks !== undefined ? { residualRisks: [...run.residualRisks] as string[] } : {}),
|
|
130
|
+
id: identity(run.id, "External run id"),
|
|
131
|
+
sessionId: identity(run.sessionId, "External run sessionId", EXTERNAL_RUN_LIMITS.maxSessionIdLength),
|
|
132
|
+
source: displayText(run.source, "External run source", EXTERNAL_RUN_LIMITS.maxTextLength, true),
|
|
133
|
+
label: displayText(run.label, "External run label", EXTERNAL_RUN_LIMITS.maxTextLength, true),
|
|
134
|
+
state: state(run.state, "External run state"),
|
|
135
|
+
startedAt: timestamp(run.startedAt, "External run startedAt", true),
|
|
136
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
137
|
+
...(endedAt !== undefined ? { endedAt } : {}),
|
|
138
|
+
...(currentAction ? { currentAction } : {}),
|
|
139
|
+
...(preview ? { preview } : {}),
|
|
140
|
+
...(reportPath ? { reportPath } : {}),
|
|
141
|
+
...(transcriptPath ? { transcriptPath } : {}),
|
|
96
142
|
};
|
|
97
143
|
}
|
|
98
144
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
145
|
+
function key(sessionId: string, id: string): string {
|
|
146
|
+
return `${sessionId}\0${id}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function clone(run: ExternalRun): ExternalRun {
|
|
150
|
+
return { ...run };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Register one current-session external job. pi-subagents never controls the job. */
|
|
154
|
+
export function registerExternalRun(input: ExternalRun): ExternalRun {
|
|
155
|
+
const run = validateRun(input);
|
|
156
|
+
const current = registry();
|
|
157
|
+
const runKey = key(run.sessionId, run.id);
|
|
158
|
+
if (current.runs.has(runKey)) throw new Error(`External run '${run.id}' is already registered for session '${run.sessionId}'.`);
|
|
159
|
+
if (current.runs.size >= EXTERNAL_RUN_LIMITS.maxCachedRuns) throw new Error(`External-run registry supports at most ${EXTERNAL_RUN_LIMITS.maxCachedRuns} cached runs.`);
|
|
160
|
+
current.runs.set(runKey, run);
|
|
161
|
+
return clone(run);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Update display fields for a registered external job without changing its identity or owner. */
|
|
165
|
+
export function updateExternalRun(sessionId: string, id: string, update: ExternalRunUpdate): ExternalRun {
|
|
166
|
+
const safeSessionId = identity(sessionId, "External run sessionId", EXTERNAL_RUN_LIMITS.maxSessionIdLength);
|
|
167
|
+
const safeId = identity(id, "External run id");
|
|
168
|
+
const patch = inputObject(update, "External run update", UPDATE_FIELDS);
|
|
169
|
+
const current = registry();
|
|
170
|
+
const runKey = key(safeSessionId, safeId);
|
|
171
|
+
const previous = current.runs.get(runKey);
|
|
172
|
+
if (!previous) throw new Error(`External run '${safeId}' is not registered for session '${safeSessionId}'.`);
|
|
173
|
+
const next = validateRun({ ...previous, ...patch });
|
|
174
|
+
current.runs.set(runKey, next);
|
|
175
|
+
return clone(next);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Remove a cached external job. The caller remains responsible for its process and artifacts. */
|
|
179
|
+
export function unregisterExternalRun(sessionId: string, id: string): boolean {
|
|
180
|
+
return registry().runs.delete(key(identity(sessionId, "External run sessionId", EXTERNAL_RUN_LIMITS.maxSessionIdLength), identity(id, "External run id")));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function snapshotBytes(runs: readonly ExternalRun[]): number {
|
|
184
|
+
return Buffer.byteLength(JSON.stringify(runs), "utf8");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getErrorMessage(error: unknown): string {
|
|
188
|
+
return error instanceof Error ? error.message : String(error);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Read a bounded cached snapshot for one Pi session. This never invokes third-party code. */
|
|
192
|
+
export function snapshotExternalRuns(sessionId: string, options: ExternalRunSnapshotOptions = {}): readonly ExternalRun[] {
|
|
193
|
+
const safeSessionId = identity(sessionId, "External-run snapshot sessionId", EXTERNAL_RUN_LIMITS.maxSessionIdLength);
|
|
104
194
|
const current = registry();
|
|
105
|
-
|
|
106
|
-
current.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (provider.name !== name) throw new Error(`External-run registry key '${name}' does not match provider name '${provider.name}'.`);
|
|
117
|
-
const runs = provider.listExternalRuns();
|
|
118
|
-
if (!Array.isArray(runs)) throw new Error(`External-run provider '${name}' listExternalRuns() must return an array.`);
|
|
119
|
-
if (runs.length > MAX_RUNS_PER_PROVIDER) throw new Error(`External-run provider '${name}' returned more than ${MAX_RUNS_PER_PROVIDER} runs.`);
|
|
120
|
-
runs.forEach((value, index) => {
|
|
121
|
-
const run = validateRun(value, name, index);
|
|
122
|
-
const identity = `${name}\0${run.sessionId}\0${run.id}`;
|
|
123
|
-
if (identities.has(identity)) throw new Error(`External-run provider '${name}' returned duplicate run '${run.id}' for session '${run.sessionId}'.`);
|
|
124
|
-
identities.add(identity);
|
|
125
|
-
if (run.sessionId === sessionId) records.push({ provider: name, ...run });
|
|
126
|
-
});
|
|
195
|
+
const runs: ExternalRun[] = [];
|
|
196
|
+
for (const [cacheKey, value] of current.runs.entries()) {
|
|
197
|
+
try {
|
|
198
|
+
const run = validateRun(value);
|
|
199
|
+
if (run.sessionId === safeSessionId) runs.push(run);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const message = `Malformed cached external run '${cacheKey}': ${getErrorMessage(error)}`;
|
|
202
|
+
if (!options.ignoreMalformed) throw new Error(message, { cause: error instanceof Error ? error : undefined });
|
|
203
|
+
current.runs.delete(cacheKey);
|
|
204
|
+
options.onMalformedRecord?.(message);
|
|
205
|
+
}
|
|
127
206
|
}
|
|
128
|
-
|
|
207
|
+
runs.sort((left, right) => {
|
|
208
|
+
const leftActive = left.state === "queued" || left.state === "running";
|
|
209
|
+
const rightActive = right.state === "queued" || right.state === "running";
|
|
210
|
+
if (leftActive !== rightActive) return leftActive ? -1 : 1;
|
|
211
|
+
return (right.updatedAt ?? right.endedAt ?? right.startedAt) - (left.updatedAt ?? left.endedAt ?? left.startedAt) || left.id.localeCompare(right.id);
|
|
212
|
+
});
|
|
213
|
+
const snapshot = runs.slice(0, EXTERNAL_RUN_LIMITS.maxSnapshotRuns).map(clone);
|
|
214
|
+
while (snapshot.length > 0 && snapshotBytes(snapshot) > EXTERNAL_RUN_LIMITS.maxSerializedBytes) snapshot.pop();
|
|
215
|
+
return snapshot;
|
|
129
216
|
}
|
|
217
|
+
|
|
218
|
+
/** Alias for callers that prefer list terminology. */
|
|
219
|
+
export const listExternalRuns = snapshotExternalRuns;
|
package/src/api/preflight.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { resolveStepBehavior } from "../shared/settings.ts";
|
|
|
20
20
|
import { agentDefinitionDigest, AGENT_DEFINITION_PROJECTION_VERSION, launchBindingDigest } from "../shared/launch-contract.ts";
|
|
21
21
|
import { DIRS, TEMP_ROOT_DIR } from "../shared/types.ts";
|
|
22
22
|
import { processTerminalCandidatePath, processTerminalPath } from "../runs/background/process-terminal.ts";
|
|
23
|
+
import { resultFilePath } from "../runs/background/result-files.ts";
|
|
23
24
|
import { nestedResultsPath } from "../runs/shared/nested-events.ts";
|
|
24
25
|
|
|
25
26
|
export const SUBAGENT_LAUNCH_CONTRACT_VERSION = 2 as const;
|
|
@@ -259,13 +260,18 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
259
260
|
}
|
|
260
261
|
if (resolvedSkills.missing.length > 0) diagnostics.push({ code: "missing_skill", severity: "error", message: `Missing skills: ${resolvedSkills.missing.join(", ")}` });
|
|
261
262
|
|
|
263
|
+
const externalRunner = agent.runner?.type === "external-cli";
|
|
262
264
|
const availableModels = normalizeAvailableModels(input.availableModels);
|
|
263
265
|
const preferredProvider = input.preferredProvider ?? input.parentModel?.provider;
|
|
264
|
-
const primaryModel =
|
|
266
|
+
const primaryModel = externalRunner
|
|
267
|
+
? undefined
|
|
268
|
+
: resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: discovered.modelScope });
|
|
265
269
|
const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking;
|
|
266
|
-
const model = applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
|
|
267
|
-
const modelCandidates =
|
|
268
|
-
|
|
270
|
+
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
|
|
271
|
+
const modelCandidates = externalRunner
|
|
272
|
+
? []
|
|
273
|
+
: buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, { scope: discovered.modelScope })
|
|
274
|
+
.map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
|
|
269
275
|
let toolPlan: PiLaunchToolPlan;
|
|
270
276
|
try {
|
|
271
277
|
toolPlan = resolvePiLaunchToolPlan({
|
|
@@ -295,9 +301,9 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
295
301
|
: path.join(DIRS.async, runId);
|
|
296
302
|
const lifecycleResultPath = input.nestedRootRunId
|
|
297
303
|
? nestedResultsPath(input.nestedRootRunId, runId)
|
|
298
|
-
:
|
|
304
|
+
: resultFilePath(DIRS.results, runId);
|
|
299
305
|
if (!sessionDir) diagnostics.push({ code: "host_required", severity: "host-required", message: "No sessionRoot/sessionDir was supplied; exact child session paths require the Pi host session-root policy." });
|
|
300
|
-
if (input.availableModels === undefined && (input.model || agent.model || input.parentModel)) {
|
|
306
|
+
if (!externalRunner && input.availableModels === undefined && (input.model || agent.model || input.parentModel)) {
|
|
301
307
|
diagnostics.push({ code: "host_required", severity: "host-required", message: "No availableModels snapshot was supplied; model resolution may differ from the active Pi host registry." });
|
|
302
308
|
}
|
|
303
309
|
if (resolvedSkills.missing.length > 0) {
|
package/src/extension/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { Key } from "@earendil-works/pi-tui";
|
|
4
5
|
import { FLEET_KEYBINDING_ACTIONS, type ArtifactDirPreference, type ExtensionConfig } from "../shared/types.ts";
|
|
5
6
|
import { validateMissionStoreConfig } from "../missions/store.ts";
|
|
6
7
|
import { validateAuthorityPolicy } from "../policy/authority.ts";
|
|
@@ -9,6 +10,21 @@ import { validatePermissionConfig } from "../runs/shared/permissions.ts";
|
|
|
9
10
|
|
|
10
11
|
const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
|
|
11
12
|
const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
|
|
13
|
+
const KEY_MODIFIERS = new Set(["ctrl", "shift", "alt", "super"]);
|
|
14
|
+
const BASE_KEY_IDS = new Set([
|
|
15
|
+
..."abcdefghijklmnopqrstuvwxyz0123456789",
|
|
16
|
+
...Object.values(Key).flatMap((value) => typeof value === "string" ? [value.toLowerCase()] : []),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function isValidKeyId(value: string): boolean {
|
|
20
|
+
if (value !== value.trim()) return false;
|
|
21
|
+
const parts = value.toLowerCase().split("+");
|
|
22
|
+
const base = parts.pop();
|
|
23
|
+
if (!base || !BASE_KEY_IDS.has(base)) return false;
|
|
24
|
+
return parts.length <= KEY_MODIFIERS.size
|
|
25
|
+
&& new Set(parts).size === parts.length
|
|
26
|
+
&& parts.every((modifier) => KEY_MODIFIERS.has(modifier));
|
|
27
|
+
}
|
|
12
28
|
|
|
13
29
|
export function resolveScheduledStoreRoot(value: string): string {
|
|
14
30
|
const expanded = value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
@@ -46,6 +62,18 @@ function validateArtifactConfig(value: unknown): void {
|
|
|
46
62
|
}
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
function validateOrcaProgressTabsConfig(value: unknown): void {
|
|
66
|
+
if (value === undefined) return;
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.orcaProgressTabs must be a JSON object");
|
|
68
|
+
const config = value as Record<string, unknown>;
|
|
69
|
+
for (const key of Object.keys(config)) {
|
|
70
|
+
if (key !== "enabled") throw new Error(`config.orcaProgressTabs.${key} is not supported`);
|
|
71
|
+
}
|
|
72
|
+
if (config.enabled !== undefined && typeof config.enabled !== "boolean") {
|
|
73
|
+
throw new Error("config.orcaProgressTabs.enabled must be a boolean");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
function validateMainWindowRendererConfig(value: unknown): void {
|
|
50
78
|
if (value === undefined) return;
|
|
51
79
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.mainWindowRenderer must be a JSON object");
|
|
@@ -66,6 +94,10 @@ function validateMainWindowRendererConfig(value: unknown): void {
|
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
function validateConfig(config: Record<string, unknown>): void {
|
|
97
|
+
if (config.foregroundDetachShortcut !== undefined
|
|
98
|
+
&& (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
|
|
99
|
+
throw new Error("config.foregroundDetachShortcut must be a valid keybinding string such as \"ctrl+b\"");
|
|
100
|
+
}
|
|
69
101
|
if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
|
|
70
102
|
throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
|
|
71
103
|
}
|
|
@@ -85,6 +117,7 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
85
117
|
validateFleetKeybindingsConfig(config.fleetKeybindings);
|
|
86
118
|
validateArtifactConfig(config.artifactConfig);
|
|
87
119
|
validateMainWindowRendererConfig(config.mainWindowRenderer);
|
|
120
|
+
validateOrcaProgressTabsConfig(config.orcaProgressTabs);
|
|
88
121
|
}
|
|
89
122
|
|
|
90
123
|
export function getConfigPath(): string {
|
package/src/extension/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ 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
33
|
import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
34
|
+
import { cleanupResultIndexes } from "../runs/background/result-files.ts";
|
|
34
35
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
35
36
|
import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
|
|
36
37
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
@@ -82,6 +83,13 @@ import {
|
|
|
82
83
|
|
|
83
84
|
export { loadConfig, resolveAsyncByDefault } from "./config.ts";
|
|
84
85
|
|
|
86
|
+
const SLOW_RELOAD_PHASE_MS = 250;
|
|
87
|
+
|
|
88
|
+
function logSlowPhase(label: string, startedAt: number): void {
|
|
89
|
+
const elapsed = Date.now() - startedAt;
|
|
90
|
+
if (elapsed >= SLOW_RELOAD_PHASE_MS) console.error(`Subagent reload phase '${label}' took ${elapsed}ms.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
function workflowLaneKeys(script: string): string[] {
|
|
86
94
|
const keys: string[] = [];
|
|
87
95
|
const seen = new Set<string>();
|
|
@@ -255,12 +263,13 @@ function rebuildSlashResultContainer(
|
|
|
255
263
|
options: { expanded: boolean },
|
|
256
264
|
theme: ExtensionContext["ui"]["theme"],
|
|
257
265
|
rendererConfig?: MainWindowRendererConfig,
|
|
266
|
+
foregroundDetachShortcut?: string,
|
|
258
267
|
): void {
|
|
259
268
|
container.clear();
|
|
260
269
|
container.addChild(new Spacer(1));
|
|
261
270
|
const boxTheme = isSlashResultRunning(result) ? "toolPendingBg" : isSlashResultError(result) ? "toolErrorBg" : "toolSuccessBg";
|
|
262
271
|
const box = new Box(1, 1, (text: string) => theme.bg(boxTheme, text));
|
|
263
|
-
box.addChild(renderSubagentResult(result, options, theme, undefined, rendererConfig));
|
|
272
|
+
box.addChild(renderSubagentResult(result, options, theme, undefined, rendererConfig, foregroundDetachShortcut));
|
|
264
273
|
container.addChild(box);
|
|
265
274
|
}
|
|
266
275
|
|
|
@@ -269,6 +278,7 @@ function createSlashResultComponent(
|
|
|
269
278
|
options: { expanded: boolean },
|
|
270
279
|
theme: ExtensionContext["ui"]["theme"],
|
|
271
280
|
rendererConfig?: MainWindowRendererConfig,
|
|
281
|
+
foregroundDetachShortcut?: string,
|
|
272
282
|
): Container {
|
|
273
283
|
const container = new Container();
|
|
274
284
|
let lastVersion = -1;
|
|
@@ -276,7 +286,7 @@ function createSlashResultComponent(
|
|
|
276
286
|
const snapshot = getSlashRenderableSnapshot(details);
|
|
277
287
|
if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
|
|
278
288
|
lastVersion = snapshot.version;
|
|
279
|
-
rebuildSlashResultContainer(container, snapshot.result, options, theme, rendererConfig);
|
|
289
|
+
rebuildSlashResultContainer(container, snapshot.result, options, theme, rendererConfig, foregroundDetachShortcut);
|
|
280
290
|
}
|
|
281
291
|
return Container.prototype.render.call(container, width);
|
|
282
292
|
};
|
|
@@ -373,6 +383,14 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
373
383
|
const tempArtifactsDir = getArtifactsDir(null);
|
|
374
384
|
const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
|
|
375
385
|
cleanupAllArtifactDirs(artifactCleanupDays);
|
|
386
|
+
const resultIndexCleanupTimer = setTimeout(() => {
|
|
387
|
+
try {
|
|
388
|
+
cleanupResultIndexes(DIRS.results);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error("Failed to clean stale subagent result indexes:", error);
|
|
391
|
+
}
|
|
392
|
+
}, 30_000);
|
|
393
|
+
resultIndexCleanupTimer.unref?.();
|
|
376
394
|
|
|
377
395
|
const state: SubagentState = {
|
|
378
396
|
baseCwd: "",
|
|
@@ -455,6 +473,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
455
473
|
);
|
|
456
474
|
|
|
457
475
|
const runtimeCleanup = () => {
|
|
476
|
+
clearTimeout(resultIndexCleanupTimer);
|
|
458
477
|
stopResultWatcher();
|
|
459
478
|
state.currentSessionId = null;
|
|
460
479
|
completionNotifier.dispose();
|
|
@@ -485,7 +504,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
485
504
|
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
486
505
|
const details = resolveSlashMessageDetails(message.details);
|
|
487
506
|
if (!details) return undefined;
|
|
488
|
-
return createSlashResultComponent(details, options, theme, config.mainWindowRenderer);
|
|
507
|
+
return createSlashResultComponent(details, options, theme, config.mainWindowRenderer, config.foregroundDetachShortcut);
|
|
489
508
|
});
|
|
490
509
|
|
|
491
510
|
pi.registerMessageRenderer<undefined>(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
|
|
@@ -613,7 +632,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
613
632
|
const renderedResult = { ...result, isError: context.isError };
|
|
614
633
|
return summaryInlineToolDisplay
|
|
615
634
|
? renderSubagentSummary(renderedResult, options, theme)
|
|
616
|
-
: renderSubagentResult(renderedResult, options, theme, undefined, config.mainWindowRenderer);
|
|
635
|
+
: renderSubagentResult(renderedResult, options, theme, undefined, config.mainWindowRenderer, config.foregroundDetachShortcut);
|
|
617
636
|
},
|
|
618
637
|
|
|
619
638
|
};
|
|
@@ -643,7 +662,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
643
662
|
}
|
|
644
663
|
});
|
|
645
664
|
|
|
646
|
-
registerSlashCommands(pi, state, {
|
|
665
|
+
registerSlashCommands(pi, state, {
|
|
666
|
+
fleetKeybindings: config.fleetKeybindings,
|
|
667
|
+
foregroundDetachShortcut: config.foregroundDetachShortcut,
|
|
668
|
+
});
|
|
647
669
|
|
|
648
670
|
const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
|
|
649
671
|
const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
|
|
@@ -783,18 +805,38 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
783
805
|
}
|
|
784
806
|
}
|
|
785
807
|
state.lastUiContext = ctx;
|
|
808
|
+
let phaseStartedAt = Date.now();
|
|
786
809
|
refreshActiveAsyncCapacity();
|
|
810
|
+
logSlowPhase("active-capacity", phaseStartedAt);
|
|
811
|
+
phaseStartedAt = Date.now();
|
|
787
812
|
cleanupSessionArtifacts(ctx);
|
|
813
|
+
logSlowPhase("session-artifact-cleanup", phaseStartedAt);
|
|
788
814
|
state.foregroundControls.clear();
|
|
789
815
|
state.lastForegroundControlId = null;
|
|
816
|
+
phaseStartedAt = Date.now();
|
|
790
817
|
resetJobs(ctx);
|
|
818
|
+
logSlowPhase("reset-jobs", phaseStartedAt);
|
|
819
|
+
phaseStartedAt = Date.now();
|
|
791
820
|
restoreForegroundRunHistory(state, { resultsDir: DIRS.results });
|
|
821
|
+
logSlowPhase("foreground-history", phaseStartedAt);
|
|
822
|
+
phaseStartedAt = Date.now();
|
|
792
823
|
restoreActiveJobs(ctx);
|
|
824
|
+
logSlowPhase("active-job-restore", phaseStartedAt);
|
|
825
|
+
phaseStartedAt = Date.now();
|
|
793
826
|
scheduledRunManager.bindSession(ctx);
|
|
827
|
+
logSlowPhase("scheduled-runs", phaseStartedAt);
|
|
828
|
+
phaseStartedAt = Date.now();
|
|
794
829
|
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
830
|
+
logSlowPhase("slash-snapshots", phaseStartedAt);
|
|
831
|
+
phaseStartedAt = Date.now();
|
|
795
832
|
waitSubscriptionManager.restore();
|
|
833
|
+
logSlowPhase("wait-subscriptions", phaseStartedAt);
|
|
834
|
+
phaseStartedAt = Date.now();
|
|
796
835
|
startResultWatcher();
|
|
836
|
+
logSlowPhase("result-watcher-start", phaseStartedAt);
|
|
837
|
+
phaseStartedAt = Date.now();
|
|
797
838
|
primeExistingResults({ triggerTurn: !recovering });
|
|
839
|
+
logSlowPhase("result-prime", phaseStartedAt);
|
|
798
840
|
fleetStatus?.setContext(ctx);
|
|
799
841
|
};
|
|
800
842
|
|
|
@@ -837,6 +879,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
837
879
|
|
|
838
880
|
pi.on("session_shutdown", async () => {
|
|
839
881
|
state.widgetsSuspended = false;
|
|
882
|
+
clearTimeout(resultIndexCleanupTimer);
|
|
840
883
|
stopResultWatcher();
|
|
841
884
|
state.currentSessionId = null;
|
|
842
885
|
state.parentSessionFile = null;
|
|
@@ -9,6 +9,7 @@ export interface PublicSubagentExecutionParams {
|
|
|
9
9
|
concurrency?: unknown;
|
|
10
10
|
chainDir?: unknown;
|
|
11
11
|
workflowScript?: unknown;
|
|
12
|
+
output?: unknown;
|
|
12
13
|
resume?: unknown;
|
|
13
14
|
clarify?: unknown;
|
|
14
15
|
runFanoutBudget?: unknown;
|
|
@@ -82,10 +83,11 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
82
83
|
if (params.task !== undefined && typeof params.task !== "string") {
|
|
83
84
|
return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
|
|
84
85
|
}
|
|
85
|
-
const { agent: _agent, task: _task, ...workflowDefaults } = params;
|
|
86
|
+
const { agent: _agent, task: _task, output, ...workflowDefaults } = params;
|
|
86
87
|
const child = {
|
|
87
88
|
agent: params.agent.trim(),
|
|
88
89
|
...(params.task !== undefined ? { task: params.task } : {}),
|
|
90
|
+
output: output === undefined ? true : output,
|
|
89
91
|
};
|
|
90
92
|
return {
|
|
91
93
|
ok: true,
|