pi-subagents 0.44.0 → 0.45.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 +15 -0
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +10 -0
- package/src/inspectors/herdr/actions.ts +1 -1
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/runs/background/async-execution.ts +10 -2
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/result-watcher.ts +5 -0
- package/src/runs/background/subagent-runner.ts +33 -34
- package/src/runs/background/subagent-wait.ts +12 -2
- package/src/runs/background/wait-completions.ts +112 -0
- package/src/runs/foreground/subagent-executor.ts +69 -4
- package/src/shared/settings.ts +16 -3
- package/src/shared/types.ts +34 -0
- package/src/workflows/scripted-workflow.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.45.0] - 2026-08-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Surface terminal completion payloads in `subagent_wait` tool-result details (`details.completions`): run identity, per-child agent/`runId`/success, and artifact paths. Async completions previously reached the parent only as text — the result file is consumed and deleted after delivery — so extensions and automation had no structured way to learn which runs finished or where their artifacts live. Workflow result files now also record each child's `runId`, which was previously dropped even though the workflow engine knows it; a workflow child's `artifactPaths` entry points at its saved output (`outputs/<runId>/…`), so without the explicit field the child's identity was not recoverable from the payload. Thanks to @lucasgrecco for #915.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Clarified mission-use policy in the packaged `pi-subagents` skill.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Prefix quoted Herdr pane commands with PowerShell's call operator on Windows. Thanks to @qsgy-edge for #921.
|
|
15
|
+
- Report live child activity for async workflow runs instead of deriving a false activity age from the workflow launch time. Thanks to @alexei-led (Alexei Ledenev) for #920.
|
|
16
|
+
- Expand `reads` home paths and apply configured reads to single-run launches. Thanks to @Adjuvant (Thomas Deacon) for #916.
|
|
17
|
+
- Drop late workflow child responses after worker settlement. Thanks to @xz-dev (Xiangzhe) for #922.
|
|
18
|
+
- Stabilize steering recovery tests by invalidating cached status metadata after fast test rewrites.
|
|
19
|
+
|
|
5
20
|
## [0.44.0] - 2026-08-08
|
|
6
21
|
|
|
7
22
|
### Added
|
package/package.json
CHANGED
|
@@ -280,6 +280,16 @@ Ordinary launches with a task create a mission by default, so substantial delega
|
|
|
280
280
|
|
|
281
281
|
Use `mission.update` while work runs to record decisions, artifacts, labels, summaries, or delivery receipts. A receipt records a pull request, CI, deployment, or release link with a concise status; it does not authorize or automate merge, CI polling, or deployment. Record open product, architecture, or safety decisions there and escalate them upward; do not let a child decide silently. Use `mission.attach-run` only for runs launched outside the normal mission-backed path, and use `mission.close` with a terminal status and concise summary when the mission is done.
|
|
282
282
|
|
|
283
|
+
### Mission use policy
|
|
284
|
+
|
|
285
|
+
- **Keep the default.** Every ordinary `workflowScript` launch with a task creates one enclosing mission automatically. All workflow children share it and never get their own. Do not add `mission: {...}` boilerplate. Pass it only to set the title, objective, labels, or to enable `goal` with `budget`.
|
|
286
|
+
- **Use `mission: false` for noise.** Use it for trivial one-shot lookups, scouts, disposable probes, and quick validation where a recovery record is noise. It removes the mission and the `state` global for the whole workflow, so do not use it for monitors or multi-workflow loops that coordinate through `state`. Scheduled runs already launch without automatic missions.
|
|
287
|
+
- **Use `missionId` for follow-up work.** Attach later work to an existing objective with `missionId`; attachment re-marks the mission active. `missionId` and `mission` are mutually exclusive. Explicit attachment fails before launch if the mission is missing, while automatic missions degrade to `details.missionWarning` without blocking the run.
|
|
288
|
+
- **Keep `state` small.** Mission `state` is JSON coordination across workflows on the same mission. Keys use the same format as run keys, values must be JSON, and the whole state file is capped at 256 KiB. Each `set` merges one key under a file lock. Put large content in artifact files and store paths in state. In goal missions, write `state.set("nextReadyAction", "...")` so the next idle-turn notice names the exact ready step.
|
|
289
|
+
- **Use artifacts and receipts as evidence.** Mission-backed launches already record run artifacts such as async `status.json`, `events.jsonl`, child output paths, and handoff manifests. Add `mission.update` artifacts only for extra durable outputs such as `patch`, `review`, or `note` files. Add receipts for external outcomes: `pull_request`, `ci`, `deployment`, or `release`; each receipt needs an absolute URL. Receipts are evidence, not authority to merge, deploy, or release.
|
|
290
|
+
- **Treat decisions as append-only.** `mission.update` `decisions` can only add open decisions. No tool action resolves one. In a goal mission, an unresolved decision becomes the fallback next ready action in each notice. Use decisions sparingly there; record them for escalation and audit, steer goal continuation through `state.nextReadyAction`, and close the mission when the question is settled.
|
|
291
|
+
- **Close missions when done.** `mission.close` takes `missionStatus` `completed`, `failed`, or `cancelled` plus a concise `summary`, and ends any goal loop. Goal notices go only to the owning session and stop silently at `budget-exhausted` without closing or claiming success, so close explicitly. Terminal missions are pruned beyond configured retention, so store durable outputs as artifacts, receipts, and summary before closing.
|
|
292
|
+
|
|
283
293
|
After compaction, restart, or confusing history, recover from durable state first: `mission.list` in the project, `mission.list` with `missionScope: "global"` for the user-local cross-project pointer index, then `mission.show` for the relevant mission. `mission.show` refreshes linked async status when available and returns warnings instead of hiding the mission if a linked status file is temporarily unreadable. Use the linked run ids with normal `status`, `steer`, `resume`, or `stop` actions. Project mission JSON remains authoritative over chat history.
|
|
284
294
|
|
|
285
295
|
Routing rule:
|
|
@@ -93,7 +93,7 @@ function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId:
|
|
|
93
93
|
const args = [process.execPath, input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop)];
|
|
94
94
|
if (input.index !== undefined) args.push("--index", String(input.index));
|
|
95
95
|
if (input.missionPath) args.push("--mission-path", input.missionPath);
|
|
96
|
-
return args.map(shellQuote).join(" ")
|
|
96
|
+
return `${process.platform === "win32" ? "& " : ""}${args.map(shellQuote).join(" ")}`;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function missionForRun(asyncDir: string, cwd: string, config: MissionStoreConfig | undefined, runId: string): { id: string; path: string } | undefined {
|
|
@@ -94,7 +94,7 @@ async function paneExists(client: HerdrClient, paneId: string, signal?: AbortSig
|
|
|
94
94
|
function projectPaneCommand(message: string | undefined): string {
|
|
95
95
|
const args = message?.trim() ? [message.trim()] : [];
|
|
96
96
|
const command = getPiSpawnCommand(args);
|
|
97
|
-
return [command.command, ...command.args].map(shellQuote).join(" ")
|
|
97
|
+
return `${process.platform === "win32" ? "& " : ""}${[command.command, ...command.args].map(shellQuote).join(" ")}`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export async function handleHerdrProjectPaneAction(action: HerdrProjectPaneAction, params: ProjectPaneParams, deps: ProjectPaneDeps): Promise<AgentToolResult<Details>> {
|
|
@@ -15,7 +15,7 @@ import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts"
|
|
|
15
15
|
import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
16
16
|
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
|
|
17
17
|
import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
18
|
-
import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
18
|
+
import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isParallelStep, resolveChainPath, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
19
19
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
20
20
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
21
21
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
@@ -192,6 +192,7 @@ interface AsyncSingleParams {
|
|
|
192
192
|
context?: ContextMode;
|
|
193
193
|
skills?: string[];
|
|
194
194
|
output?: string | boolean;
|
|
195
|
+
reads?: string[] | false;
|
|
195
196
|
outputMode?: "inline" | "file-only";
|
|
196
197
|
outputBaseDir?: string;
|
|
197
198
|
agentContract?: AgentContract;
|
|
@@ -1288,6 +1289,13 @@ export function executeAsyncSingle(
|
|
|
1288
1289
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
1289
1290
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
1290
1291
|
const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath, agentConfig);
|
|
1292
|
+
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
1293
|
+
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
1294
|
+
const reads = params.reads !== undefined ? params.reads : agentConfig.defaultReads ?? false;
|
|
1295
|
+
const readsInstruction = Array.isArray(reads) && reads.length > 0
|
|
1296
|
+
? `[Read from: ${reads.map((f) => resolveChainPath(f, runnerCwd)).join(", ")}]\n\n`
|
|
1297
|
+
: "";
|
|
1298
|
+
const taskText = readsInstruction + taskWithOutputInstruction;
|
|
1291
1299
|
const primaryModel = externalRunner ? undefined : resolveSubagentModelOverride(
|
|
1292
1300
|
params.modelOverride ?? agentConfig.model,
|
|
1293
1301
|
ctx.currentModel,
|
|
@@ -1415,7 +1423,7 @@ export function executeAsyncSingle(
|
|
|
1415
1423
|
permissionRules,
|
|
1416
1424
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
1417
1425
|
agent,
|
|
1418
|
-
task:
|
|
1426
|
+
task: taskText,
|
|
1419
1427
|
...(agentConfig.runner ? { runner: agentConfig.runner } : {}),
|
|
1420
1428
|
...(params.context ? { context: params.context } : {}),
|
|
1421
1429
|
cwd: runnerCwd,
|
|
@@ -207,7 +207,10 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
|
|
|
207
207
|
const currentStep = typeof status.currentStep === "number" ? status.steps?.[status.currentStep] : undefined;
|
|
208
208
|
return {
|
|
209
209
|
activityState: status.activityState,
|
|
210
|
-
lastActivityAt: status.lastActivityAt
|
|
210
|
+
lastActivityAt: status.lastActivityAt
|
|
211
|
+
?? outputFileMtime(outputPath)
|
|
212
|
+
?? currentStep?.lastActivityAt
|
|
213
|
+
?? (status.mode === "workflow" ? undefined : currentStep?.startedAt ?? status.startedAt),
|
|
211
214
|
};
|
|
212
215
|
}
|
|
213
216
|
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "../../intercom/result-intercom.ts";
|
|
21
21
|
import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-events.ts";
|
|
22
22
|
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
23
|
+
import { recordWaitCompletion } from "./wait-completions.ts";
|
|
23
24
|
import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
|
|
24
25
|
|
|
25
26
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
@@ -155,6 +156,10 @@ export function createResultWatcher(
|
|
|
155
156
|
}
|
|
156
157
|
const epoch = deliveryEpoch;
|
|
157
158
|
if (!ownsSession(data.sessionId, epoch)) return;
|
|
159
|
+
// Recorded before dedupe and before the unlink below: the result file is
|
|
160
|
+
// the only durable carrier of the per-run payload, and subagent_wait
|
|
161
|
+
// surfaces this record in details once the file is gone.
|
|
162
|
+
recordWaitCompletion(state, runId, data, Date.now(), completionTtlMs);
|
|
158
163
|
const hasExplicitNestedChildren = data.nestedChildren !== undefined;
|
|
159
164
|
let nestedChildren = compactNestedResultChildren(sanitizeNestedResultChildren(data.nestedChildren, resultPath, "nestedChildren"));
|
|
160
165
|
if (!nestedChildren?.length && !hasExplicitNestedChildren) {
|
|
@@ -4439,40 +4439,6 @@ async function runSubagent(
|
|
|
4439
4439
|
statusPayload.error = `Step failed: ${failedStep.agent}`;
|
|
4440
4440
|
}
|
|
4441
4441
|
}
|
|
4442
|
-
writeStatusPayload();
|
|
4443
|
-
appendJsonl(
|
|
4444
|
-
eventsPath,
|
|
4445
|
-
JSON.stringify({
|
|
4446
|
-
type: "subagent.run.completed",
|
|
4447
|
-
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
4448
|
-
ts: runEndedAt,
|
|
4449
|
-
runId: id,
|
|
4450
|
-
status: statusPayload.state,
|
|
4451
|
-
durationMs: runEndedAt - overallStartTime,
|
|
4452
|
-
totalTokens: statusPayload.totalTokens,
|
|
4453
|
-
totalCost: finalTotalCost,
|
|
4454
|
-
usageBudget: statusPayload.usageBudget,
|
|
4455
|
-
}),
|
|
4456
|
-
);
|
|
4457
|
-
writeRunLog(logPath, omitUndefinedProperties({
|
|
4458
|
-
id,
|
|
4459
|
-
mode: statusPayload.mode,
|
|
4460
|
-
cwd,
|
|
4461
|
-
startedAt: overallStartTime,
|
|
4462
|
-
endedAt: runEndedAt,
|
|
4463
|
-
steps: statusPayload.steps.map((step) => omitUndefinedProperties({
|
|
4464
|
-
agent: step.agent,
|
|
4465
|
-
status: step.status,
|
|
4466
|
-
durationMs: step.durationMs,
|
|
4467
|
-
})),
|
|
4468
|
-
summary,
|
|
4469
|
-
truncated,
|
|
4470
|
-
artifactsDir,
|
|
4471
|
-
sessionFile: effectiveSessionFile,
|
|
4472
|
-
shareUrl,
|
|
4473
|
-
shareError,
|
|
4474
|
-
}));
|
|
4475
|
-
|
|
4476
4442
|
try {
|
|
4477
4443
|
writeAtomicJson(resultPath, {
|
|
4478
4444
|
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -4572,6 +4538,38 @@ async function runSubagent(
|
|
|
4572
4538
|
} catch (err) {
|
|
4573
4539
|
console.error(`Failed to write result file ${resultPath}:`, err);
|
|
4574
4540
|
}
|
|
4541
|
+
appendJsonl(
|
|
4542
|
+
eventsPath,
|
|
4543
|
+
JSON.stringify({
|
|
4544
|
+
type: "subagent.run.completed",
|
|
4545
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
4546
|
+
ts: runEndedAt,
|
|
4547
|
+
runId: id,
|
|
4548
|
+
status: statusPayload.state,
|
|
4549
|
+
durationMs: runEndedAt - overallStartTime,
|
|
4550
|
+
totalTokens: statusPayload.totalTokens,
|
|
4551
|
+
totalCost: finalTotalCost,
|
|
4552
|
+
usageBudget: statusPayload.usageBudget,
|
|
4553
|
+
}),
|
|
4554
|
+
);
|
|
4555
|
+
writeRunLog(logPath, omitUndefinedProperties({
|
|
4556
|
+
id,
|
|
4557
|
+
mode: statusPayload.mode,
|
|
4558
|
+
cwd,
|
|
4559
|
+
startedAt: overallStartTime,
|
|
4560
|
+
endedAt: runEndedAt,
|
|
4561
|
+
steps: statusPayload.steps.map((step) => omitUndefinedProperties({
|
|
4562
|
+
agent: step.agent,
|
|
4563
|
+
status: step.status,
|
|
4564
|
+
durationMs: step.durationMs,
|
|
4565
|
+
})),
|
|
4566
|
+
summary,
|
|
4567
|
+
truncated,
|
|
4568
|
+
artifactsDir,
|
|
4569
|
+
sessionFile: effectiveSessionFile,
|
|
4570
|
+
shareUrl,
|
|
4571
|
+
shareError,
|
|
4572
|
+
}));
|
|
4575
4573
|
if (config.runnerProcessInstanceId) {
|
|
4576
4574
|
const writers: Record<string, Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }>> = {};
|
|
4577
4575
|
const expectedWriters: Record<string, number> = {};
|
|
@@ -4594,6 +4592,7 @@ async function runSubagent(
|
|
|
4594
4592
|
console.error(`Failed to write process-terminal candidate for '${id}':`, error);
|
|
4595
4593
|
}
|
|
4596
4594
|
}
|
|
4595
|
+
writeStatusPayload();
|
|
4597
4596
|
}
|
|
4598
4597
|
|
|
4599
4598
|
async function waitForStartupControl(
|
|
@@ -58,8 +58,10 @@ import {
|
|
|
58
58
|
type Details,
|
|
59
59
|
type ForegroundResumeRun,
|
|
60
60
|
type SubagentState,
|
|
61
|
+
type WaitCompletion,
|
|
61
62
|
} from "../../shared/types.ts";
|
|
62
63
|
import { formatDuration, shortenPath } from "../../shared/formatters.ts";
|
|
64
|
+
import { collectWaitCompletions } from "./wait-completions.ts";
|
|
63
65
|
export { WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, type ResolvedWaitToolConfig } from "./wait-config.ts";
|
|
64
66
|
|
|
65
67
|
/** States that mean a run is still in flight (not yet resolved). */
|
|
@@ -303,11 +305,15 @@ function summarizeTerminalRuns(runs: AsyncRunSummary[], providerFinishedCount =
|
|
|
303
305
|
return parts.join(", ");
|
|
304
306
|
}
|
|
305
307
|
|
|
306
|
-
function result(text: string, isError = false): AgentToolResult<Details> {
|
|
308
|
+
function result(text: string, isError = false, completions?: WaitCompletion[]): AgentToolResult<Details> {
|
|
307
309
|
return {
|
|
308
310
|
content: [{ type: "text", text }],
|
|
309
311
|
...(isError ? { isError: true } : {}),
|
|
310
|
-
details: {
|
|
312
|
+
details: {
|
|
313
|
+
mode: "management",
|
|
314
|
+
results: [],
|
|
315
|
+
...(completions && completions.length > 0 ? { completions } : {}),
|
|
316
|
+
},
|
|
311
317
|
};
|
|
312
318
|
}
|
|
313
319
|
|
|
@@ -597,6 +603,7 @@ export async function waitForSubagents(
|
|
|
597
603
|
let terminalSummary: string;
|
|
598
604
|
let finishedAsyncCount: number;
|
|
599
605
|
let failedAsyncCount: number;
|
|
606
|
+
let completions: WaitCompletion[] | undefined;
|
|
600
607
|
const activeProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
601
608
|
const providerFinishedCount = [...initialProviderIds].filter((id) => !activeProviderIds.has(id)).length;
|
|
602
609
|
try {
|
|
@@ -605,6 +612,7 @@ export async function waitForSubagents(
|
|
|
605
612
|
finishedAsyncCount = terminal.length;
|
|
606
613
|
failedAsyncCount = terminal.filter((run) => run.state === "failed").length;
|
|
607
614
|
terminalSummary = summarizeTerminalRuns(terminal, providerFinishedCount);
|
|
615
|
+
completions = collectWaitCompletions(terminal, deps.state, deps.resultsDir ?? DIRS.results);
|
|
608
616
|
} catch (error) {
|
|
609
617
|
return result(error instanceof Error ? error.message : String(error), true);
|
|
610
618
|
}
|
|
@@ -631,6 +639,7 @@ export async function waitForSubagents(
|
|
|
631
639
|
return result(
|
|
632
640
|
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
633
641
|
deps.failOnFailedRuns === true && failedAsyncCount > 0,
|
|
642
|
+
completions,
|
|
634
643
|
);
|
|
635
644
|
}
|
|
636
645
|
|
|
@@ -647,5 +656,6 @@ export async function waitForSubagents(
|
|
|
647
656
|
return result(
|
|
648
657
|
`Waited ${elapsed}; ${progress}.${outcome}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
649
658
|
deps.failOnFailedRuns === true && failedAsyncCount > 0,
|
|
659
|
+
completions,
|
|
650
660
|
);
|
|
651
661
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ArtifactPaths, SubagentState, WaitCompletion, WaitCompletionChild } from "../../shared/types.ts";
|
|
4
|
+
import type { AsyncRunSummary } from "./async-status.ts";
|
|
5
|
+
|
|
6
|
+
function asNonEmptyString(value: unknown): string | undefined {
|
|
7
|
+
return typeof value === "string" && value ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function errorCode(error: unknown): string | undefined {
|
|
11
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
12
|
+
? (error as NodeJS.ErrnoException).code
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function errorMessage(error: unknown): string {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Project a terminal result payload into the slim shape that is safe to surface in
|
|
22
|
+
* tool_result details: run identity, per-child outcome, and the artifact trail.
|
|
23
|
+
* Output text is deliberately excluded — it already travels in the tool result
|
|
24
|
+
* content, and duplicating it in details would double the payload for every wait.
|
|
25
|
+
*/
|
|
26
|
+
export function toWaitCompletion(data: Record<string, unknown>, runId: string): WaitCompletion {
|
|
27
|
+
const results = Array.isArray(data.results)
|
|
28
|
+
? data.results.flatMap((entry): WaitCompletionChild[] => {
|
|
29
|
+
if (entry === null || typeof entry !== "object") return [];
|
|
30
|
+
const child = entry as Record<string, unknown>;
|
|
31
|
+
const outputState = child.outputState === "present" || child.outputState === "absent" || child.outputState === "unknown"
|
|
32
|
+
? child.outputState
|
|
33
|
+
: undefined;
|
|
34
|
+
const artifactPaths = child.artifactPaths !== null && typeof child.artifactPaths === "object"
|
|
35
|
+
? (child.artifactPaths as Partial<ArtifactPaths>)
|
|
36
|
+
: undefined;
|
|
37
|
+
const agent = asNonEmptyString(child.agent);
|
|
38
|
+
const childRunId = asNonEmptyString(child.runId);
|
|
39
|
+
const error = asNonEmptyString(child.error);
|
|
40
|
+
const model = asNonEmptyString(child.model);
|
|
41
|
+
return [{
|
|
42
|
+
...(agent ? { agent } : {}),
|
|
43
|
+
...(childRunId ? { runId: childRunId } : {}),
|
|
44
|
+
...(typeof child.success === "boolean" ? { success: child.success } : {}),
|
|
45
|
+
...(outputState ? { outputState } : {}),
|
|
46
|
+
...(error ? { error } : {}),
|
|
47
|
+
...(model ? { model } : {}),
|
|
48
|
+
...(artifactPaths ? { artifactPaths } : {}),
|
|
49
|
+
}];
|
|
50
|
+
})
|
|
51
|
+
: undefined;
|
|
52
|
+
const agent = asNonEmptyString(data.agent);
|
|
53
|
+
const mode = asNonEmptyString(data.mode);
|
|
54
|
+
const state = asNonEmptyString(data.state);
|
|
55
|
+
return {
|
|
56
|
+
runId,
|
|
57
|
+
...(agent ? { agent } : {}),
|
|
58
|
+
...(mode ? { mode } : {}),
|
|
59
|
+
...(state ? { state } : {}),
|
|
60
|
+
...(typeof data.success === "boolean" ? { success: data.success } : {}),
|
|
61
|
+
...(results && results.length > 0 ? { results } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Record a consumed terminal payload for later surfacing by subagent_wait, pruning
|
|
67
|
+
* stale entries with the same TTL that dedupes completion notifications. The result
|
|
68
|
+
* file is deleted after delivery, so this record is the only in-process source once
|
|
69
|
+
* the watcher has consumed it.
|
|
70
|
+
*/
|
|
71
|
+
export function recordWaitCompletion(state: SubagentState, runId: string, data: Record<string, unknown>, now: number, ttlMs: number): void {
|
|
72
|
+
const store = state.completedResults ??= new Map();
|
|
73
|
+
for (const [key, entry] of store) {
|
|
74
|
+
if (now - entry.seenAt > ttlMs) store.delete(key);
|
|
75
|
+
}
|
|
76
|
+
store.set(runId, { seenAt: now, completion: toWaitCompletion(data, runId) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Terminal payloads for the runs a wait covered: the watcher's in-memory record
|
|
81
|
+
* first, then the not-yet-consumed result file. Result files are written atomically,
|
|
82
|
+
* so a direct read never observes a torn write; the read is deliberately read-only —
|
|
83
|
+
* the watcher owns notification and cleanup.
|
|
84
|
+
*/
|
|
85
|
+
export function collectWaitCompletions(terminal: AsyncRunSummary[], state: SubagentState, resultsDir: string): WaitCompletion[] | undefined {
|
|
86
|
+
if (terminal.length === 0) return undefined;
|
|
87
|
+
const completions: WaitCompletion[] = [];
|
|
88
|
+
for (const run of terminal) {
|
|
89
|
+
const recorded = state.completedResults?.get(run.id);
|
|
90
|
+
if (recorded) {
|
|
91
|
+
completions.push(recorded.completion);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const resultPath = path.join(resultsDir, `${run.id}.json`);
|
|
95
|
+
try {
|
|
96
|
+
const raw = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as Record<string, unknown>;
|
|
97
|
+
completions.push(toWaitCompletion(raw, run.id));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (errorCode(error) !== "ENOENT") {
|
|
100
|
+
throw new Error(`Failed to read subagent result '${resultPath}': ${errorMessage(error)}`, {
|
|
101
|
+
cause: error instanceof Error ? error : undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// The watcher may have consumed the file between the store check and the
|
|
105
|
+
// read; its record is authoritative when present, otherwise the payload
|
|
106
|
+
// is gone and the text summary remains the only surface for this run.
|
|
107
|
+
const late = state.completedResults?.get(run.id);
|
|
108
|
+
if (late) completions.push(late.completion);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return completions.length > 0 ? completions : undefined;
|
|
112
|
+
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
getStepAgents,
|
|
37
37
|
isParallelStep,
|
|
38
38
|
isDynamicParallelStep,
|
|
39
|
+
resolveChainPath,
|
|
39
40
|
resolveStepBehavior,
|
|
40
41
|
suppressProgressForReadOnlyTask,
|
|
41
42
|
taskDisallowsFileUpdates,
|
|
@@ -256,6 +257,8 @@ export interface SubagentParamsLike {
|
|
|
256
257
|
focus?: boolean;
|
|
257
258
|
skill?: string | string[] | boolean;
|
|
258
259
|
output?: string | boolean;
|
|
260
|
+
/** Internal-only; not part of the public tool schema. Wired for single-run reads (chain steps use their own field). */
|
|
261
|
+
reads?: string[] | false;
|
|
259
262
|
outputMode?: "inline" | "file-only";
|
|
260
263
|
outputSchema?: JsonSchemaObject;
|
|
261
264
|
agentScope?: unknown;
|
|
@@ -2562,6 +2565,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2562
2565
|
skills,
|
|
2563
2566
|
output: effectiveOutput,
|
|
2564
2567
|
outputMode: effectiveOutputMode,
|
|
2568
|
+
...(params.reads !== undefined ? { reads: params.reads } : {}),
|
|
2565
2569
|
outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
|
|
2566
2570
|
modelOverride,
|
|
2567
2571
|
thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
|
|
@@ -3590,6 +3594,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3590
3594
|
data.modelScope === undefined ? {} : { scope: data.modelScope },
|
|
3591
3595
|
);
|
|
3592
3596
|
let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
|
|
3597
|
+
let readsOverride: string[] | false | undefined = params.reads;
|
|
3593
3598
|
const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
|
|
3594
3599
|
let effectiveOutput = normalizeSingleOutputOverride(rawOutput, agentConfig.output);
|
|
3595
3600
|
const effectiveOutputMode = params.outputMode ?? "inline";
|
|
@@ -3627,6 +3632,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3627
3632
|
if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
|
|
3628
3633
|
if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
|
|
3629
3634
|
if (override?.skills !== undefined) skillOverride = override.skills;
|
|
3635
|
+
if (override?.reads !== undefined) readsOverride = override.reads;
|
|
3630
3636
|
|
|
3631
3637
|
if (result.runInBackground) {
|
|
3632
3638
|
if (!isAsyncAvailable()) {
|
|
@@ -3666,6 +3672,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3666
3672
|
skills: skillOverride === false ? [] : skillOverride,
|
|
3667
3673
|
output: effectiveOutput,
|
|
3668
3674
|
outputMode: effectiveOutputMode,
|
|
3675
|
+
...(readsOverride !== undefined ? { reads: readsOverride } : {}),
|
|
3669
3676
|
outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
|
|
3670
3677
|
modelOverride,
|
|
3671
3678
|
thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
|
|
@@ -3702,6 +3709,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3702
3709
|
const structuredRuntime = params.outputSchema
|
|
3703
3710
|
? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined)
|
|
3704
3711
|
: undefined;
|
|
3712
|
+
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
3713
|
+
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
3714
|
+
const reads = readsOverride !== undefined ? readsOverride : agentConfig.defaultReads ?? false;
|
|
3715
|
+
const readsInstruction = Array.isArray(reads) && reads.length > 0
|
|
3716
|
+
? `[Read from: ${reads.map((f) => resolveChainPath(f, effectiveCwd)).join(", ")}]\n\n`
|
|
3717
|
+
: "";
|
|
3718
|
+
task = readsInstruction + task;
|
|
3705
3719
|
task = injectSingleOutputInstruction(task, outputPath, agentConfig);
|
|
3706
3720
|
|
|
3707
3721
|
let effectiveSkills: string[] | undefined;
|
|
@@ -4016,6 +4030,7 @@ function prepareWorkflowChildParams(params: SubagentParamsLike): SubagentParamsL
|
|
|
4016
4030
|
acceptance,
|
|
4017
4031
|
agentContract,
|
|
4018
4032
|
toolBudget,
|
|
4033
|
+
reads,
|
|
4019
4034
|
...runParams
|
|
4020
4035
|
} = params;
|
|
4021
4036
|
return {
|
|
@@ -4027,6 +4042,7 @@ function prepareWorkflowChildParams(params: SubagentParamsLike): SubagentParamsL
|
|
|
4027
4042
|
...(model !== undefined ? { model } : {}),
|
|
4028
4043
|
...(skill !== undefined ? { skill } : {}),
|
|
4029
4044
|
...(output !== undefined ? { output } : {}),
|
|
4045
|
+
...(reads !== undefined ? { reads } : {}),
|
|
4030
4046
|
...(outputMode !== undefined ? { outputMode } : {}),
|
|
4031
4047
|
...(outputSchema !== undefined ? { outputSchema } : {}),
|
|
4032
4048
|
...(acceptance !== undefined ? { acceptance } : {}),
|
|
@@ -4238,6 +4254,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4238
4254
|
if (job) {
|
|
4239
4255
|
job.status = status.state;
|
|
4240
4256
|
job.updatedAt = status.lastUpdate;
|
|
4257
|
+
job.activityState = status.activityState;
|
|
4258
|
+
job.lastActivityAt = status.lastActivityAt;
|
|
4259
|
+
job.currentTool = status.currentTool;
|
|
4260
|
+
job.currentToolStartedAt = status.currentToolStartedAt;
|
|
4261
|
+
job.currentPath = status.currentPath;
|
|
4262
|
+
job.turnCount = status.turnCount;
|
|
4263
|
+
job.toolCount = status.toolCount;
|
|
4264
|
+
job.currentStep = status.currentStep;
|
|
4241
4265
|
if (status.steps) {
|
|
4242
4266
|
job.steps = status.steps.map((step, index) => ({ ...step, index }));
|
|
4243
4267
|
job.agents = status.steps.map((step) => step.agent);
|
|
@@ -4248,6 +4272,26 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4248
4272
|
job.workflow = status.workflow;
|
|
4249
4273
|
}
|
|
4250
4274
|
};
|
|
4275
|
+
const projectWorkflowActivity = () => {
|
|
4276
|
+
const runningSteps = (status.steps ?? []).filter((step) => step.status === "running");
|
|
4277
|
+
const lastActivityAt = runningSteps.reduce<number | undefined>((latest, step) => step.lastActivityAt === undefined ? latest : Math.max(latest ?? step.lastActivityAt, step.lastActivityAt), undefined);
|
|
4278
|
+
const activeToolStep = runningSteps
|
|
4279
|
+
.filter((step) => step.currentTool)
|
|
4280
|
+
.sort((left, right) => (left.lastActivityAt ?? 0) - (right.lastActivityAt ?? 0))
|
|
4281
|
+
.at(-1);
|
|
4282
|
+
status.activityState = runningSteps.some((step) => step.activityState === "needs_attention")
|
|
4283
|
+
? "needs_attention"
|
|
4284
|
+
: runningSteps.some((step) => step.activityState === "active_long_running") ? "active_long_running" : undefined;
|
|
4285
|
+
status.lastActivityAt = lastActivityAt;
|
|
4286
|
+
status.currentTool = activeToolStep?.currentTool;
|
|
4287
|
+
status.currentToolStartedAt = activeToolStep?.currentToolStartedAt;
|
|
4288
|
+
status.currentPath = activeToolStep?.currentPath;
|
|
4289
|
+
const turnCounts = (status.steps ?? []).flatMap((step) => step.turnCount === undefined ? [] : [step.turnCount]);
|
|
4290
|
+
const toolCounts = (status.steps ?? []).flatMap((step) => step.toolCount === undefined ? [] : [step.toolCount]);
|
|
4291
|
+
status.turnCount = turnCounts.length > 0 ? turnCounts.reduce((total, count) => total + count, 0) : undefined;
|
|
4292
|
+
status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
|
|
4293
|
+
status.currentStep = runningSteps.length === 1 ? status.steps?.indexOf(runningSteps[0]!) : undefined;
|
|
4294
|
+
};
|
|
4251
4295
|
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 };
|
|
4252
4296
|
deps.state.asyncJobs.set(workflowRunId, workflowJob);
|
|
4253
4297
|
deps.state.fleetJobs ??= new Map();
|
|
@@ -4271,9 +4315,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4271
4315
|
if (entry.durationMs === undefined) delete existing.durationMs;
|
|
4272
4316
|
else existing.durationMs = entry.durationMs;
|
|
4273
4317
|
} else {
|
|
4274
|
-
status.steps?.push({ agent: entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped });
|
|
4318
|
+
status.steps?.push({ agent: entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() });
|
|
4275
4319
|
}
|
|
4276
4320
|
}
|
|
4321
|
+
projectWorkflowActivity();
|
|
4277
4322
|
persist();
|
|
4278
4323
|
appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
|
|
4279
4324
|
};
|
|
@@ -4296,7 +4341,27 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4296
4341
|
if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
|
|
4297
4342
|
patchMissionObjective(childParams.task);
|
|
4298
4343
|
const childRequest = prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions });
|
|
4299
|
-
const result = await execute(randomUUID(), childRequest, workflowSignal,
|
|
4344
|
+
const result = await execute(randomUUID(), childRequest, workflowSignal, (update) => {
|
|
4345
|
+
const progress = update.details.progress?.[0];
|
|
4346
|
+
const step = status.steps?.find((candidate) => candidate.workflowKey === key);
|
|
4347
|
+
if (!progress || !step) return;
|
|
4348
|
+
step.status = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
|
|
4349
|
+
step.activityState = progress.activityState;
|
|
4350
|
+
step.lastActivityAt = progress.lastActivityAt;
|
|
4351
|
+
step.currentTool = progress.currentTool;
|
|
4352
|
+
step.currentToolArgs = progress.currentToolArgs;
|
|
4353
|
+
step.currentToolStartedAt = progress.currentToolStartedAt;
|
|
4354
|
+
step.currentPath = progress.currentPath;
|
|
4355
|
+
step.recentTools = progress.recentTools.map((tool) => ({ ...tool }));
|
|
4356
|
+
step.recentOutput = [...progress.recentOutput];
|
|
4357
|
+
step.turnCount = progress.turnCount;
|
|
4358
|
+
step.toolCount = progress.toolCount;
|
|
4359
|
+
step.model = progress.model;
|
|
4360
|
+
step.thinking = progress.thinking;
|
|
4361
|
+
step.error = progress.error;
|
|
4362
|
+
projectWorkflowActivity();
|
|
4363
|
+
persist();
|
|
4364
|
+
}, ctx, preserveActiveSession);
|
|
4300
4365
|
workflowResults.push(...result.details.results);
|
|
4301
4366
|
const child = workflowChildResult(key, result);
|
|
4302
4367
|
if (result.details.asyncId) {
|
|
@@ -4312,16 +4377,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4312
4377
|
const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
|
|
4313
4378
|
const workflowUsage = sumResultsUsage(workflowResults);
|
|
4314
4379
|
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) };
|
|
4380
|
+
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, ...(child.runId ? { runId: child.runId } : {}), 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 });
|
|
4315
4381
|
persist();
|
|
4316
4382
|
appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
|
|
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 });
|
|
4318
4383
|
} catch (error) {
|
|
4319
4384
|
const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
|
|
4320
4385
|
const stopped = controller.signal.aborted;
|
|
4321
4386
|
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 } });
|
|
4387
|
+
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, ...(child.runId ? { runId: child.runId } : {}), 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 });
|
|
4322
4388
|
persist();
|
|
4323
4389
|
appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
|
|
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 });
|
|
4325
4390
|
} finally {
|
|
4326
4391
|
deps.state.workflowControllers?.delete(workflowRunId);
|
|
4327
4392
|
}
|
package/src/shared/settings.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
6
7
|
import * as path from "node:path";
|
|
7
8
|
import type { AgentConfig } from "../agents/agents.ts";
|
|
8
9
|
import { normalizeSkillInput } from "../agents/skills.ts";
|
|
@@ -334,10 +335,22 @@ export function suppressProgressForReadOnlyTask(behavior: ResolvedStepBehavior,
|
|
|
334
335
|
// =============================================================================
|
|
335
336
|
|
|
336
337
|
/**
|
|
337
|
-
*
|
|
338
|
+
* Expand a leading `~`/`~/` to the user's home directory. Other forms (relative,
|
|
339
|
+
* absolute, `~user/`) pass through unchanged.
|
|
338
340
|
*/
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
+
export function expandHomePath(filePath: string): string {
|
|
342
|
+
if (filePath === "~") return os.homedir();
|
|
343
|
+
if (filePath.startsWith("~/")) return path.join(os.homedir(), filePath.slice(2));
|
|
344
|
+
return filePath;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Resolve a file path: `~`/`~/` expand to home first, then absolute paths pass
|
|
349
|
+
* through and relative paths get chainDir prepended.
|
|
350
|
+
*/
|
|
351
|
+
export function resolveChainPath(filePath: string, chainDir: string): string {
|
|
352
|
+
const expanded = expandHomePath(filePath);
|
|
353
|
+
return path.isAbsolute(expanded) ? expanded : path.join(chainDir, expanded);
|
|
341
354
|
}
|
|
342
355
|
|
|
343
356
|
/**
|
package/src/shared/types.ts
CHANGED
|
@@ -947,6 +947,31 @@ export interface SpawnBudgetSnapshot {
|
|
|
947
947
|
grantHistory: SpawnBudgetGrant[];
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
+
/** Slim per-child projection of a terminal result payload, safe to surface in tool_result details. */
|
|
951
|
+
export interface WaitCompletionChild {
|
|
952
|
+
agent?: string;
|
|
953
|
+
/** Child run identity where the producer records one (workflow children); artifact files are keyed by it. */
|
|
954
|
+
runId?: string;
|
|
955
|
+
success?: boolean;
|
|
956
|
+
outputState?: SubagentOutputState;
|
|
957
|
+
error?: string;
|
|
958
|
+
model?: string;
|
|
959
|
+
artifactPaths?: Partial<ArtifactPaths>;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Terminal completion observed for a run a subagent_wait call covered. Carries run
|
|
964
|
+
* identity and the artifact trail; output text stays in the tool result content.
|
|
965
|
+
*/
|
|
966
|
+
export interface WaitCompletion {
|
|
967
|
+
runId: string;
|
|
968
|
+
agent?: string;
|
|
969
|
+
mode?: string;
|
|
970
|
+
state?: string;
|
|
971
|
+
success?: boolean;
|
|
972
|
+
results?: WaitCompletionChild[];
|
|
973
|
+
}
|
|
974
|
+
|
|
950
975
|
export interface Details {
|
|
951
976
|
mode: SubagentResultMode | "management";
|
|
952
977
|
runId?: string;
|
|
@@ -955,6 +980,13 @@ export interface Details {
|
|
|
955
980
|
/** Run-level context summary. "mixed" when children resolved to different modes. */
|
|
956
981
|
context?: "fresh" | "fork" | "mixed";
|
|
957
982
|
results: SingleResult[];
|
|
983
|
+
/**
|
|
984
|
+
* Terminal completion payloads for runs this subagent_wait call observed
|
|
985
|
+
* finishing. Async completions travel as result files that are consumed and
|
|
986
|
+
* deleted after text delivery, so without this field their run and artifact
|
|
987
|
+
* identity never reaches tool_result details.
|
|
988
|
+
*/
|
|
989
|
+
completions?: WaitCompletion[];
|
|
958
990
|
controlEvents?: ControlEvent[];
|
|
959
991
|
steering?: SteerActionResult;
|
|
960
992
|
asyncId?: string;
|
|
@@ -1582,6 +1614,8 @@ export interface SubagentState {
|
|
|
1582
1614
|
lastUiContext: ExtensionContext | null;
|
|
1583
1615
|
poller: NodeJS.Timeout | null;
|
|
1584
1616
|
completionSeen: Map<string, number>;
|
|
1617
|
+
/** Terminal result payloads observed by the result watcher, keyed by run id and pruned by the completion TTL. */
|
|
1618
|
+
completedResults?: Map<string, { seenAt: number; completion: WaitCompletion }>;
|
|
1585
1619
|
watcher: FSWatcher | null;
|
|
1586
1620
|
watcherRestartTimer: ReturnType<typeof setTimeout> | null;
|
|
1587
1621
|
resultFileCoalescer: {
|
|
@@ -385,8 +385,12 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
385
385
|
|
|
386
386
|
const respond = (promise: Promise<unknown>) => {
|
|
387
387
|
void promise.then(
|
|
388
|
-
(value) =>
|
|
389
|
-
|
|
388
|
+
(value) => {
|
|
389
|
+
if (!settled) worker.postMessage({ type: "response", callId: message.callId, ok: true, value: omitUndefinedWorkflowValues(value) });
|
|
390
|
+
},
|
|
391
|
+
(error: unknown) => {
|
|
392
|
+
if (!settled) worker.postMessage({ type: "response", callId: message.callId, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
393
|
+
},
|
|
390
394
|
);
|
|
391
395
|
};
|
|
392
396
|
|