pi-subagents 0.49.0 → 0.51.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 +97 -0
- package/agents/gpt-pro.md +17 -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/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +37 -2
- package/docs/configuration.md +76 -14
- package/docs/extension-api.md +78 -1
- package/docs/missions.md +1 -1
- package/docs/observability.md +20 -4
- package/docs/tool-reference.md +55 -39
- package/docs/workflows.md +171 -5
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
- package/skills/pi-subagents/references/execution-controls.md +22 -18
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +135 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +42 -11
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +36 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +210 -90
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +14 -36
- package/src/extension/tool-description.ts +37 -24
- package/src/inspectors/herdr/actions.ts +5 -9
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +4 -8
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +49 -51
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +94 -1
- package/src/runs/background/async-execution.ts +98 -38
- package/src/runs/background/async-job-tracker.ts +21 -4
- package/src/runs/background/async-resume.ts +30 -17
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +47 -56
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +505 -0
- package/src/runs/background/result-watcher.ts +250 -51
- package/src/runs/background/retained-children.ts +79 -20
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +37 -29
- package/src/runs/background/run-status.ts +34 -20
- package/src/runs/background/scheduled-runs.ts +71 -27
- package/src/runs/background/stale-run-reconciler.ts +33 -16
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +535 -161
- package/src/runs/background/subagent-wait.ts +9 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +22 -2
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +221 -15
- package/src/runs/foreground/subagent-executor.ts +529 -1519
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/external-job-bridge.ts +450 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +34 -3
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +437 -0
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +6 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +41 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +88 -27
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +45 -28
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +186 -28
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +7 -5
- package/src/workflows/scripted-workflow.ts +424 -125
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1565
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
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { discoverAgents, discoverAgentsAll, resolveAgentName, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
|
|
5
|
+
import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
|
|
6
6
|
import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
|
|
7
7
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
|
|
8
8
|
import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
|
|
@@ -17,9 +17,12 @@ import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
|
|
|
17
17
|
import type { ResolvedTurnBudget } from "../shared/types.ts";
|
|
18
18
|
import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
|
|
19
19
|
import { resolveStepBehavior } from "../shared/settings.ts";
|
|
20
|
+
import { canPreferForkFromSnapshot, resolveSubagentLaunchContext } from "../shared/fork-context.ts";
|
|
21
|
+
import { loadConfig } from "../extension/config.ts";
|
|
20
22
|
import { agentDefinitionDigest, AGENT_DEFINITION_PROJECTION_VERSION, launchBindingDigest } from "../shared/launch-contract.ts";
|
|
21
23
|
import { DIRS, TEMP_ROOT_DIR } from "../shared/types.ts";
|
|
22
24
|
import { processTerminalCandidatePath, processTerminalPath } from "../runs/background/process-terminal.ts";
|
|
25
|
+
import { resultFilePath } from "../runs/background/result-files.ts";
|
|
23
26
|
import { nestedResultsPath } from "../runs/shared/nested-events.ts";
|
|
24
27
|
|
|
25
28
|
export const SUBAGENT_LAUNCH_CONTRACT_VERSION = 2 as const;
|
|
@@ -59,6 +62,8 @@ export interface SubagentLaunchContractInput {
|
|
|
59
62
|
artifacts?: boolean;
|
|
60
63
|
artifactDir?: ArtifactDirPreference;
|
|
61
64
|
parentSessionFile?: string | null;
|
|
65
|
+
/** Current parent leaf required before an implicit `defaultContext: fork` stays `fork`. */
|
|
66
|
+
parentLeafId?: string | null;
|
|
62
67
|
sessionRoot?: string;
|
|
63
68
|
sessionDir?: string;
|
|
64
69
|
runId?: string;
|
|
@@ -189,6 +194,18 @@ function normalizeAvailableModels(models: SubagentLaunchContractInput["available
|
|
|
189
194
|
return (models ?? []).map((model) => ({ ...model, fullId: model.fullId ?? `${model.provider}/${model.id}` }));
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
function resolveLaunchContractContext(input: SubagentLaunchContractInput, agent: AgentConfig): "fresh" | "fork" {
|
|
198
|
+
return resolveSubagentLaunchContext({
|
|
199
|
+
explicitContext: input.context,
|
|
200
|
+
agentDefaultContext: agent.defaultContext,
|
|
201
|
+
defaultSubagentContext: loadConfig().defaultSubagentContext,
|
|
202
|
+
canUseImplicitFork: canPreferForkFromSnapshot({
|
|
203
|
+
parentSessionFile: input.parentSessionFile,
|
|
204
|
+
leafId: input.parentLeafId,
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
192
209
|
function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string): SubagentLaunchContractAgentCandidate[] {
|
|
193
210
|
const all = discoverAgentsAll(cwd);
|
|
194
211
|
return [...all.builtin, ...all.package, ...all.user, ...all.project]
|
|
@@ -221,12 +238,17 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
221
238
|
if (input.artifactDir !== undefined && input.artifactDir !== "project" && input.artifactDir !== "session" && input.artifactDir !== "temp") {
|
|
222
239
|
return { ok: false, code: "invalid_artifact_dir", message: `Unsupported artifactDir '${String(input.artifactDir)}'; expected 'project', 'session', or 'temp'.`, diagnostics };
|
|
223
240
|
}
|
|
224
|
-
if (input.context === "fork") {
|
|
225
|
-
diagnostics.push({ code: "host_required", severity: "host-required", message: "Exact fork session branching and fork-thinking downgrade checks require Pi host session and model-registry snapshots." });
|
|
226
|
-
}
|
|
227
241
|
const scope = resolveExecutionAgentScope(input.agentScope);
|
|
228
242
|
const discovered = discoverAgents(effectiveCwd, scope);
|
|
229
243
|
const resolvedAgent = resolveAgentName(input.agent, discovered.agents);
|
|
244
|
+
const ambiguousCandidates = resolvedAgent.error
|
|
245
|
+
? discovered.agents.filter((agent) => resolveAgentName(input.agent, [agent]).agent)
|
|
246
|
+
: resolvedAgent.agent;
|
|
247
|
+
const invalidAgent = findBlockingAgentDiagnostic(input.agent, ambiguousCandidates, discovered.agentDiagnostics);
|
|
248
|
+
if (invalidAgent) {
|
|
249
|
+
const message = `Agent '${input.agent}' has invalid configuration: ${invalidAgent.error}`;
|
|
250
|
+
return { ok: false, code: "missing_agent", message, diagnostics: [{ code: "missing_agent", severity: "error", message }] };
|
|
251
|
+
}
|
|
230
252
|
if (resolvedAgent.error) {
|
|
231
253
|
return { ok: false, code: "ambiguous_agent", message: resolvedAgent.error, diagnostics };
|
|
232
254
|
}
|
|
@@ -234,6 +256,10 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
234
256
|
return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
|
|
235
257
|
}
|
|
236
258
|
const agent = resolvedAgent.agent;
|
|
259
|
+
const context = resolveLaunchContractContext(input, agent);
|
|
260
|
+
if (context === "fork") {
|
|
261
|
+
diagnostics.push({ code: "host_required", severity: "host-required", message: "Exact fork session branching and fork-thinking downgrade checks require Pi host session and model-registry snapshots." });
|
|
262
|
+
}
|
|
237
263
|
const effectiveCapabilityCeiling = intersectSubagentCapabilityCeilings(input.capabilityCeiling, input.inheritedCapabilityCeiling);
|
|
238
264
|
const restrictionMessage = capabilityCeilingAgentRestrictionMessage(agent.name, effectiveCapabilityCeiling);
|
|
239
265
|
if (restrictionMessage) return { ok: false, code: "restricted_agent", message: restrictionMessage, diagnostics };
|
|
@@ -259,13 +285,18 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
259
285
|
}
|
|
260
286
|
if (resolvedSkills.missing.length > 0) diagnostics.push({ code: "missing_skill", severity: "error", message: `Missing skills: ${resolvedSkills.missing.join(", ")}` });
|
|
261
287
|
|
|
288
|
+
const externalRunner = agent.runner?.type === "external-cli" || agent.runner?.type === "external-job";
|
|
262
289
|
const availableModels = normalizeAvailableModels(input.availableModels);
|
|
263
290
|
const preferredProvider = input.preferredProvider ?? input.parentModel?.provider;
|
|
264
|
-
const primaryModel =
|
|
291
|
+
const primaryModel = externalRunner
|
|
292
|
+
? undefined
|
|
293
|
+
: resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: discovered.modelScope });
|
|
265
294
|
const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking;
|
|
266
|
-
const model = applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
|
|
267
|
-
const modelCandidates =
|
|
268
|
-
|
|
295
|
+
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
|
|
296
|
+
const modelCandidates = externalRunner
|
|
297
|
+
? []
|
|
298
|
+
: buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, { scope: discovered.modelScope })
|
|
299
|
+
.map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
|
|
269
300
|
let toolPlan: PiLaunchToolPlan;
|
|
270
301
|
try {
|
|
271
302
|
toolPlan = resolvePiLaunchToolPlan({
|
|
@@ -295,9 +326,9 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
295
326
|
: path.join(DIRS.async, runId);
|
|
296
327
|
const lifecycleResultPath = input.nestedRootRunId
|
|
297
328
|
? nestedResultsPath(input.nestedRootRunId, runId)
|
|
298
|
-
:
|
|
329
|
+
: resultFilePath(DIRS.results, runId);
|
|
299
330
|
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)) {
|
|
331
|
+
if (!externalRunner && input.availableModels === undefined && (input.model || agent.model || input.parentModel)) {
|
|
301
332
|
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
333
|
}
|
|
303
334
|
if (resolvedSkills.missing.length > 0) {
|
|
@@ -329,7 +360,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
329
360
|
definitionDigest,
|
|
330
361
|
shadowedCandidates,
|
|
331
362
|
},
|
|
332
|
-
context
|
|
363
|
+
context,
|
|
333
364
|
...(model ? { model } : {}),
|
|
334
365
|
modelCandidates,
|
|
335
366
|
...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}),
|
package/src/api/shared-types.ts
CHANGED
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,12 +94,16 @@ function validateMainWindowRendererConfig(value: unknown): void {
|
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
function validateConfig(config: Record<string, unknown>): void {
|
|
97
|
+
if (config.defaultSubagentContext !== undefined && config.defaultSubagentContext !== "fresh" && config.defaultSubagentContext !== "fork") {
|
|
98
|
+
throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
|
|
99
|
+
}
|
|
100
|
+
if (config.foregroundDetachShortcut !== undefined
|
|
101
|
+
&& (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
|
|
102
|
+
throw new Error("config.foregroundDetachShortcut must be a valid keybinding string such as \"ctrl+b\"");
|
|
103
|
+
}
|
|
69
104
|
if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
|
|
70
105
|
throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
|
|
71
106
|
}
|
|
72
|
-
if (config.legacyChainControls !== undefined && typeof config.legacyChainControls !== "boolean") {
|
|
73
|
-
throw new Error("config.legacyChainControls must be a boolean");
|
|
74
|
-
}
|
|
75
107
|
if (config.maxActiveAsyncRunsPerSession !== undefined
|
|
76
108
|
&& (typeof config.maxActiveAsyncRunsPerSession !== "number"
|
|
77
109
|
|| !Number.isInteger(config.maxActiveAsyncRunsPerSession)
|
|
@@ -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/doctor.ts
CHANGED
|
@@ -134,7 +134,7 @@ function formatSessionLines(input: DoctorReportInput): string[] {
|
|
|
134
134
|
|
|
135
135
|
function formatDiscovery(input: DoctorReportInput, deps: DoctorDeps): string[] {
|
|
136
136
|
return [
|
|
137
|
-
lineFromCheck("agents
|
|
137
|
+
lineFromCheck("agents", () => {
|
|
138
138
|
const discovered = deps.discoverAgentsAll(input.cwd);
|
|
139
139
|
const agentCounts = {
|
|
140
140
|
builtin: discovered.builtin.length,
|
|
@@ -142,13 +142,10 @@ function formatDiscovery(input: DoctorReportInput, deps: DoctorDeps): string[] {
|
|
|
142
142
|
user: discovered.user.length,
|
|
143
143
|
project: discovered.project.length,
|
|
144
144
|
};
|
|
145
|
-
const
|
|
146
|
-
counts[chain.source] += 1;
|
|
147
|
-
return counts;
|
|
148
|
-
}, { builtin: 0, package: 0, user: 0, project: 0 });
|
|
145
|
+
const diagnostics = discovered.agentDiagnostics ?? [];
|
|
149
146
|
return [
|
|
150
147
|
`- agents: total ${agentCounts.builtin + agentCounts.package + agentCounts.user + agentCounts.project} (${formatSourceCounts(agentCounts)})`,
|
|
151
|
-
`-
|
|
148
|
+
...diagnostics.map((diagnostic) => `- invalid agent ${diagnostic.name ?? diagnostic.filePath} (${diagnostic.source}): ${diagnostic.error}`),
|
|
152
149
|
].join("\n");
|
|
153
150
|
}),
|
|
154
151
|
lineFromCheck("skills", () => {
|
|
@@ -171,13 +171,13 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
171
171
|
allowMutatingManagementActions: false,
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
const params = createSubagentParamsSchema(
|
|
174
|
+
const params = createSubagentParamsSchema();
|
|
175
175
|
const tool: ToolDefinition<typeof params, Details> = {
|
|
176
176
|
name: "subagent",
|
|
177
177
|
label: "Subagent",
|
|
178
178
|
description: [
|
|
179
179
|
"Delegate to subagents from child-safe fanout mode.",
|
|
180
|
-
|
|
180
|
+
"Allowed management/control actions: list, get, status, interrupt, resume, steer, doctor.",
|
|
181
181
|
"Mutating management actions (create, update, delete, eject, disable, enable, reset, grant-spawn-budget) are blocked in this mode.",
|
|
182
182
|
].join("\n"),
|
|
183
183
|
parameters: params,
|