pi-subagents 0.43.0 → 0.44.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 +22 -0
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +3 -1
- package/src/extension/config.ts +28 -7
- package/src/extension/index.ts +3 -1
- package/src/extension/tool-description.ts +2 -2
- package/src/missions/lifecycle.ts +1 -1
- package/src/missions/workflow-state.ts +188 -15
- package/src/runs/background/async-execution.ts +2 -1
- package/src/runs/background/async-status.ts +2 -0
- package/src/runs/background/run-id-resolver.ts +71 -0
- package/src/runs/background/run-status.ts +3 -2
- package/src/runs/background/subagent-runner.ts +40 -19
- package/src/runs/foreground/chain-clarify.ts +7 -3
- package/src/runs/foreground/execution.ts +25 -19
- package/src/runs/foreground/subagent-executor.ts +92 -29
- package/src/runs/shared/subagent-prompt-runtime.ts +6 -3
- package/src/shared/settings.ts +10 -6
- package/src/shared/types.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.44.0] - 2026-08-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added one automatic enclosing mission and durable workflow state to plain `workflowScript` launches; child runs no longer create separate missions.
|
|
9
|
+
- Added `scheduledRuns.storeRoot` for durable schedules outside project repositories. Thanks to @ProCleiton for #891 and the prior #890 implementation.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Clarified native supervisor messaging and optional external intercom result delivery in the docs and packaged skill.
|
|
13
|
+
- Identify status and transcript targets before the spawn-budget summary in collapsed tool-result cards.
|
|
14
|
+
- Point interactive async-launch guidance to `subagent_wait({ id, nonBlocking: true })` when an explicit wake is needed without blocking the current turn.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Report the explicit workflow execution cwd in async workflow status, job, and result records. Thanks to @nicobailon for #907.
|
|
18
|
+
- Ignore stale extension-context errors from advisory foreground control notifications after reload. Thanks to @alexei-led for #905.
|
|
19
|
+
- Bound inherited portable tool IDs to 64 characters for Codex-compatible child contexts while keeping tool calls and results paired. Thanks to @alexei-led for #903.
|
|
20
|
+
- Prevent boolean chain `output` values from crashing clarify rendering. Thanks to @ftoleedo for #901.
|
|
21
|
+
- Preserve `workflow` mode when asynchronous workflow mission runs complete.
|
|
22
|
+
- Serialize and merge each mission workflow-state write with the latest file so separate workflows do not drop unrelated keys.
|
|
23
|
+
- Preserve `workflowScript` worktree children that detach for supervisor coordination instead of cleaning a live managed worktree. Thanks to @astarktc for #896.
|
|
24
|
+
- Accept schema-valid structured output after a child recovers from an earlier tool error. Thanks to @white-hat for the report in #888.
|
|
25
|
+
|
|
5
26
|
## [0.43.0] - 2026-08-07
|
|
6
27
|
|
|
7
28
|
### Added
|
|
@@ -19,6 +40,7 @@
|
|
|
19
40
|
- Require workflowScript-only persisted schedule targets. Removed legacy agent-target restore conversion.
|
|
20
41
|
|
|
21
42
|
### Fixed
|
|
43
|
+
- Use portable internal ids for async workflow directories and preserve host tool-call ids as correlation metadata. Thanks to @DrunkenDonkey80 for #889.
|
|
22
44
|
- Represent gate normalization with explicit success and failure results, removing ambiguous internal states without changing gate behavior.
|
|
23
45
|
- Preserve live composite child tool-call ids for APIs that normalize them, preventing context rewriting from breaking the next tool-loop turn.
|
|
24
46
|
- Sanitize inherited child tool history ids so forked subagent context stays provider-portable.
|
package/package.json
CHANGED
|
@@ -367,7 +367,9 @@ Use `oracle` as a smart-friend escalation when the parent needs help with trajec
|
|
|
367
367
|
|
|
368
368
|
## Subagent + Intercom Coordination
|
|
369
369
|
|
|
370
|
-
`pi-subagents` includes native supervisor coordination. Child agents can use `contact_supervisor` to ask the exact parent session that spawned them; messages are scoped by parent session id and should not appear in other Pi sessions.
|
|
370
|
+
`pi-subagents` includes native supervisor coordination. Child agents can use `contact_supervisor` to ask the exact parent session that spawned them; messages are scoped by parent session id and should not appear in other Pi sessions. Parents inspect or reply with `subagent_supervisor`. This path does not require `pi-intercom`.
|
|
371
|
+
|
|
372
|
+
This is separate from optional external completion delivery. Set `intercomBridge.resultDelivery: true` only when an external listener consumes and acknowledges `subagent:result-intercom` grouped results. It does not deliver results by itself, and it does not change native supervisor asks or progress updates.
|
|
371
373
|
|
|
372
374
|
Most agents should not call generic `intercom` directly unless bridge instructions provide a target and `contact_supervisor` is unavailable. Do not invent a target. Prefer the tool from the injected bridge instructions.
|
|
373
375
|
|
package/src/extension/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { ArtifactDirPreference, ExtensionConfig } from "../shared/types.ts";
|
|
4
5
|
import { validateMissionStoreConfig } from "../missions/store.ts";
|
|
@@ -8,6 +9,31 @@ import { validatePermissionConfig } from "../runs/shared/permissions.ts";
|
|
|
8
9
|
|
|
9
10
|
const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
|
|
10
11
|
|
|
12
|
+
export function resolveScheduledStoreRoot(value: string): string {
|
|
13
|
+
const expanded = value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
14
|
+
if (!path.isAbsolute(expanded)) throw new Error(`config.scheduledRuns.storeRoot must be an absolute path or "~/...", got ${JSON.stringify(value)}`);
|
|
15
|
+
return path.normalize(expanded);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateScheduledRunsConfig(value: unknown): void {
|
|
19
|
+
if (value === undefined) return;
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.scheduledRuns must be a JSON object");
|
|
21
|
+
const storeRoot = (value as Record<string, unknown>).storeRoot;
|
|
22
|
+
if (storeRoot === undefined) return;
|
|
23
|
+
if (typeof storeRoot !== "string" || !storeRoot.trim()) throw new Error("config.scheduledRuns.storeRoot must be a non-empty string");
|
|
24
|
+
resolveScheduledStoreRoot(storeRoot);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateConfig(config: Record<string, unknown>): void {
|
|
28
|
+
if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
|
|
29
|
+
throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
|
|
30
|
+
}
|
|
31
|
+
validateMissionStoreConfig(config.missions);
|
|
32
|
+
validateAuthorityPolicy(config.authorityPolicy);
|
|
33
|
+
validatePermissionConfig(config.permissions);
|
|
34
|
+
validateScheduledRunsConfig(config.scheduledRuns);
|
|
35
|
+
}
|
|
36
|
+
|
|
11
37
|
export function getConfigPath(): string {
|
|
12
38
|
return path.join(getAgentDir(), "extensions", "subagent", "config.json");
|
|
13
39
|
}
|
|
@@ -18,13 +44,7 @@ function readConfigForUpdate(configPath = getConfigPath()): ExtensionConfig {
|
|
|
18
44
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
19
45
|
throw new Error(`Subagent config at '${configPath}' must be a JSON object`);
|
|
20
46
|
}
|
|
21
|
-
|
|
22
|
-
if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
|
|
23
|
-
throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
|
|
24
|
-
}
|
|
25
|
-
validateMissionStoreConfig(config.missions);
|
|
26
|
-
validateAuthorityPolicy(config.authorityPolicy);
|
|
27
|
-
validatePermissionConfig(config.permissions);
|
|
47
|
+
validateConfig(parsed as Record<string, unknown>);
|
|
28
48
|
return parsed as ExtensionConfig;
|
|
29
49
|
}
|
|
30
50
|
|
|
@@ -36,6 +56,7 @@ export function saveConfig(config: ExtensionConfig, configPath = getConfigPath()
|
|
|
36
56
|
export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConfig): ExtensionConfig {
|
|
37
57
|
const configPath = getConfigPath();
|
|
38
58
|
const next = updater(readConfigForUpdate(configPath));
|
|
59
|
+
validateConfig(next as Record<string, unknown>);
|
|
39
60
|
saveConfig(next, configPath);
|
|
40
61
|
return next;
|
|
41
62
|
}
|
package/src/extension/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_M
|
|
|
50
50
|
import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
|
|
51
51
|
import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
52
52
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
53
|
-
import { loadConfig, resolveAsyncByDefault } from "./config.ts";
|
|
53
|
+
import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
|
|
54
54
|
import { buildSubagentToolDescription } from "./tool-description.ts";
|
|
55
55
|
import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
|
|
56
56
|
import { syncMissionFromAsyncCompletion } from "../missions/lifecycle.ts";
|
|
@@ -383,8 +383,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
383
383
|
: undefined;
|
|
384
384
|
let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
|
|
385
385
|
let goalTurnId = 0;
|
|
386
|
+
const scheduledStoreRoot = config.scheduledRuns?.storeRoot === undefined ? undefined : resolveScheduledStoreRoot(config.scheduledRuns.storeRoot);
|
|
386
387
|
const scheduledRunManager = createScheduledRunManager({
|
|
387
388
|
config,
|
|
389
|
+
storeRoot: scheduledStoreRoot,
|
|
388
390
|
launch: (params, ctx, signal) => {
|
|
389
391
|
if (!executorScheduled) {
|
|
390
392
|
return Promise.resolve({
|
|
@@ -18,7 +18,7 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run subagents only through { work
|
|
|
18
18
|
|
|
19
19
|
EXECUTION:
|
|
20
20
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
21
|
-
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Every execution is a workflow. 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. 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 {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, and resume keeps the stored agent/model/tool contract. 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, emit, console, and standard JavaScript only.
|
|
21
|
+
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Every execution is a workflow. 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. 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 {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, and resume keeps the stored agent/model/tool contract. 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, 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
22
|
• 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
23
|
• 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
24
|
• 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.
|
|
@@ -37,7 +37,7 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run subagents only through { w
|
|
|
37
37
|
|
|
38
38
|
EXECUTE:
|
|
39
39
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
40
|
-
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. 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.
|
|
40
|
+
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. 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. 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. 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
41
|
• 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
42
|
• 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.
|
|
43
43
|
|
|
@@ -339,7 +339,7 @@ export function syncMissionFromAsyncCompletion(value: unknown): MissionRecord |
|
|
|
339
339
|
: undefined);
|
|
340
340
|
return updateMission(binding.location, binding.missionId, {
|
|
341
341
|
status: missionStatusForRun(current, runId, runStatus),
|
|
342
|
-
addRuns: [{ runId, mode: typeof event.mode === "string" && ["single", "parallel", "chain"].includes(event.mode) ? event.mode as SubagentRunMode : "external", asyncDir: event.asyncDir, status: runStatus, completedAt, ...(usage && usage.tokens > 0 ? { usage } : {}) }],
|
|
342
|
+
addRuns: [{ runId, mode: typeof event.mode === "string" && ["single", "parallel", "chain", "workflow"].includes(event.mode) ? event.mode as SubagentRunMode : "external", asyncDir: event.asyncDir, status: runStatus, completedAt, ...(usage && usage.tokens > 0 ? { usage } : {}) }],
|
|
343
343
|
addArtifacts: artifacts,
|
|
344
344
|
...(summary ? { summary } : {}),
|
|
345
345
|
});
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
1
3
|
import * as fs from "node:fs";
|
|
2
4
|
import * as path from "node:path";
|
|
3
5
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
6
|
+
import { DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS, waitForFileSystemRetry } from "../shared/file-system-retry.ts";
|
|
4
7
|
import { assertWorkflowJsonValue } from "../workflows/scripted-workflow.ts";
|
|
5
8
|
import type { MissionStoreLocation } from "./types.ts";
|
|
6
9
|
import { validateMissionId } from "./store.ts";
|
|
7
10
|
|
|
8
11
|
const STATE_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
12
|
+
const STATE_LOCK_STALE_MS = 60_000;
|
|
9
13
|
export const MISSION_STATE_MAX_BYTES = 256 * 1024;
|
|
10
14
|
|
|
11
15
|
export interface MissionWorkflowState {
|
|
@@ -18,6 +22,172 @@ export function missionStatePath(location: MissionStoreLocation, missionId: stri
|
|
|
18
22
|
return path.join(location.missionDir, validateMissionId(missionId), "state.json");
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
function isProcessAlive(pid: number): boolean {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface StateLockOwner {
|
|
35
|
+
pid: number;
|
|
36
|
+
token: string;
|
|
37
|
+
createdAt: number;
|
|
38
|
+
processKey?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function linuxProcessStartKey(pid: number): string | undefined {
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
44
|
+
const tail = raw.slice(raw.lastIndexOf(")") + 2).trim().split(/\s+/);
|
|
45
|
+
return tail[19] ? `linux:${tail[19]}` : undefined;
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function psProcessStartKey(pid: number): string | undefined {
|
|
52
|
+
try {
|
|
53
|
+
const raw = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 1000 }).trim();
|
|
54
|
+
return raw ? `ps:${raw}` : undefined;
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function windowsProcessStartKey(pid: number): string | undefined {
|
|
61
|
+
try {
|
|
62
|
+
const raw = execFileSync("powershell.exe", ["-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter \"ProcessId=${pid}\").CreationDate`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 1000 }).trim();
|
|
63
|
+
return raw ? `win:${raw}` : undefined;
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function processStartKey(pid: number): string | undefined {
|
|
70
|
+
if (process.platform === "linux") return linuxProcessStartKey(pid) ?? psProcessStartKey(pid);
|
|
71
|
+
if (process.platform === "win32") return windowsProcessStartKey(pid);
|
|
72
|
+
return psProcessStartKey(pid);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CURRENT_PROCESS_KEY = processStartKey(process.pid);
|
|
76
|
+
|
|
77
|
+
function readStateLockOwner(lockPath: string): StateLockOwner | undefined {
|
|
78
|
+
try {
|
|
79
|
+
const owner = JSON.parse(fs.readFileSync(path.join(lockPath, "owner.json"), "utf-8")) as { pid?: unknown; token?: unknown; createdAt?: unknown; processKey?: unknown };
|
|
80
|
+
if (Number.isSafeInteger(owner.pid) && (owner.pid as number) > 0 && typeof owner.token === "string" && owner.token && Number.isSafeInteger(owner.createdAt)) {
|
|
81
|
+
return {
|
|
82
|
+
pid: owner.pid as number,
|
|
83
|
+
token: owner.token,
|
|
84
|
+
createdAt: owner.createdAt as number,
|
|
85
|
+
...(typeof owner.processKey === "string" && owner.processKey ? { processKey: owner.processKey } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function stateLockIsStale(lockPath: string, now = Date.now()): boolean {
|
|
95
|
+
const owner = readStateLockOwner(lockPath);
|
|
96
|
+
if (owner) {
|
|
97
|
+
if (!isProcessAlive(owner.pid)) return true;
|
|
98
|
+
const currentProcessKey = processStartKey(owner.pid);
|
|
99
|
+
return Boolean(owner.processKey && currentProcessKey && owner.processKey !== currentProcessKey);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return now - fs.statSync(lockPath).mtimeMs > STATE_LOCK_STALE_MS;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function removeOwnedStateLock(lockPath: string, owner: StateLockOwner): void {
|
|
110
|
+
const current = readStateLockOwner(lockPath);
|
|
111
|
+
if (current?.token !== owner.token) return;
|
|
112
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function staleDirectoryExists(dirPath: string, now = Date.now()): boolean {
|
|
116
|
+
try {
|
|
117
|
+
return now - fs.statSync(dirPath).mtimeMs > STATE_LOCK_STALE_MS;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function tryMakeDirectory(dirPath: string, mode: number): boolean {
|
|
125
|
+
try {
|
|
126
|
+
fs.mkdirSync(dirPath, { mode });
|
|
127
|
+
return true;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function waitForStateLock(delayMs: number | undefined, lockPath: string): void {
|
|
135
|
+
if (delayMs === undefined) throw new Error(`Timed out acquiring mission state lock '${lockPath}'.`);
|
|
136
|
+
waitForFileSystemRetry(delayMs);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function reclaimStaleStateLock(lockPath: string, reclaimPath: string): boolean {
|
|
140
|
+
if (!stateLockIsStale(lockPath)) return false;
|
|
141
|
+
if (!tryMakeDirectory(reclaimPath, 0o700)) return false;
|
|
142
|
+
try {
|
|
143
|
+
if (!stateLockIsStale(lockPath)) return false;
|
|
144
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
145
|
+
return true;
|
|
146
|
+
} finally {
|
|
147
|
+
fs.rmSync(reclaimPath, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function withStateFileLock<T>(filePath: string, operation: () => T): T {
|
|
152
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
153
|
+
const lockPath = `${filePath}.lock`;
|
|
154
|
+
const reclaimPath = `${lockPath}.reclaim`;
|
|
155
|
+
let owner: StateLockOwner | undefined;
|
|
156
|
+
for (let attempt = 0; ; attempt++) {
|
|
157
|
+
if (fs.existsSync(reclaimPath)) {
|
|
158
|
+
if (staleDirectoryExists(reclaimPath)) {
|
|
159
|
+
fs.rmSync(reclaimPath, { recursive: true, force: true });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
waitForStateLock(DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS[attempt], lockPath);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
fs.mkdirSync(lockPath, { mode: 0o700 });
|
|
167
|
+
owner = { pid: process.pid, token: randomUUID(), createdAt: Date.now(), ...(CURRENT_PROCESS_KEY ? { processKey: CURRENT_PROCESS_KEY } : {}) };
|
|
168
|
+
try {
|
|
169
|
+
fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify(owner), { encoding: "utf-8", mode: 0o600 });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
removeOwnedStateLock(lockPath, owner);
|
|
172
|
+
owner = undefined;
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
178
|
+
throw new Error(`Failed to acquire mission state lock '${lockPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
179
|
+
}
|
|
180
|
+
if (reclaimStaleStateLock(lockPath, reclaimPath)) continue;
|
|
181
|
+
waitForStateLock(DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS[attempt], lockPath);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return operation();
|
|
186
|
+
} finally {
|
|
187
|
+
if (owner) removeOwnedStateLock(lockPath, owner);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
21
191
|
function validateStateKey(value: unknown): string {
|
|
22
192
|
if (typeof value !== "string" || !STATE_KEY_PATTERN.test(value)) {
|
|
23
193
|
throw new Error("state key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.");
|
|
@@ -30,16 +200,12 @@ export function createMissionWorkflowState(location: MissionStoreLocation, missi
|
|
|
30
200
|
let loaded = false;
|
|
31
201
|
let values: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
32
202
|
|
|
33
|
-
const
|
|
34
|
-
if (loaded) return values;
|
|
203
|
+
const readStateFile = (): Record<string, unknown> => {
|
|
35
204
|
let raw: string;
|
|
36
205
|
try {
|
|
37
206
|
raw = fs.readFileSync(filePath, "utf-8");
|
|
38
207
|
} catch (error) {
|
|
39
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
40
|
-
loaded = true;
|
|
41
|
-
return values;
|
|
42
|
-
}
|
|
208
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return Object.create(null) as Record<string, unknown>;
|
|
43
209
|
throw new Error(`Failed to read mission state '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
44
210
|
}
|
|
45
211
|
const bytes = Buffer.byteLength(raw);
|
|
@@ -48,14 +214,19 @@ export function createMissionWorkflowState(location: MissionStoreLocation, missi
|
|
|
48
214
|
const parsed: unknown = JSON.parse(raw);
|
|
49
215
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("root must be a JSON object");
|
|
50
216
|
assertWorkflowJsonValue(parsed, "mission state");
|
|
51
|
-
|
|
52
|
-
loaded = true;
|
|
53
|
-
return values;
|
|
217
|
+
return Object.assign(Object.create(null) as Record<string, unknown>, parsed);
|
|
54
218
|
} catch (error) {
|
|
55
219
|
throw new Error(`Invalid mission state file '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
56
220
|
}
|
|
57
221
|
};
|
|
58
222
|
|
|
223
|
+
const load = (): Record<string, unknown> => {
|
|
224
|
+
if (loaded) return values;
|
|
225
|
+
values = readStateFile();
|
|
226
|
+
loaded = true;
|
|
227
|
+
return values;
|
|
228
|
+
};
|
|
229
|
+
|
|
59
230
|
return {
|
|
60
231
|
path: filePath,
|
|
61
232
|
get(key) {
|
|
@@ -66,12 +237,14 @@ export function createMissionWorkflowState(location: MissionStoreLocation, missi
|
|
|
66
237
|
set(key, value) {
|
|
67
238
|
const validKey = validateStateKey(key);
|
|
68
239
|
assertWorkflowJsonValue(value, `state.set('${validKey}') value`);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
240
|
+
withStateFileLock(filePath, () => {
|
|
241
|
+
const next = Object.assign(Object.create(null) as Record<string, unknown>, readStateFile(), { [validKey]: value });
|
|
242
|
+
const bytes = Buffer.byteLength(JSON.stringify(next, null, 2));
|
|
243
|
+
if (bytes > MISSION_STATE_MAX_BYTES) throw new Error(`Mission state exceeds the 256 KiB limit (${bytes} bytes; maximum ${MISSION_STATE_MAX_BYTES} bytes).`);
|
|
244
|
+
writePrivateAtomicJson(filePath, next);
|
|
245
|
+
values = next;
|
|
246
|
+
loaded = true;
|
|
247
|
+
});
|
|
75
248
|
},
|
|
76
249
|
};
|
|
77
250
|
}
|
|
@@ -269,7 +269,8 @@ export function formatAsyncStartedMessage(headline: string, interactive: boolean
|
|
|
269
269
|
? [
|
|
270
270
|
"The async run is detached and running in the background.",
|
|
271
271
|
"You are in an interactive session. By default, return control to the user now; Pi will wake you on completion when the run finishes or needs attention. Do NOT call subagent_wait() merely to wait, and do not run sleep/polling loops to wait for it.",
|
|
272
|
-
"
|
|
272
|
+
"When you need an explicit wake for one known run but do not need same-turn results, call subagent_wait({ id: \"...\", nonBlocking: true }) to arm a subscription and return immediately.",
|
|
273
|
+
"Override the default and call blocking subagent_wait() before ending the turn only when the current request is run-to-completion — for example, the user asked you to report results back here before continuing, or a skill must finish in one turn. In that case, call subagent_wait() to block until the run completes so its results are delivered in this turn instead of deferred.",
|
|
273
274
|
"Otherwise, continue any independent work or return control to the user. Use subagent({ action: \"status\", id: \"...\" }) for a one-shot status/result or to inspect a blocked/stale run, never as a wait loop.",
|
|
274
275
|
]
|
|
275
276
|
: [
|
|
@@ -65,6 +65,7 @@ interface AsyncRunStepSummary {
|
|
|
65
65
|
export interface AsyncRunSummary {
|
|
66
66
|
id: string;
|
|
67
67
|
asyncDir: string;
|
|
68
|
+
toolCallId?: string;
|
|
68
69
|
sessionId?: string;
|
|
69
70
|
state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
|
|
70
71
|
error?: string;
|
|
@@ -288,6 +289,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
288
289
|
return {
|
|
289
290
|
id: status.runId || path.basename(asyncDir),
|
|
290
291
|
asyncDir,
|
|
292
|
+
...(status.toolCallId ? { toolCallId: status.toolCallId } : {}),
|
|
291
293
|
...(status.sessionId ? { sessionId: status.sessionId } : {}),
|
|
292
294
|
state: status.state,
|
|
293
295
|
...(status.error ? { error: status.error } : {}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { DIRS, type SubagentState } from "../../shared/types.ts";
|
|
4
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
4
5
|
import { findAsyncRunPrefixMatches, type AsyncRunLocation } from "./async-resume.ts";
|
|
5
6
|
import { assertSafeNestedId, findNestedRunMatchesById, type NestedRoute, type NestedRunMatch, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
6
7
|
|
|
@@ -27,6 +28,70 @@ function exactAsyncLocation(id: string, asyncDirRoot: string, resultsDir: string
|
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
type AsyncRunMatch = { id: string; location: AsyncRunLocation };
|
|
32
|
+
|
|
33
|
+
type WorkflowResultIdentity = {
|
|
34
|
+
id?: string;
|
|
35
|
+
runId?: string;
|
|
36
|
+
toolCallId?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function readWorkflowResultIdentity(resultPath: string): WorkflowResultIdentity | undefined {
|
|
40
|
+
let parsed: unknown;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as unknown;
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
47
|
+
const record = parsed as Record<string, unknown>;
|
|
48
|
+
return {
|
|
49
|
+
...(typeof record.id === "string" ? { id: record.id } : {}),
|
|
50
|
+
...(typeof record.runId === "string" ? { runId: record.runId } : {}),
|
|
51
|
+
...(typeof record.toolCallId === "string" ? { toolCallId: record.toolCallId } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function directoryEntries(root: string): string[] {
|
|
56
|
+
try {
|
|
57
|
+
return fs.readdirSync(root);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resultPathFor(resultsDir: string, runId: string): string | null {
|
|
65
|
+
const resultPath = path.join(resultsDir, `${runId}.json`);
|
|
66
|
+
return fs.existsSync(resultPath) ? resultPath : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toolCallIdMatches(value: string | undefined, query: string, options: { prefix?: boolean }): boolean {
|
|
70
|
+
if (value === undefined) return false;
|
|
71
|
+
return options.prefix === true ? value.startsWith(query) : value === query;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toolCallIdAsyncLocations(toolCallId: string, asyncDirRoot: string, resultsDir: string, options: { prefix?: boolean } = {}): AsyncRunMatch[] {
|
|
75
|
+
const byId = new Map<string, AsyncRunLocation>();
|
|
76
|
+
for (const entry of directoryEntries(asyncDirRoot)) {
|
|
77
|
+
const asyncDir = path.join(asyncDirRoot, entry);
|
|
78
|
+
const status = readStatus(asyncDir);
|
|
79
|
+
if (!status || !toolCallIdMatches(status.toolCallId, toolCallId, options)) continue;
|
|
80
|
+
const runId = status.runId || entry;
|
|
81
|
+
byId.set(runId, { asyncDir, resultPath: resultPathFor(resultsDir, runId), resolvedId: runId });
|
|
82
|
+
}
|
|
83
|
+
for (const entry of directoryEntries(resultsDir)) {
|
|
84
|
+
if (!entry.endsWith(".json")) continue;
|
|
85
|
+
const resultPath = path.join(resultsDir, entry);
|
|
86
|
+
const identity = readWorkflowResultIdentity(resultPath);
|
|
87
|
+
if (!identity || !toolCallIdMatches(identity.toolCallId, toolCallId, options)) continue;
|
|
88
|
+
const runId = identity.runId ?? identity.id ?? entry.slice(0, -".json".length);
|
|
89
|
+
const asyncDir = path.join(asyncDirRoot, runId);
|
|
90
|
+
byId.set(runId, { asyncDir: fs.existsSync(asyncDir) ? asyncDir : null, resultPath, resolvedId: runId });
|
|
91
|
+
}
|
|
92
|
+
return [...byId.entries()].map(([id, location]) => ({ id, location }));
|
|
93
|
+
}
|
|
94
|
+
|
|
30
95
|
function foregroundIds(state: SubagentState | undefined): string[] {
|
|
31
96
|
if (!state) return [];
|
|
32
97
|
const remembered = state.currentSessionId
|
|
@@ -73,6 +138,9 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
73
138
|
if (hasExactForegroundId(deps.state, id)) return { kind: "foreground", id };
|
|
74
139
|
const exactAsync = exactAsyncLocation(id, asyncDirRoot, resultsDir);
|
|
75
140
|
if (exactAsync) return { kind: "async", id, location: exactAsync };
|
|
141
|
+
const exactToolCallIdMatches = toolCallIdAsyncLocations(id, asyncDirRoot, resultsDir);
|
|
142
|
+
if (exactToolCallIdMatches.length > 1) throw new Error(`Subagent tool-call id '${id}' is ambiguous across async runs. Use the returned asyncId instead.`);
|
|
143
|
+
if (exactToolCallIdMatches[0]) return { kind: "async", id: exactToolCallIdMatches[0].id, location: exactToolCallIdMatches[0].location };
|
|
76
144
|
const exactNested = findNestedRunMatchesById(id, nestedScope ? { scope: nestedScope } : {});
|
|
77
145
|
if (exactNested.length > 1) throw new Error(`Nested run id '${id}' is ambiguous across authorized registries. Provide the full id after stale registries are cleaned up.`);
|
|
78
146
|
if (exactNested[0]) return { kind: "nested", id, match: exactNested[0] };
|
|
@@ -84,6 +152,9 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
|
|
|
84
152
|
for (const match of asyncPrefixMatches(id, asyncDirRoot, resultsDir)) {
|
|
85
153
|
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
86
154
|
}
|
|
155
|
+
for (const match of toolCallIdAsyncLocations(id, asyncDirRoot, resultsDir, { prefix: true })) {
|
|
156
|
+
matches.push({ kind: "async", id: match.id, location: match.location });
|
|
157
|
+
}
|
|
87
158
|
for (const match of findNestedRunMatchesById(id, nestedScope ? { prefix: true, scope: nestedScope } : { prefix: true })) {
|
|
88
159
|
matches.push({ kind: "nested", id: match.run.id, match });
|
|
89
160
|
}
|
|
@@ -382,6 +382,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
382
382
|
const workflowEmitPreview = status.workflow?.emits.length ? formatWorkflowJsonPreview(status.workflow.emits.at(-1), 240) : undefined;
|
|
383
383
|
const lines = [
|
|
384
384
|
`Run: ${status.runId}`,
|
|
385
|
+
status.toolCallId ? `Tool call: ${status.toolCallId}` : undefined,
|
|
385
386
|
missionId ? `Mission: ${missionId}` : undefined,
|
|
386
387
|
`State: ${status.state}`,
|
|
387
388
|
processTerminal ? `Process terminal: ${processTerminal.state}${processTerminal.reason ? ` (${processTerminal.reason})` : ""}` : undefined,
|
|
@@ -454,7 +455,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
454
455
|
if (resultPath) {
|
|
455
456
|
try {
|
|
456
457
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
457
|
-
const data = JSON.parse(raw) as { id?: string; runId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
|
|
458
|
+
const data = JSON.parse(raw) as { id?: string; runId?: string; toolCallId?: string; agent?: string; success?: boolean; summary?: string; output?: string; exitCode?: number; state?: string; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; processSignal?: string | null; sessionFile?: string; parallelHandoff?: { path?: string }; results?: Array<{ agent?: string; output?: string; summary?: string; sessionFile?: string; state?: string; success?: boolean; exitCode?: number | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean; interrupted?: boolean; processSignal?: string | null }> };
|
|
458
459
|
if (params.view === "transcript") {
|
|
459
460
|
try {
|
|
460
461
|
return { content: [{ type: "text", text: formatAsyncResultTranscript(data, resultPath, { index: params.index, lines: params.lines }) }], details: { mode: "single", results: [] } };
|
|
@@ -479,7 +480,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
479
480
|
? "stopped"
|
|
480
481
|
: data.success ? "complete" : data.state === "paused" || data.exitCode === 0 ? "paused" : "failed";
|
|
481
482
|
const runId = data.runId ?? data.id ?? resolvedId;
|
|
482
|
-
const lines = [`Run: ${runId}`, `State: ${status}`, `Result: ${resultPath}`];
|
|
483
|
+
const lines = [`Run: ${runId}`, data.toolCallId ? `Tool call: ${data.toolCallId}` : undefined, `State: ${status}`, `Result: ${resultPath}`].filter((line): line is string => Boolean(line));
|
|
483
484
|
if (data.parallelHandoff?.path) lines.push(`Parallel handoff: ${data.parallelHandoff.path}`);
|
|
484
485
|
const children = Array.isArray(data.results) ? data.results : data.agent ? [{ agent: data.agent, sessionFile: data.sessionFile }] : [];
|
|
485
486
|
lines.push(formatResumeGuidance(runId, children, data.sessionFile, { stopped: status === "stopped" }));
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
|
|
70
70
|
import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
|
|
71
71
|
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
72
|
-
import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
|
|
72
|
+
import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
|
|
73
73
|
import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
|
|
74
74
|
import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
75
75
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
@@ -503,6 +503,8 @@ interface RunPiStreamingResult {
|
|
|
503
503
|
toolBudget?: ToolBudgetState;
|
|
504
504
|
toolBudgetBlocked?: boolean;
|
|
505
505
|
observedMutationAttempt?: boolean;
|
|
506
|
+
structuredOutputToolInvoked?: boolean;
|
|
507
|
+
structuredOutputMessageStartIndex?: number;
|
|
506
508
|
watchdog?: ChildWatchdogStateSnapshot;
|
|
507
509
|
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
|
|
508
510
|
processInstanceId: string;
|
|
@@ -568,6 +570,8 @@ function runPiStreaming(
|
|
|
568
570
|
let turnBudgetMessage: string | undefined;
|
|
569
571
|
let turnBudget: TurnBudgetState | undefined;
|
|
570
572
|
let observedMutationAttempt = false;
|
|
573
|
+
let structuredOutputToolInvoked = false;
|
|
574
|
+
let structuredOutputMessageStartIndex: number | undefined;
|
|
571
575
|
let toolCount = 0;
|
|
572
576
|
const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
|
|
573
577
|
let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
|
|
@@ -658,6 +662,10 @@ function runPiStreaming(
|
|
|
658
662
|
|
|
659
663
|
if (event.type === "tool_execution_start" && event.toolName) {
|
|
660
664
|
toolCount += 1;
|
|
665
|
+
if (event.toolName === "structured_output") {
|
|
666
|
+
structuredOutputToolInvoked = true;
|
|
667
|
+
structuredOutputMessageStartIndex = messages.length;
|
|
668
|
+
}
|
|
661
669
|
observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args);
|
|
662
670
|
const toolArgs = extractToolArgsPreview(event.args ?? {});
|
|
663
671
|
writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
|
|
@@ -923,6 +931,8 @@ function runPiStreaming(
|
|
|
923
931
|
turnBudgetExceeded,
|
|
924
932
|
wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
|
|
925
933
|
observedMutationAttempt,
|
|
934
|
+
structuredOutputToolInvoked,
|
|
935
|
+
structuredOutputMessageStartIndex,
|
|
926
936
|
watchdog: childWatchdogState,
|
|
927
937
|
processInstanceId,
|
|
928
938
|
processCloseObservedAt,
|
|
@@ -949,7 +959,7 @@ function runPiStreaming(
|
|
|
949
959
|
const stderr = stderrTail.text();
|
|
950
960
|
const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
|
|
951
961
|
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
952
|
-
resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId }));
|
|
962
|
+
resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, structuredOutputToolInvoked, structuredOutputMessageStartIndex, watchdog: childWatchdogState, processInstanceId }));
|
|
953
963
|
});
|
|
954
964
|
});
|
|
955
965
|
}
|
|
@@ -1444,31 +1454,42 @@ async function runSingleStep(
|
|
|
1444
1454
|
const runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
|
|
1445
1455
|
cleanupTempDir(tempDir);
|
|
1446
1456
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1457
|
+
let structuredOutput: unknown;
|
|
1458
|
+
let structuredError: string | undefined;
|
|
1459
|
+
let validatedStructuredOutput = false;
|
|
1460
|
+
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError) {
|
|
1461
|
+
if (!run.structuredOutputToolInvoked) {
|
|
1462
|
+
structuredError = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
|
|
1463
|
+
} else {
|
|
1464
|
+
const structured = await readStructuredOutput({
|
|
1465
|
+
schema: effectiveStructuredOutput.schema,
|
|
1466
|
+
schemaPath: effectiveStructuredOutput.schemaPath,
|
|
1467
|
+
outputPath: effectiveStructuredOutput.outputPath,
|
|
1468
|
+
});
|
|
1469
|
+
if (structured.error) structuredError = structured.error;
|
|
1470
|
+
else {
|
|
1471
|
+
structuredOutput = structured.value;
|
|
1472
|
+
validatedStructuredOutput = true;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const errorMessages = validatedStructuredOutput
|
|
1477
|
+
? run.messages.slice(run.structuredOutputMessageStartIndex ?? run.messages.length)
|
|
1478
|
+
: run.messages;
|
|
1479
|
+
const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError
|
|
1480
|
+
? detectSubagentError(errorMessages)
|
|
1481
|
+
: null;
|
|
1451
1482
|
const emptyOutputError = run.exitCode === 0
|
|
1452
1483
|
&& !run.error
|
|
1453
1484
|
&& !toolAvailabilityError
|
|
1485
|
+
&& !structuredError
|
|
1454
1486
|
&& !run.finalOutput.trim()
|
|
1455
|
-
&&
|
|
1487
|
+
&& !validatedStructuredOutput
|
|
1456
1488
|
&& (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
|
|
1457
1489
|
? "Subagent produced no output (possible model cold-start or empty response)."
|
|
1458
1490
|
: undefined;
|
|
1459
|
-
let structuredOutput: unknown;
|
|
1460
|
-
let structuredError: string | undefined;
|
|
1461
|
-
if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError) {
|
|
1462
|
-
const structured = await readStructuredOutput({
|
|
1463
|
-
schema: effectiveStructuredOutput.schema,
|
|
1464
|
-
schemaPath: effectiveStructuredOutput.schemaPath,
|
|
1465
|
-
outputPath: effectiveStructuredOutput.outputPath,
|
|
1466
|
-
});
|
|
1467
|
-
if (structured.error) structuredError = structured.error;
|
|
1468
|
-
else structuredOutput = structured.value;
|
|
1469
|
-
}
|
|
1470
1491
|
const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
|
|
1471
|
-
const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
|
|
1492
|
+
const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
|
|
1472
1493
|
? evaluateCompletionMutationGuard(omitUndefinedProperties({
|
|
1473
1494
|
agent: step.agent,
|
|
1474
1495
|
task: taskForCompletionGuard,
|
|
@@ -532,7 +532,7 @@ export class ChainClarifyComponent implements Component {
|
|
|
532
532
|
buffer = template.split("\n")[0] ?? "";
|
|
533
533
|
} else if (mode === "output") {
|
|
534
534
|
const behavior = this.getEffectiveBehavior(this.selectedStep);
|
|
535
|
-
buffer = behavior.output ===
|
|
535
|
+
buffer = typeof behavior.output === "string" ? behavior.output : "";
|
|
536
536
|
} else if (mode === "reads") {
|
|
537
537
|
const behavior = this.getEffectiveBehavior(this.selectedStep);
|
|
538
538
|
buffer = behavior.reads === false ? "" : (behavior.reads?.join(", ") || "");
|
|
@@ -1165,7 +1165,9 @@ export class ChainClarifyComponent implements Component {
|
|
|
1165
1165
|
|
|
1166
1166
|
const writesValue = behavior.output === false
|
|
1167
1167
|
? th.fg("dim", "(disabled)")
|
|
1168
|
-
: (behavior.output
|
|
1168
|
+
: (typeof behavior.output === "string" && behavior.output
|
|
1169
|
+
? behavior.output
|
|
1170
|
+
: th.fg("dim", "(none)"));
|
|
1169
1171
|
const writesLabel = th.fg("dim", "writes: ");
|
|
1170
1172
|
lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
|
|
1171
1173
|
|
|
@@ -1296,7 +1298,9 @@ export class ChainClarifyComponent implements Component {
|
|
|
1296
1298
|
|
|
1297
1299
|
const writesValue = behavior.output === false
|
|
1298
1300
|
? th.fg("dim", "(disabled)")
|
|
1299
|
-
: (behavior.output
|
|
1301
|
+
: (typeof behavior.output === "string" && behavior.output
|
|
1302
|
+
? behavior.output
|
|
1303
|
+
: th.fg("dim", "(none)"));
|
|
1300
1304
|
const writesLabel = th.fg("dim", "writes: ");
|
|
1301
1305
|
lines.push(this.row(` ${writesLabel}${truncateToWidth(writesValue, innerW - 14)}`));
|
|
1302
1306
|
|
|
@@ -459,6 +459,7 @@ async function runSingleAttempt(
|
|
|
459
459
|
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
|
|
460
460
|
let observedMutationAttempt = false;
|
|
461
461
|
let structuredOutputToolInvoked = false;
|
|
462
|
+
let structuredOutputMessageStartIndex: number | undefined;
|
|
462
463
|
|
|
463
464
|
const exitCode = await new Promise<number>((resolve) => {
|
|
464
465
|
const spawnSpec = getPiSpawnCommand(args);
|
|
@@ -874,7 +875,10 @@ async function runSingleAttempt(
|
|
|
874
875
|
const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
|
|
875
876
|
? evt.args as Record<string, unknown>
|
|
876
877
|
: {};
|
|
877
|
-
if (options.structuredOutput && evt.toolName === "structured_output")
|
|
878
|
+
if (options.structuredOutput && evt.toolName === "structured_output") {
|
|
879
|
+
structuredOutputToolInvoked = true;
|
|
880
|
+
structuredOutputMessageStartIndex = result.messages?.length ?? 0;
|
|
881
|
+
}
|
|
878
882
|
if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
|
|
879
883
|
intercomStarted = true;
|
|
880
884
|
}
|
|
@@ -1183,24 +1187,7 @@ async function runSingleAttempt(
|
|
|
1183
1187
|
if (result.error && result.exitCode === 0) {
|
|
1184
1188
|
result.exitCode = 1;
|
|
1185
1189
|
}
|
|
1186
|
-
|
|
1187
|
-
const messages = result.messages ?? [];
|
|
1188
|
-
const finalText = getFinalOutput(messages);
|
|
1189
|
-
const missingStructuredOutput = options.structuredOutput
|
|
1190
|
-
? !existsSync(options.structuredOutput.outputPath)
|
|
1191
|
-
: false;
|
|
1192
|
-
const errInfo = detectSubagentError(messages);
|
|
1193
|
-
const missingOutput = !finalText?.trim() && (!options.structuredOutput || missingStructuredOutput);
|
|
1194
|
-
if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
|
|
1195
|
-
result.exitCode = 1;
|
|
1196
|
-
result.error = "Subagent produced no output (possible model cold-start or empty response).";
|
|
1197
|
-
} else if (errInfo.hasError) {
|
|
1198
|
-
result.exitCode = errInfo.exitCode ?? 1;
|
|
1199
|
-
result.error = errInfo.details
|
|
1200
|
-
? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
|
|
1201
|
-
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1190
|
+
let validatedStructuredOutput = false;
|
|
1204
1191
|
if (options.structuredOutput && result.exitCode === 0 && !result.error) {
|
|
1205
1192
|
result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
|
|
1206
1193
|
result.structuredOutputPath = options.structuredOutput.outputPath;
|
|
@@ -1220,9 +1207,28 @@ async function runSingleAttempt(
|
|
|
1220
1207
|
result.structuredOutputFailed = true;
|
|
1221
1208
|
} else {
|
|
1222
1209
|
result.structuredOutput = structured.value;
|
|
1210
|
+
validatedStructuredOutput = true;
|
|
1223
1211
|
}
|
|
1224
1212
|
}
|
|
1225
1213
|
}
|
|
1214
|
+
if (result.exitCode === 0 && !result.error) {
|
|
1215
|
+
const messages = result.messages ?? [];
|
|
1216
|
+
const finalText = getFinalOutput(messages);
|
|
1217
|
+
const errorMessages = validatedStructuredOutput
|
|
1218
|
+
? messages.slice(structuredOutputMessageStartIndex ?? messages.length)
|
|
1219
|
+
: messages;
|
|
1220
|
+
const errInfo = detectSubagentError(errorMessages);
|
|
1221
|
+
const missingOutput = !finalText?.trim() && !validatedStructuredOutput;
|
|
1222
|
+
if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
|
|
1223
|
+
result.exitCode = 1;
|
|
1224
|
+
result.error = "Subagent produced no output (possible model cold-start or empty response).";
|
|
1225
|
+
} else if (errInfo.hasError) {
|
|
1226
|
+
result.exitCode = errInfo.exitCode ?? 1;
|
|
1227
|
+
result.error = errInfo.details
|
|
1228
|
+
? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
|
|
1229
|
+
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1226
1232
|
|
|
1227
1233
|
progress.status = result.exitCode === 0 ? "completed" : "failed";
|
|
1228
1234
|
progress.durationMs = Date.now() - startTime;
|
|
@@ -91,11 +91,12 @@ import { inspectSubagentStatus } from "../background/run-status.ts";
|
|
|
91
91
|
import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
|
|
92
92
|
import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
|
|
93
93
|
import { attachMissionToLaunchResult, prepareMissionLaunch, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
|
|
94
|
+
import { updateMission } from "../../missions/store.ts";
|
|
94
95
|
import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
|
|
95
96
|
import { resolveAuthorityDecision } from "../../policy/authority.ts";
|
|
96
97
|
import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
|
|
97
98
|
import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
|
|
98
|
-
import { runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
|
|
99
|
+
import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
|
|
99
100
|
import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
|
|
100
101
|
import {
|
|
101
102
|
cleanupWorktrees,
|
|
@@ -798,6 +799,20 @@ function getAsyncInterruptTarget(
|
|
|
798
799
|
return newest ? { asyncId: newest.asyncId, asyncDir: newest.asyncDir } : undefined;
|
|
799
800
|
}
|
|
800
801
|
|
|
802
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
803
|
+
if (!(error instanceof Error)) return false;
|
|
804
|
+
return /extension ctx is stale|stale after session replacement or reload/i.test(error.message);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function emitAdvisoryControlEvent(pi: ExtensionAPI, channel: string, payload: unknown): void {
|
|
808
|
+
try {
|
|
809
|
+
pi.events.emit(channel, payload);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (isStaleExtensionContextError(error)) return;
|
|
812
|
+
throw error;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
801
816
|
function emitControlNotification(input: {
|
|
802
817
|
pi: ExtensionAPI;
|
|
803
818
|
controlConfig: ResolvedControlConfig;
|
|
@@ -815,10 +830,10 @@ function emitControlNotification(input: {
|
|
|
815
830
|
noticeText: formatControlNoticeMessage(input.event, childIntercomTarget),
|
|
816
831
|
};
|
|
817
832
|
if (input.controlConfig.notifyChannels.includes("event")) {
|
|
818
|
-
input.pi
|
|
833
|
+
emitAdvisoryControlEvent(input.pi, SUBAGENT_CONTROL_EVENT, payload);
|
|
819
834
|
}
|
|
820
835
|
if (input.event.type !== "active_long_running" && input.controlConfig.notifyChannels.includes("intercom") && input.intercomBridge.active && input.intercomBridge.orchestratorTarget) {
|
|
821
|
-
input.pi
|
|
836
|
+
emitAdvisoryControlEvent(input.pi, SUBAGENT_CONTROL_INTERCOM_EVENT, {
|
|
822
837
|
...payload,
|
|
823
838
|
to: input.intercomBridge.orchestratorTarget,
|
|
824
839
|
message: formatControlIntercomMessage(input.event, childIntercomTarget),
|
|
@@ -1865,6 +1880,19 @@ function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
|
|
|
1865
1880
|
return "single";
|
|
1866
1881
|
}
|
|
1867
1882
|
|
|
1883
|
+
function formatStatusTargetLabel(params: Pick<SubagentParamsLike, "dir" | "index" | "view">, targetRunId: string | undefined): string {
|
|
1884
|
+
let target: string;
|
|
1885
|
+
if (targetRunId) {
|
|
1886
|
+
target = `run ${targetRunId}`;
|
|
1887
|
+
} else if (params.dir) {
|
|
1888
|
+
target = `dir ${params.dir}`;
|
|
1889
|
+
} else {
|
|
1890
|
+
target = params.view === "transcript" ? "active run" : "active runs";
|
|
1891
|
+
}
|
|
1892
|
+
if (params.view !== "transcript") return `Status target: ${target}`;
|
|
1893
|
+
return `Transcript target: ${target}${params.index !== undefined ? ` · child ${params.index}` : ""}`;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1868
1896
|
interface AgentDefaultContextPolicy {
|
|
1869
1897
|
params: SubagentParamsLike;
|
|
1870
1898
|
contextForAgent(agentName: string): ContextMode;
|
|
@@ -3323,10 +3351,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3323
3351
|
);
|
|
3324
3352
|
if (errorResult) return errorResult;
|
|
3325
3353
|
|
|
3326
|
-
let
|
|
3354
|
+
let worktreeCleanupHandled = false;
|
|
3355
|
+
let pendingHandoff: Details["parallelHandoff"];
|
|
3327
3356
|
try {
|
|
3328
3357
|
if (worktreeSetup) {
|
|
3329
|
-
writePendingParallelHandoff({
|
|
3358
|
+
pendingHandoff = writePendingParallelHandoff({
|
|
3330
3359
|
manifestPath: parallelHandoffPath(artifactsDir, runId),
|
|
3331
3360
|
runId,
|
|
3332
3361
|
mode: "parallel",
|
|
@@ -3429,10 +3458,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3429
3458
|
updateForegroundNestedProjection(foregroundControl);
|
|
3430
3459
|
attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
|
|
3431
3460
|
}
|
|
3461
|
+
const detached = results.find((result) => result.detached);
|
|
3432
3462
|
let handoff: ReturnType<typeof finalizeParallelWorktreeHandoff> | undefined;
|
|
3433
3463
|
if (worktreeSetup) {
|
|
3434
|
-
|
|
3435
|
-
handoff =
|
|
3464
|
+
worktreeCleanupHandled = true;
|
|
3465
|
+
handoff = detached
|
|
3466
|
+
? { suffix: pendingHandoff ? formatParallelHandoffReference(pendingHandoff) : "", reference: pendingHandoff }
|
|
3467
|
+
: finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
|
|
3436
3468
|
}
|
|
3437
3469
|
const interrupted = results.find((result) => result.interrupted);
|
|
3438
3470
|
const totalCost = sumResultsCost(results);
|
|
@@ -3455,11 +3487,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3455
3487
|
details,
|
|
3456
3488
|
};
|
|
3457
3489
|
}
|
|
3458
|
-
const detachedIndex = results.findIndex((result) => result.detached);
|
|
3459
|
-
const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
|
|
3460
3490
|
if (detached) {
|
|
3491
|
+
const handoffSuffix = handoff?.suffix ? `\n\n${handoff.suffix}` : "";
|
|
3461
3492
|
return {
|
|
3462
|
-
content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached
|
|
3493
|
+
content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.${handoffSuffix}` }],
|
|
3463
3494
|
details,
|
|
3464
3495
|
};
|
|
3465
3496
|
}
|
|
@@ -3508,7 +3539,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3508
3539
|
details,
|
|
3509
3540
|
};
|
|
3510
3541
|
} finally {
|
|
3511
|
-
if (worktreeSetup && !
|
|
3542
|
+
if (worktreeSetup && !worktreeCleanupHandled) cleanupWorktrees(worktreeSetup);
|
|
3512
3543
|
}
|
|
3513
3544
|
}
|
|
3514
3545
|
|
|
@@ -3895,6 +3926,8 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
|
|
|
3895
3926
|
const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
|
|
3896
3927
|
? result.details.results[0].finalOutput
|
|
3897
3928
|
: receiptOutput;
|
|
3929
|
+
const detached = result.details.results.some((child) => child.detached);
|
|
3930
|
+
const ok = result.isError !== true && !detached;
|
|
3898
3931
|
const artifactPaths = new Set<string>();
|
|
3899
3932
|
if (result.details.asyncDir) artifactPaths.add(result.details.asyncDir);
|
|
3900
3933
|
if (result.details.parallelHandoff?.path) artifactPaths.add(result.details.parallelHandoff.path);
|
|
@@ -3906,10 +3939,10 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
|
|
|
3906
3939
|
const structured = result.details.results.map((child) => child.structuredOutput).filter((value) => value !== undefined);
|
|
3907
3940
|
return {
|
|
3908
3941
|
key,
|
|
3909
|
-
ok
|
|
3942
|
+
ok,
|
|
3910
3943
|
...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
|
|
3911
3944
|
output,
|
|
3912
|
-
...(
|
|
3945
|
+
...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
|
|
3913
3946
|
...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
|
|
3914
3947
|
artifactPaths: [...artifactPaths],
|
|
3915
3948
|
results: result.details.results,
|
|
@@ -4123,11 +4156,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4123
4156
|
if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
|
|
4124
4157
|
const chatProgress = chatProgressResult.projection!;
|
|
4125
4158
|
const explicitMission = requestParams.missionId !== undefined || requestParams.mission !== undefined;
|
|
4159
|
+
const autoMission = !explicitMission;
|
|
4160
|
+
const workflowPreview = autoMission ? previewSimpleWorkflowRun(requestParams.workflowScript) : undefined;
|
|
4161
|
+
const previewTask = workflowPreview?.task?.trim() || undefined;
|
|
4162
|
+
const previewAgent = workflowPreview?.agent?.trim() || undefined;
|
|
4163
|
+
const scriptFirstLine = requestParams.workflowScript.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "Workflow";
|
|
4164
|
+
const boundedScriptPreview = scriptFirstLine.length > 100 ? `${scriptFirstLine.slice(0, 97)}...` : scriptFirstLine;
|
|
4165
|
+
const derivedObjective = previewTask || (previewAgent ? `Workflow: ${previewAgent}` : boundedScriptPreview);
|
|
4126
4166
|
let missionBinding: MissionLaunchBinding | undefined;
|
|
4127
4167
|
let missionWarning: string | undefined;
|
|
4128
4168
|
try {
|
|
4129
4169
|
missionBinding = prepareMissionLaunch({
|
|
4130
|
-
params: requestParams,
|
|
4170
|
+
params: autoMission ? { ...requestParams, task: derivedObjective } : requestParams,
|
|
4131
4171
|
projectRoot: workflowCwd,
|
|
4132
4172
|
...(deps.config.missions ? { config: deps.config.missions } : {}),
|
|
4133
4173
|
ownerSessionId: resolveCurrentSessionId(ctx.sessionManager),
|
|
@@ -4136,6 +4176,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4136
4176
|
if (explicitMission) return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
|
|
4137
4177
|
missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
4138
4178
|
}
|
|
4179
|
+
let shouldPatchMissionObjective = autoMission && previewTask === undefined && missionBinding !== undefined;
|
|
4180
|
+
const patchMissionObjective = (task: unknown): void => {
|
|
4181
|
+
if (!shouldPatchMissionObjective || !missionBinding || typeof task !== "string" || !task.trim()) return;
|
|
4182
|
+
shouldPatchMissionObjective = false;
|
|
4183
|
+
const objective = task.trim();
|
|
4184
|
+
const firstLine = objective.split(/\r?\n/, 1)[0]?.trim() || objective;
|
|
4185
|
+
const title = firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
|
|
4186
|
+
try {
|
|
4187
|
+
updateMission(missionBinding.location, missionBinding.missionId, { title, objective });
|
|
4188
|
+
} catch (error) {
|
|
4189
|
+
console.warn(`[pi-subagents] Failed to update automatic mission objective: ${error instanceof Error ? error.message : String(error)}`);
|
|
4190
|
+
}
|
|
4191
|
+
};
|
|
4192
|
+
const detachWorkflowChildMissions = autoMission || missionBinding !== undefined || requestParams.mission === false;
|
|
4139
4193
|
const workflowState = missionBinding ? createMissionWorkflowState(missionBinding.location, missionBinding.missionId) : undefined;
|
|
4140
4194
|
const attachWorkflowMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
|
|
4141
4195
|
if (!missionBinding) return missionWarning ? { ...result, details: { ...result.details, missionWarning } } : result;
|
|
@@ -4149,7 +4203,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4149
4203
|
}
|
|
4150
4204
|
};
|
|
4151
4205
|
if (requestParams.async !== false) {
|
|
4152
|
-
const
|
|
4206
|
+
const toolCallId = _id;
|
|
4207
|
+
const workflowRunId = randomUUID();
|
|
4153
4208
|
const asyncDir = path.join(DIRS.async, workflowRunId);
|
|
4154
4209
|
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
4155
4210
|
const statusPath = path.join(asyncDir, "status.json");
|
|
@@ -4163,13 +4218,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4163
4218
|
deps.state.workflowControllers.set(workflowRunId, controller);
|
|
4164
4219
|
let status: AsyncStatus = {
|
|
4165
4220
|
runId: workflowRunId,
|
|
4221
|
+
toolCallId,
|
|
4166
4222
|
sessionId: currentSessionId ?? undefined,
|
|
4167
4223
|
mode: "workflow",
|
|
4168
4224
|
state: "running",
|
|
4169
4225
|
startedAt,
|
|
4170
4226
|
lastUpdate: startedAt,
|
|
4171
4227
|
...(timeout !== undefined ? { deadlineAt: startedAt + timeout, timeoutMs: timeout } : {}),
|
|
4172
|
-
cwd:
|
|
4228
|
+
cwd: workflowCwd,
|
|
4173
4229
|
pid: process.pid,
|
|
4174
4230
|
steps: [],
|
|
4175
4231
|
workflow: { trace: [], emits: [], console: [] },
|
|
@@ -4192,7 +4248,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4192
4248
|
job.workflow = status.workflow;
|
|
4193
4249
|
}
|
|
4194
4250
|
};
|
|
4195
|
-
const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd:
|
|
4251
|
+
const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
|
|
4196
4252
|
deps.state.asyncJobs.set(workflowRunId, workflowJob);
|
|
4197
4253
|
deps.state.fleetJobs ??= new Map();
|
|
4198
4254
|
deps.state.fleetJobs.set(workflowRunId, workflowJob);
|
|
@@ -4238,7 +4294,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4238
4294
|
if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
|
|
4239
4295
|
const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
|
|
4240
4296
|
if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
|
|
4241
|
-
|
|
4297
|
+
patchMissionObjective(childParams.task);
|
|
4298
|
+
const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions });
|
|
4242
4299
|
const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
|
|
4243
4300
|
workflowResults.push(...result.details.results);
|
|
4244
4301
|
const child = workflowChildResult(key, result);
|
|
@@ -4257,21 +4314,21 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4257
4314
|
status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
|
|
4258
4315
|
persist();
|
|
4259
4316
|
appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
|
|
4260
|
-
writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd:
|
|
4317
|
+
writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
|
|
4261
4318
|
} catch (error) {
|
|
4262
4319
|
const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
|
|
4263
4320
|
const stopped = controller.signal.aborted;
|
|
4264
4321
|
status = compactOptional<AsyncStatus>({ ...status, state: stopped ? "stopped" : "failed", stopped: stopped || undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
|
|
4265
4322
|
persist();
|
|
4266
4323
|
appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
|
|
4267
|
-
writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd:
|
|
4324
|
+
writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
|
|
4268
4325
|
} finally {
|
|
4269
4326
|
deps.state.workflowControllers?.delete(workflowRunId);
|
|
4270
4327
|
}
|
|
4271
4328
|
});
|
|
4272
4329
|
return attachWorkflowMission({
|
|
4273
4330
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
|
|
4274
|
-
details: { mode: "workflow", runId: workflowRunId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
|
|
4331
|
+
details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
|
|
4275
4332
|
});
|
|
4276
4333
|
}
|
|
4277
4334
|
const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = requestParams;
|
|
@@ -4299,7 +4356,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4299
4356
|
if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
|
|
4300
4357
|
const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
|
|
4301
4358
|
if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
|
|
4302
|
-
|
|
4359
|
+
patchMissionObjective(childParams.task);
|
|
4360
|
+
const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" });
|
|
4303
4361
|
const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
|
|
4304
4362
|
workflowResults.push(...result.details.results);
|
|
4305
4363
|
return workflowChildResult(key, result);
|
|
@@ -4572,13 +4630,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4572
4630
|
}
|
|
4573
4631
|
if (action === "status") {
|
|
4574
4632
|
if (!preserveActiveSession) deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
4575
|
-
const withBudget = (result: AgentToolResult<Details>) => withSpawnBudgetStatus(
|
|
4576
|
-
result,
|
|
4577
|
-
deps.state,
|
|
4578
|
-
deps.config,
|
|
4579
|
-
deps.state.currentSessionId,
|
|
4580
|
-
);
|
|
4581
4633
|
const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
|
|
4634
|
+
const hasDirectoryTarget = Boolean(paramsWithResolvedCwd.dir);
|
|
4635
|
+
const targetLabel = formatStatusTargetLabel(paramsWithResolvedCwd, targetRunId);
|
|
4636
|
+
const withBudget = (result: AgentToolResult<Details>) => {
|
|
4637
|
+
const budgeted = withSpawnBudgetStatus(result, deps.state, deps.config, deps.state.currentSessionId);
|
|
4638
|
+
return {
|
|
4639
|
+
...budgeted,
|
|
4640
|
+
content: budgeted.content.map((item, index) => index === 0 && item.type === "text"
|
|
4641
|
+
? { ...item, text: `${targetLabel}\n${item.text}` }
|
|
4642
|
+
: item),
|
|
4643
|
+
};
|
|
4644
|
+
};
|
|
4582
4645
|
const nestedScope = nestedResolutionScopeForExecutor(deps);
|
|
4583
4646
|
const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
|
|
4584
4647
|
if (paramsWithResolvedCwd.view === "fleet") {
|
|
@@ -4603,7 +4666,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4603
4666
|
const message = error instanceof Error ? error.message : String(error);
|
|
4604
4667
|
return withBudget({ content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } });
|
|
4605
4668
|
}
|
|
4606
|
-
} else {
|
|
4669
|
+
} else if (!hasDirectoryTarget) {
|
|
4607
4670
|
const foreground = getForegroundControl(deps.state, undefined);
|
|
4608
4671
|
if (foreground && paramsWithResolvedCwd.view !== "transcript") return withBudget(foregroundStatusResult(foreground));
|
|
4609
4672
|
if (foreground && paramsWithResolvedCwd.view === "transcript") {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -206,16 +207,18 @@ function isSubagentToolCallBlock(block: unknown): boolean {
|
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
const PORTABLE_TOOL_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
210
|
+
const MAX_PORTABLE_TOOL_ID_LENGTH = 64;
|
|
209
211
|
const COMPOSITE_TOOL_ID_APIS = new Set([
|
|
210
212
|
"azure-openai-responses",
|
|
211
|
-
"openai-codex-responses",
|
|
212
213
|
"openai-completions",
|
|
213
214
|
"openai-responses",
|
|
214
215
|
]);
|
|
215
216
|
|
|
216
217
|
function portableToolId(id: string): string {
|
|
217
|
-
if (PORTABLE_TOOL_ID_PATTERN.test(id)) return id;
|
|
218
|
-
|
|
218
|
+
if (PORTABLE_TOOL_ID_PATTERN.test(id) && id.length <= MAX_PORTABLE_TOOL_ID_LENGTH) return id;
|
|
219
|
+
const encoded = `tool_${Buffer.from(id).toString("base64url") || "empty"}`;
|
|
220
|
+
if (encoded.length <= MAX_PORTABLE_TOOL_ID_LENGTH) return encoded;
|
|
221
|
+
return `tool_${createHash("sha256").update(id).digest("base64url")}`;
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
function sanitizeToolHistoryMessage(message: unknown): unknown {
|
package/src/shared/settings.ts
CHANGED
|
@@ -23,8 +23,10 @@ export interface ResolvedStepBehavior {
|
|
|
23
23
|
model?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export type OutputOverrideInput = string | boolean;
|
|
27
|
+
|
|
26
28
|
export interface StepOverrides {
|
|
27
|
-
output?:
|
|
29
|
+
output?: OutputOverrideInput;
|
|
28
30
|
outputMode?: OutputMode;
|
|
29
31
|
reads?: string[] | false;
|
|
30
32
|
progress?: boolean;
|
|
@@ -32,8 +34,10 @@ export interface StepOverrides {
|
|
|
32
34
|
model?: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
function normalizeOutputOverride(output:
|
|
36
|
-
|
|
37
|
+
function normalizeOutputOverride(output: unknown): string | false | undefined {
|
|
38
|
+
if (output === false || output === "false") return false;
|
|
39
|
+
if (output === true || output === "true") return undefined;
|
|
40
|
+
return typeof output === "string" && output.length > 0 ? output : undefined;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
// =============================================================================
|
|
@@ -49,7 +53,7 @@ export interface SequentialStep {
|
|
|
49
53
|
as?: string;
|
|
50
54
|
outputSchema?: JsonSchemaObject;
|
|
51
55
|
cwd?: string;
|
|
52
|
-
output?:
|
|
56
|
+
output?: OutputOverrideInput;
|
|
53
57
|
outputMode?: OutputMode;
|
|
54
58
|
reads?: string[] | false;
|
|
55
59
|
progress?: boolean;
|
|
@@ -71,7 +75,7 @@ export interface ParallelTaskItem {
|
|
|
71
75
|
outputSchema?: JsonSchemaObject;
|
|
72
76
|
cwd?: string;
|
|
73
77
|
count?: number;
|
|
74
|
-
output?:
|
|
78
|
+
output?: OutputOverrideInput;
|
|
75
79
|
outputMode?: OutputMode;
|
|
76
80
|
reads?: string[] | false;
|
|
77
81
|
progress?: boolean;
|
|
@@ -123,7 +127,7 @@ export interface CheckpointStep {
|
|
|
123
127
|
agent?: string;
|
|
124
128
|
task?: string;
|
|
125
129
|
as?: string;
|
|
126
|
-
output?:
|
|
130
|
+
output?: OutputOverrideInput;
|
|
127
131
|
outputMode?: OutputMode;
|
|
128
132
|
reads?: string[] | false;
|
|
129
133
|
progress?: boolean;
|
package/src/shared/types.ts
CHANGED
|
@@ -950,6 +950,8 @@ export interface SpawnBudgetSnapshot {
|
|
|
950
950
|
export interface Details {
|
|
951
951
|
mode: SubagentResultMode | "management";
|
|
952
952
|
runId?: string;
|
|
953
|
+
/** Host tool-call id retained when it differs from the internal run id. */
|
|
954
|
+
toolCallId?: string;
|
|
953
955
|
/** Run-level context summary. "mixed" when children resolved to different modes. */
|
|
954
956
|
context?: "fresh" | "fork" | "mixed";
|
|
955
957
|
results: SingleResult[];
|
|
@@ -1246,6 +1248,8 @@ export interface ExternalProcessStatus {
|
|
|
1246
1248
|
export interface AsyncStatus {
|
|
1247
1249
|
lifecycleArtifactVersion?: SubagentLifecycleArtifactVersion;
|
|
1248
1250
|
runId: string;
|
|
1251
|
+
/** Host tool-call id retained when it differs from the internal run id. */
|
|
1252
|
+
toolCallId?: string;
|
|
1249
1253
|
sessionId?: string;
|
|
1250
1254
|
mode: SubagentRunMode;
|
|
1251
1255
|
context?: "fresh" | "fork" | "mixed";
|
|
@@ -1736,6 +1740,8 @@ export type InlineToolDisplay = "rich" | "summary";
|
|
|
1736
1740
|
export interface ScheduledRunsConfig {
|
|
1737
1741
|
enabled?: boolean;
|
|
1738
1742
|
maxPending?: number;
|
|
1743
|
+
/** Absolute or `~/` root for per-project durable schedules. */
|
|
1744
|
+
storeRoot?: string;
|
|
1739
1745
|
}
|
|
1740
1746
|
|
|
1741
1747
|
export type FleetViewPlacement = "aboveEditor" | "belowEditor";
|