pi-subagents 0.58.0 → 0.60.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 +102 -0
- package/docs/agents.md +5 -3
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +1 -1
- package/docs/models.md +24 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +93 -5
- package/docs/workflows.md +94 -3
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +87 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/skills/pi-subagents/references/execution-controls.md +52 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +159 -42
- package/src/agents/agent-serializer.ts +4 -2
- package/src/agents/agents.ts +67 -26
- package/src/agents/runtime-agent-registry.ts +9 -16
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +6 -9
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +51 -23
- package/src/extension/public-execution.ts +16 -2
- package/src/extension/schemas.ts +36 -10
- package/src/extension/tool-description.ts +24 -5
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +52 -37
- package/src/runs/background/async-job-tracker.ts +17 -12
- package/src/runs/background/async-resume.ts +23 -25
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +110 -8
- package/src/runs/background/chain-append.ts +6 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +158 -6
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +2 -0
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +30 -9
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +351 -358
- package/src/runs/background/subagent-wait.ts +68 -12
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +25 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +125 -122
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +576 -240
- package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +597 -0
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/mcp-config-sources.ts +42 -6
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
- package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
- package/src/runs/shared/model-fallback.ts +9 -2
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +67 -3
- package/src/runs/shared/single-output.ts +72 -22
- package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
- package/src/runs/shared/workflow-graph.ts +15 -0
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +5 -2
- package/src/shared/launch-contract.ts +1 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +232 -30
- package/src/shared/utils.ts +35 -55
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/tui/fleet-status.ts +134 -75
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +895 -121
- package/src/watchdog/change-signature.ts +40 -1
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +235 -0
- package/src/workflows/scripted-workflow.ts +503 -38
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +43 -4
- package/src/workflows/workflow-settlement.ts +246 -0
- package/src/runs/shared/turn-budget.ts +0 -98
|
@@ -58,12 +58,15 @@ import {
|
|
|
58
58
|
type Details,
|
|
59
59
|
type ForegroundResumeRun,
|
|
60
60
|
type SubagentState,
|
|
61
|
+
type Usage,
|
|
61
62
|
type WaitCompletion,
|
|
62
63
|
} from "../../shared/types.ts";
|
|
63
64
|
import { formatDuration, shortenPath } from "../../shared/formatters.ts";
|
|
65
|
+
import { toAgentToolUsage } from "../../shared/utils.ts";
|
|
64
66
|
import { collectWaitCompletions } from "./wait-completions.ts";
|
|
65
67
|
import { formatResumeFirstFailedRunsNote } from "./resume-guidance.ts";
|
|
66
|
-
|
|
68
|
+
import { formatTimeoutRecoveryLines } from "../shared/mutation-evidence.ts";
|
|
69
|
+
export { WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV, WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, type ResolvedWaitToolConfig } from "./wait-config.ts";
|
|
67
70
|
|
|
68
71
|
/** States that mean a run is still in flight (not yet resolved). */
|
|
69
72
|
const ACTIVE_STATES: ReadonlyArray<AsyncRunSummary["state"]> = ["queued", "running"];
|
|
@@ -84,7 +87,7 @@ export interface SubagentWaitParams {
|
|
|
84
87
|
* targets a single run.
|
|
85
88
|
*/
|
|
86
89
|
all?: boolean;
|
|
87
|
-
/** Give up after this many milliseconds. Defaults to 30 minutes. */
|
|
90
|
+
/** Give up after this many milliseconds. Defaults to waitTool.defaultTimeoutMs, then 30 minutes. */
|
|
88
91
|
timeoutMs?: number;
|
|
89
92
|
/** False keeps a blocking wait open through idle attention; supervisor/contact requests still stop the wait. */
|
|
90
93
|
stopOnAttention?: boolean;
|
|
@@ -106,6 +109,8 @@ export interface SubagentWaitDeps {
|
|
|
106
109
|
pollIntervalMs?: number;
|
|
107
110
|
/** False makes the tool return immediately without blocking active async runs. */
|
|
108
111
|
enabled?: boolean;
|
|
112
|
+
/** Configured blocking window used when the call omits timeoutMs. */
|
|
113
|
+
defaultTimeoutMs?: number;
|
|
109
114
|
/** Injectable sleep for tests. */
|
|
110
115
|
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
111
116
|
/** Internal auto-drain mode waits through needs-attention states. */
|
|
@@ -312,10 +317,31 @@ function summarizeTerminalRuns(runs: AsyncRunSummary[], providerFinishedCount =
|
|
|
312
317
|
return parts.join(", ");
|
|
313
318
|
}
|
|
314
319
|
|
|
320
|
+
function completionUsage(completions: WaitCompletion[] | undefined): Usage | undefined {
|
|
321
|
+
if (!completions?.length) return undefined;
|
|
322
|
+
const usage: Usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
323
|
+
let projected = false;
|
|
324
|
+
for (const completion of completions) {
|
|
325
|
+
for (const child of completion.results ?? []) {
|
|
326
|
+
if (!child.usage || (child.usage.input === 0 && child.usage.output === 0 && child.usage.cacheRead === 0 && child.usage.cacheWrite === 0 && child.usage.cost === 0 && child.usage.turns === 0)) continue;
|
|
327
|
+
projected = true;
|
|
328
|
+
usage.input += child.usage.input;
|
|
329
|
+
usage.output += child.usage.output;
|
|
330
|
+
usage.cacheRead += child.usage.cacheRead;
|
|
331
|
+
usage.cacheWrite += child.usage.cacheWrite;
|
|
332
|
+
usage.cost += child.usage.cost;
|
|
333
|
+
usage.turns += child.usage.turns;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return projected ? usage : undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
315
339
|
function result(text: string, isError = false, completions?: WaitCompletion[]): AgentToolResult<Details> {
|
|
340
|
+
const usage = completionUsage(completions);
|
|
316
341
|
return {
|
|
317
342
|
content: [{ type: "text", text }],
|
|
318
343
|
...(isError ? { isError: true } : {}),
|
|
344
|
+
...(usage ? { usage: toAgentToolUsage(usage) } : {}),
|
|
319
345
|
details: {
|
|
320
346
|
mode: "management",
|
|
321
347
|
results: [],
|
|
@@ -324,6 +350,32 @@ function result(text: string, isError = false, completions?: WaitCompletion[]):
|
|
|
324
350
|
};
|
|
325
351
|
}
|
|
326
352
|
|
|
353
|
+
function formatCompletionRecovery(completions: WaitCompletion[] | undefined): string {
|
|
354
|
+
const lines = (completions ?? []).flatMap((completion) =>
|
|
355
|
+
(completion.results ?? []).flatMap((child) => formatTimeoutRecoveryLines(child.timeoutRecovery)));
|
|
356
|
+
return lines.length > 0 ? `\n${lines.join("\n")}` : "";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function windowElapsedResult(
|
|
360
|
+
text: string,
|
|
361
|
+
activeRunIds: string[],
|
|
362
|
+
activeProviderItems: readonly RegisteredBackgroundWorkItem[] = [],
|
|
363
|
+
): AgentToolResult<Details> {
|
|
364
|
+
return {
|
|
365
|
+
content: [{ type: "text", text }],
|
|
366
|
+
details: {
|
|
367
|
+
mode: "management",
|
|
368
|
+
results: [],
|
|
369
|
+
wait: {
|
|
370
|
+
reason: "window_elapsed",
|
|
371
|
+
timedOut: true,
|
|
372
|
+
activeRunIds,
|
|
373
|
+
activeProviderItems: activeProviderItems.map(({ provider, id }) => ({ provider, id })),
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
327
379
|
/** Build the live status shown while async work keeps subagent_wait blocked. */
|
|
328
380
|
function asyncWaitUpdate(runs: AsyncRunSummary[], providerCount: number, elapsedMs: number): AgentToolResult<Details> {
|
|
329
381
|
const activity = runs.flatMap((run) => {
|
|
@@ -471,9 +523,9 @@ async function waitForDetachedForegroundRun(
|
|
|
471
523
|
return result(`Wait aborted after ${formatDuration(now() - startedAt)}. Remembered foreground run "${run.runId}" remains detached. Reply to any pending supervisor request before resuming or launching a replacement.`, true);
|
|
472
524
|
}
|
|
473
525
|
if (now() - startedAt >= timeoutMs) {
|
|
474
|
-
return
|
|
475
|
-
`Wait
|
|
476
|
-
|
|
526
|
+
return windowElapsedResult(
|
|
527
|
+
`Wait window elapsed after ${formatDuration(timeoutMs)} with remembered foreground run "${run.runId}" still detached. Reply to any pending supervisor request, then call subagent_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
|
|
528
|
+
[run.runId],
|
|
477
529
|
);
|
|
478
530
|
}
|
|
479
531
|
await waitForWake(pollIntervalMs, signal, deps);
|
|
@@ -499,7 +551,9 @@ export async function waitForSubagents(
|
|
|
499
551
|
|
|
500
552
|
const now = deps.now ?? Date.now;
|
|
501
553
|
const pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
502
|
-
const timeoutMs = params.timeoutMs !== undefined && params.timeoutMs > 0
|
|
554
|
+
const timeoutMs = params.timeoutMs !== undefined && params.timeoutMs > 0
|
|
555
|
+
? params.timeoutMs
|
|
556
|
+
: deps.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
503
557
|
const startedAt = now();
|
|
504
558
|
const waitForAll = params.id ? true : params.all === true;
|
|
505
559
|
if (params.nonBlocking && !params.id) {
|
|
@@ -586,9 +640,10 @@ export async function waitForSubagents(
|
|
|
586
640
|
return result(`Wait aborted after ${formatDuration(now() - startedAt)}. Still active: ${stillActive}.`, true);
|
|
587
641
|
}
|
|
588
642
|
if (now() - startedAt >= timeoutMs) {
|
|
589
|
-
return
|
|
590
|
-
`Wait
|
|
591
|
-
|
|
643
|
+
return windowElapsedResult(
|
|
644
|
+
`Wait window elapsed after ${formatDuration(timeoutMs)} with ${activeInitialRuns.length} async run(s) and ${activeInitialProviderItems.length} provider item(s) still active: ${stillActive}. The work keeps going; call subagent_wait again or inspect subagent status.`,
|
|
645
|
+
activeInitialRuns.map((run) => run.id),
|
|
646
|
+
activeInitialProviderItems,
|
|
592
647
|
);
|
|
593
648
|
}
|
|
594
649
|
try {
|
|
@@ -618,7 +673,7 @@ export async function waitForSubagents(
|
|
|
618
673
|
const allNow = runsForIds(initialAsyncIds, deps);
|
|
619
674
|
const terminal = allNow.filter((run) => !ACTIVE_STATES.includes(run.state) && initialAsyncIds.has(run.id));
|
|
620
675
|
finishedAsyncCount = terminal.length;
|
|
621
|
-
failedAsyncCount = terminal.filter((run) => run.state === "failed").length;
|
|
676
|
+
failedAsyncCount = terminal.filter((run) => run.state === "failed" || run.state === "partial").length;
|
|
622
677
|
terminalSummary = summarizeTerminalRuns(terminal, providerFinishedCount);
|
|
623
678
|
resumeGuidance = formatResumeFirstFailedRunsNote(terminal);
|
|
624
679
|
completions = collectWaitCompletions(terminal, deps.state, deps.resultsDir ?? DIRS.results);
|
|
@@ -637,6 +692,7 @@ export async function waitForSubagents(
|
|
|
637
692
|
+ providerActive.filter((item) => initialProviderIds.has(backgroundWorkIdentity(item))).length;
|
|
638
693
|
const elapsed = formatDuration(now() - startedAt);
|
|
639
694
|
const outcome = terminalSummary ? ` Outcome: ${terminalSummary}.` : "";
|
|
695
|
+
const recoveryNote = formatCompletionRecovery(completions);
|
|
640
696
|
|
|
641
697
|
if (waitForAll) {
|
|
642
698
|
const scope = params.id
|
|
@@ -646,7 +702,7 @@ export async function waitForSubagents(
|
|
|
646
702
|
: `${initialAsyncIds.size} async run(s) and ${initialProviderIds.size} provider item(s)`;
|
|
647
703
|
const status = relevantAttention.length > 0 ? "attention required" : "done";
|
|
648
704
|
return result(
|
|
649
|
-
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
705
|
+
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
650
706
|
(deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
|
|
651
707
|
completions,
|
|
652
708
|
);
|
|
@@ -663,7 +719,7 @@ export async function waitForSubagents(
|
|
|
663
719
|
? `${relevantAttention.length} of ${initialCount} ${subject} need attention`
|
|
664
720
|
: `${finishedCount} of ${initialCount} ${subject} finished`;
|
|
665
721
|
return result(
|
|
666
|
-
`Waited ${elapsed}; ${progress}.${outcome}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
722
|
+
`Waited ${elapsed}; ${progress}.${outcome}${recoveryNote}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
667
723
|
(deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
|
|
668
724
|
completions,
|
|
669
725
|
);
|
|
@@ -18,7 +18,7 @@ interface TerminalRunIndexEntry {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function isTerminalState(state: AsyncStatus["state"]): boolean {
|
|
21
|
-
return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
|
|
21
|
+
return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped" || state === "rejected";
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function indexRoot(asyncDirRoot: string): string {
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import type { ArtifactPaths, SubagentState, WaitCompletion, WaitCompletionChild } from "../../shared/types.ts";
|
|
2
|
+
import type { ArtifactPaths, SubagentState, Usage, WaitCompletion, WaitCompletionChild } from "../../shared/types.ts";
|
|
3
3
|
import type { AsyncRunSummary } from "./async-status.ts";
|
|
4
4
|
import { readCompletionReplay, writeCompletionReplay } from "./completion-replay.ts";
|
|
5
5
|
import { fallbackResultPayloadPathForSessionRun, resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
|
|
6
6
|
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
7
|
+
import { projectTimeoutRecovery } from "../shared/mutation-evidence.ts";
|
|
7
8
|
|
|
8
9
|
function asNonEmptyString(value: unknown): string | undefined {
|
|
9
10
|
return typeof value === "string" && value ? value : undefined;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
function nonNegativeNumber(value: unknown): number | undefined {
|
|
14
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function projectedUsage(value: unknown): Usage | undefined {
|
|
18
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
19
|
+
const record = value as Record<string, unknown>;
|
|
20
|
+
const input = nonNegativeNumber(record.input);
|
|
21
|
+
const output = nonNegativeNumber(record.output);
|
|
22
|
+
const cacheRead = nonNegativeNumber(record.cacheRead);
|
|
23
|
+
const cacheWrite = nonNegativeNumber(record.cacheWrite);
|
|
24
|
+
const cost = nonNegativeNumber(record.cost);
|
|
25
|
+
const turns = nonNegativeNumber(record.turns);
|
|
26
|
+
if (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined || cost === undefined || turns === undefined || !Number.isSafeInteger(turns)) return undefined;
|
|
27
|
+
return { input, output, cacheRead, cacheWrite, cost, turns };
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
function errorCode(error: unknown): string | undefined {
|
|
13
31
|
return typeof error === "object" && error !== null && "code" in error
|
|
14
32
|
? (error as NodeJS.ErrnoException).code
|
|
@@ -43,18 +61,24 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
43
61
|
: undefined;
|
|
44
62
|
const agent = asNonEmptyString(child.agent);
|
|
45
63
|
const childRunId = asNonEmptyString(child.runId);
|
|
64
|
+
const usage = projectedUsage(child.usage);
|
|
65
|
+
const sessionFile = asNonEmptyString(child.sessionFile);
|
|
46
66
|
const error = asNonEmptyString(child.error);
|
|
47
67
|
const model = asNonEmptyString(child.model);
|
|
48
68
|
const contextOverflow = child.contextOverflow === true;
|
|
69
|
+
const timeoutRecovery = projectTimeoutRecovery(child.timeoutRecovery);
|
|
49
70
|
return [{
|
|
50
71
|
...(agent ? { agent } : {}),
|
|
51
72
|
...(childRunId ? { runId: childRunId } : {}),
|
|
73
|
+
...(usage ? { usage } : {}),
|
|
74
|
+
...(sessionFile ? { sessionFile } : {}),
|
|
52
75
|
...(typeof child.success === "boolean" ? { success: child.success } : {}),
|
|
53
76
|
...(outputState ? { outputState } : {}),
|
|
54
77
|
...(error ? { error } : {}),
|
|
55
78
|
...(model ? { model } : {}),
|
|
56
79
|
...(contextOverflow ? { contextOverflow: true } : {}),
|
|
57
80
|
...(artifactPaths ? { artifactPaths } : {}),
|
|
81
|
+
...(timeoutRecovery ? { timeoutRecovery } : {}),
|
|
58
82
|
}];
|
|
59
83
|
})
|
|
60
84
|
: undefined;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { WaitToolConfig } from "../../shared/types.ts";
|
|
2
2
|
|
|
3
3
|
export const WAIT_TOOL_ENABLED_ENV = "PI_SUBAGENT_WAIT_TOOL_ENABLED";
|
|
4
|
+
export const WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV = "PI_SUBAGENT_WAIT_TOOL_DEFAULT_TIMEOUT_MS";
|
|
4
5
|
|
|
5
6
|
export interface ResolvedWaitToolConfig {
|
|
6
7
|
enabled: boolean;
|
|
8
|
+
defaultTimeoutMs?: number;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
const TRUE_VALUES = new Set(["1", "true", "yes", "on", "enabled"]);
|
|
@@ -17,20 +19,32 @@ function environmentValue(value: string | undefined): boolean | undefined {
|
|
|
17
19
|
throw new Error(`${WAIT_TOOL_ENABLED_ENV} must be one of true/false, 1/0, yes/no, on/off, or enabled/disabled.`);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
function
|
|
21
|
-
if (
|
|
22
|
-
|
|
22
|
+
function environmentDefaultTimeoutMs(value: string | undefined): number | undefined {
|
|
23
|
+
if (value === undefined) return undefined;
|
|
24
|
+
const parsed = Number(value);
|
|
25
|
+
if (!Number.isInteger(parsed) || parsed < 1) throw new Error(`${WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV} must be a positive integer.`);
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function configuredValue(config: unknown): { enabled?: boolean; defaultTimeoutMs?: number } {
|
|
30
|
+
if (config === undefined) return {};
|
|
31
|
+
if (typeof config === "boolean") return { enabled: config };
|
|
23
32
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
24
|
-
throw new Error("config.waitTool must be a boolean or an object with optional enabled
|
|
33
|
+
throw new Error("config.waitTool must be a boolean or an object with optional enabled and defaultTimeoutMs values.");
|
|
34
|
+
}
|
|
35
|
+
const { enabled, defaultTimeoutMs } = config as { enabled?: unknown; defaultTimeoutMs?: unknown };
|
|
36
|
+
if (enabled !== undefined && typeof enabled !== "boolean") throw new Error("config.waitTool.enabled must be a boolean.");
|
|
37
|
+
if (defaultTimeoutMs !== undefined && (!Number.isInteger(defaultTimeoutMs) || (defaultTimeoutMs as number) < 1)) {
|
|
38
|
+
throw new Error("config.waitTool.defaultTimeoutMs must be a positive integer.");
|
|
25
39
|
}
|
|
26
|
-
|
|
27
|
-
if (enabled === undefined) return undefined;
|
|
28
|
-
if (typeof enabled !== "boolean") throw new Error("config.waitTool.enabled must be a boolean.");
|
|
29
|
-
return enabled;
|
|
40
|
+
return { enabled, ...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs: defaultTimeoutMs as number } : {}) };
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
export function resolveWaitToolConfig(config?: WaitToolConfig, env: Record<string, string | undefined> = process.env): ResolvedWaitToolConfig {
|
|
44
|
+
const configured = configuredValue(config);
|
|
45
|
+
const envDefaultTimeoutMs = environmentDefaultTimeoutMs(env[WAIT_TOOL_DEFAULT_TIMEOUT_MS_ENV]);
|
|
33
46
|
return {
|
|
34
|
-
enabled: environmentValue(env[WAIT_TOOL_ENABLED_ENV]) ??
|
|
47
|
+
enabled: environmentValue(env[WAIT_TOOL_ENABLED_ENV]) ?? configured.enabled ?? true,
|
|
48
|
+
...(envDefaultTimeoutMs ?? configured.defaultTimeoutMs !== undefined ? { defaultTimeoutMs: envDefaultTimeoutMs ?? configured.defaultTimeoutMs } : {}),
|
|
35
49
|
};
|
|
36
50
|
}
|
|
@@ -5,7 +5,13 @@ import { resolveWaitToolConfig, waitForSubagents } from "./subagent-wait.ts";
|
|
|
5
5
|
import type { WaitSubscriptionManager } from "./wait-subscriptions.ts";
|
|
6
6
|
import { finalizeToolResult } from "../../extension/tool-result.ts";
|
|
7
7
|
|
|
8
|
-
export function registerWaitTool(
|
|
8
|
+
export function registerWaitTool(
|
|
9
|
+
pi: ExtensionAPI,
|
|
10
|
+
state: SubagentState,
|
|
11
|
+
enabled = resolveWaitToolConfig().enabled,
|
|
12
|
+
subscriptions?: Pick<WaitSubscriptionManager, "arm">,
|
|
13
|
+
defaultTimeoutMs?: number,
|
|
14
|
+
): void {
|
|
9
15
|
const tool: ToolDefinition<typeof SubagentWaitParams, Details> = {
|
|
10
16
|
name: "subagent_wait",
|
|
11
17
|
label: "Subagent Wait",
|
|
@@ -18,7 +24,7 @@ In an interactive chat, do not call this merely to wait: return control to the u
|
|
|
18
24
|
• { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
|
|
19
25
|
• { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. The originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
|
|
20
26
|
• { stopOnAttention: false } — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
|
|
21
|
-
• { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running.
|
|
27
|
+
• { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running. Omitted values use waitTool.defaultTimeoutMs, then 30 minutes. Window expiry returns a non-error window_elapsed result with active work identities.
|
|
22
28
|
|
|
23
29
|
Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`subagent_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`,
|
|
24
30
|
parameters: SubagentWaitParams,
|
|
@@ -27,6 +33,7 @@ Non-blocking subscriptions are visible in subagent status and differ from disabl
|
|
|
27
33
|
state,
|
|
28
34
|
events: pi.events,
|
|
29
35
|
enabled,
|
|
36
|
+
...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
|
|
30
37
|
onUpdate,
|
|
31
38
|
...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
|
|
32
39
|
}));
|
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
5
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
6
|
-
import type { AsyncStatus, Details, SubagentState, ToolBudgetConfig
|
|
6
|
+
import type { AsyncStatus, Details, SubagentState, ToolBudgetConfig } from "../../shared/types.ts";
|
|
7
7
|
import { readStatus } from "../../shared/utils.ts";
|
|
8
8
|
import { consumeSteerAcks, deliverInterruptRequest, queueRevivalBrief, requestAsyncSteer, type SteerDeliveryMode, type SteerRequest } from "../background/control-channel.ts";
|
|
9
9
|
import { resolveAsyncResumeTarget } from "../background/async-resume.ts";
|
|
@@ -34,7 +34,7 @@ export async function steerAsyncRun(input: {
|
|
|
34
34
|
onRequestQueued?: (requestPath: string) => void;
|
|
35
35
|
onBeforeRecoveryClaim?: (requestId: string, committedAt: number) => void;
|
|
36
36
|
onRecoveryCommitted?: (requestId: string, committedAt: number) => void;
|
|
37
|
-
recover?: (limits: { timeoutMs?: number; absoluteDeadlineAt?: number;
|
|
37
|
+
recover?: (limits: { timeoutMs?: number; absoluteDeadlineAt?: number; toolBudget?: ToolBudgetConfig }) => Promise<AgentToolResult<Details>>;
|
|
38
38
|
}): Promise<AgentToolResult<Details>> {
|
|
39
39
|
if (!input.location.asyncDir) {
|
|
40
40
|
return {
|