pi-subagents 0.40.0 → 0.41.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 +76 -0
- package/README.md +246 -525
- package/agents/oracle.md +1 -0
- package/package.json +12 -4
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +6 -6
- package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
- package/skills/pi-subagents/references/execution-controls.md +98 -97
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
- package/src/agents/agent-management.ts +155 -65
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +154 -71
- package/src/agents/chain-serializer.ts +10 -7
- package/src/agents/frontmatter.ts +5 -3
- package/src/agents/identity.ts +1 -1
- package/src/agents/proactive-skills.ts +13 -10
- package/src/agents/skills.ts +23 -6
- package/src/api/control-channel.ts +4 -0
- package/src/api/delegation.ts +26 -194
- package/src/api/external-runs.ts +129 -0
- package/src/api/intercom-bridge.ts +3 -0
- package/src/api/pi-args.ts +5 -0
- package/src/api/preflight.ts +3 -3
- package/src/api/shared-types.ts +19 -0
- package/src/extension/config.ts +10 -0
- package/src/extension/control-notices.ts +5 -39
- package/src/extension/doctor.ts +10 -9
- package/src/extension/fanout-child.ts +7 -4
- package/src/extension/index.ts +232 -68
- package/src/extension/rpc.ts +18 -12
- package/src/extension/schemas.ts +48 -37
- package/src/extension/tool-description.ts +36 -86
- package/src/inspectors/herdr/actions.ts +229 -0
- package/src/inspectors/herdr/client.ts +130 -0
- package/src/inspectors/herdr/inspector-runner.ts +141 -0
- package/src/inspectors/herdr/project-panes.ts +154 -0
- package/src/integrations/herdr-status.ts +330 -0
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/result-intercom.ts +5 -1
- package/src/missions/actions.ts +372 -0
- package/src/missions/lifecycle.ts +314 -0
- package/src/missions/store.ts +442 -0
- package/src/missions/types.ts +135 -0
- package/src/policy/authority.ts +46 -0
- package/src/profiles/profiles.ts +29 -3
- package/src/runs/background/async-execution.ts +98 -49
- package/src/runs/background/async-job-tracker.ts +10 -2
- package/src/runs/background/async-resume.ts +6 -6
- package/src/runs/background/async-status.ts +29 -1
- package/src/runs/background/auto-drain.ts +3 -3
- package/src/runs/background/chain-append.ts +3 -2
- package/src/runs/background/control-channel.ts +9 -7
- package/src/runs/background/fleet-view.ts +3 -4
- package/src/runs/background/notify.ts +2 -1
- package/src/runs/background/process-terminal.ts +5 -5
- package/src/runs/background/result-watcher.ts +13 -5
- package/src/runs/background/run-id-resolver.ts +3 -3
- package/src/runs/background/run-status.ts +35 -8
- package/src/runs/background/scheduled-runs.ts +602 -375
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +608 -445
- package/src/runs/background/subagent-wait.ts +50 -9
- package/src/runs/background/wait-subscriptions.ts +253 -0
- package/src/runs/background/wait-tool.ts +12 -4
- package/src/runs/foreground/async-steering-action.ts +3 -3
- package/src/runs/foreground/chain-clarify.ts +8 -4
- package/src/runs/foreground/chain-execution.ts +56 -30
- package/src/runs/foreground/execution.ts +15 -2
- package/src/runs/foreground/subagent-executor.ts +1023 -273
- package/src/runs/shared/acceptance.ts +28 -6
- package/src/runs/shared/child-protocol.ts +302 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/external-cli-runner.ts +130 -0
- package/src/runs/shared/long-running-guard.ts +42 -1
- package/src/runs/shared/nested-events.ts +59 -5
- package/src/runs/shared/nested-render.ts +9 -4
- package/src/runs/shared/parallel-handoff.ts +86 -2
- package/src/runs/shared/parallel-utils.ts +11 -2
- package/src/runs/shared/permissions.ts +95 -0
- package/src/runs/shared/pi-args.ts +11 -1
- package/src/runs/shared/pi-spawn.ts +11 -1
- package/src/runs/shared/run-history.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
- package/src/runs/shared/subagent-startup-retry.ts +5 -2
- package/src/runs/shared/turn-budget.ts +6 -6
- package/src/runs/shared/worktree.ts +122 -12
- package/src/shared/accessible-dir.ts +29 -7
- package/src/shared/artifacts.ts +18 -1
- package/src/shared/fork-context.ts +3 -2
- package/src/shared/launch-contract.ts +1 -0
- package/src/shared/settings.ts +10 -0
- package/src/shared/types.ts +156 -14
- package/src/shared/utils.ts +8 -6
- package/src/slash/delegation-adapters.ts +32 -194
- package/src/slash/delegation-request.ts +43 -126
- package/src/slash/prompt-template-bridge.ts +158 -205
- package/src/slash/prompt-workflows.ts +21 -57
- package/src/slash/slash-bridge.ts +14 -0
- package/src/slash/slash-commands.ts +31 -632
- package/src/slash/subagents-admin.ts +18 -14
- package/src/tui/fleet-status.ts +156 -21
- package/src/tui/fleet-transcript.ts +110 -5
- package/src/tui/fleet.ts +56 -24
- package/src/tui/render.ts +291 -109
- package/src/types/pi-runtime-compat.d.ts +14 -0
- package/src/watchdog/lsp-diagnostics.ts +12 -7
- package/src/watchdog/model-selection.ts +2 -2
- package/src/watchdog/permission-arbiter.ts +145 -0
- package/src/watchdog/register-child.ts +1 -1
- package/src/watchdog/register-main.ts +1 -1
- package/src/watchdog/review.ts +4 -1
- package/src/watchdog/runtime.ts +3 -2
- package/src/workflows/chat-progress.ts +140 -0
- package/src/workflows/scripted-workflow.ts +415 -0
- package/agents/advisor.md +0 -73
- package/src/extension/chain-validation.ts +0 -181
|
@@ -46,11 +46,10 @@ import {
|
|
|
46
46
|
type BackgroundWorkSnapshot,
|
|
47
47
|
type RegisteredBackgroundWorkItem,
|
|
48
48
|
} from "../../api/background-work.ts";
|
|
49
|
-
import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
49
|
+
import { formatAsyncRunList, listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
50
50
|
import {
|
|
51
|
-
|
|
51
|
+
DIRS,
|
|
52
52
|
INTERCOM_DETACH_REQUEST_EVENT,
|
|
53
|
-
RESULTS_DIR,
|
|
54
53
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
55
54
|
SUBAGENT_FOREGROUND_COMPLETE_EVENT,
|
|
56
55
|
SUBAGENT_CONTROL_EVENT,
|
|
@@ -60,7 +59,7 @@ import {
|
|
|
60
59
|
type ForegroundResumeRun,
|
|
61
60
|
type SubagentState,
|
|
62
61
|
} from "../../shared/types.ts";
|
|
63
|
-
import { formatDuration } from "../../shared/formatters.ts";
|
|
62
|
+
import { formatDuration, shortenPath } from "../../shared/formatters.ts";
|
|
64
63
|
export { WAIT_TOOL_ENABLED_ENV, resolveWaitToolConfig, type ResolvedWaitToolConfig } from "./wait-config.ts";
|
|
65
64
|
|
|
66
65
|
/** States that mean a run is still in flight (not yet resolved). */
|
|
@@ -73,6 +72,8 @@ const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
|
73
72
|
export interface SubagentWaitParams {
|
|
74
73
|
/** Optional run id/prefix to wait for. When omitted, waits across every active run in this session. */
|
|
75
74
|
id?: string;
|
|
75
|
+
/** Arm a durable exact-run wake subscription and return immediately. Requires id. */
|
|
76
|
+
nonBlocking?: boolean;
|
|
76
77
|
/**
|
|
77
78
|
* When true, block until EVERY active run in this session (or matching `id`)
|
|
78
79
|
* is terminal. Default false: return as soon as the first run finishes, so a
|
|
@@ -106,6 +107,8 @@ export interface SubagentWaitDeps {
|
|
|
106
107
|
stopOnAttention?: boolean;
|
|
107
108
|
/** Internal auto-drain mode surfaces failed terminal subagent runs as errors. */
|
|
108
109
|
failOnFailedRuns?: boolean;
|
|
110
|
+
/** Arm a durable exact-target wait subscription in a long-lived interactive runtime. */
|
|
111
|
+
subscribe?: (input: { targetKind: "async" | "foreground"; runId: string; requestedId: string; timeoutMs: number }) => { token: string; expiresAt: number };
|
|
109
112
|
/** Injectable provider protocol surfaces for deterministic tests. */
|
|
110
113
|
backgroundWork?: {
|
|
111
114
|
snapshot(sessionId: string, nowMs: number): BackgroundWorkSnapshot;
|
|
@@ -253,8 +256,8 @@ function backgroundWorkForSession(deps: SubagentWaitDeps, nowMs: number): Backgr
|
|
|
253
256
|
|
|
254
257
|
/** Queued/running runs from this session, including runs that need attention. */
|
|
255
258
|
function activeRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps): AsyncRunSummary[] {
|
|
256
|
-
const asyncDirRoot = deps.asyncDirRoot ??
|
|
257
|
-
const resultsDir = deps.resultsDir ??
|
|
259
|
+
const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
|
|
260
|
+
const resultsDir = deps.resultsDir ?? DIRS.results;
|
|
258
261
|
const runs = listAsyncRuns(asyncDirRoot, {
|
|
259
262
|
states: [...ACTIVE_STATES],
|
|
260
263
|
sessionId: deps.state.currentSessionId ?? undefined,
|
|
@@ -273,8 +276,8 @@ function attentionRunsForSession(params: SubagentWaitParams, deps: SubagentWaitD
|
|
|
273
276
|
|
|
274
277
|
/** All runs (any state) for this session, for the final summary. */
|
|
275
278
|
function allRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps): AsyncRunSummary[] {
|
|
276
|
-
const asyncDirRoot = deps.asyncDirRoot ??
|
|
277
|
-
const resultsDir = deps.resultsDir ??
|
|
279
|
+
const asyncDirRoot = deps.asyncDirRoot ?? DIRS.async;
|
|
280
|
+
const resultsDir = deps.resultsDir ?? DIRS.results;
|
|
278
281
|
const runs = listAsyncRuns(asyncDirRoot, {
|
|
279
282
|
sessionId: deps.state.currentSessionId ?? undefined,
|
|
280
283
|
resultsDir,
|
|
@@ -289,7 +292,8 @@ function summarizeTerminalRuns(runs: AsyncRunSummary[], providerFinishedCount =
|
|
|
289
292
|
if (runs.length === 0 && providerFinishedCount === 0) return "";
|
|
290
293
|
const counts = { complete: 0, failed: 0, paused: 0 } as Record<string, number>;
|
|
291
294
|
for (const run of runs) {
|
|
292
|
-
|
|
295
|
+
const count = counts[run.state];
|
|
296
|
+
if (count !== undefined) counts[run.state] = count + 1;
|
|
293
297
|
}
|
|
294
298
|
const parts: string[] = [];
|
|
295
299
|
if (counts.complete) parts.push(`${counts.complete} complete`);
|
|
@@ -307,6 +311,25 @@ function result(text: string, isError = false): AgentToolResult<Details> {
|
|
|
307
311
|
};
|
|
308
312
|
}
|
|
309
313
|
|
|
314
|
+
/** Build the live status shown while async work keeps subagent_wait blocked. */
|
|
315
|
+
function asyncWaitUpdate(runs: AsyncRunSummary[], providerCount: number, elapsedMs: number): AgentToolResult<Details> {
|
|
316
|
+
const activity = runs.flatMap((run) => {
|
|
317
|
+
const activeSteps = run.steps.filter((step) => step.status === "pending" || step.status === "running");
|
|
318
|
+
if (activeSteps.length === 0) {
|
|
319
|
+
return [`${run.id}: ${run.state}`];
|
|
320
|
+
}
|
|
321
|
+
return activeSteps.map((step) => {
|
|
322
|
+
const current = step.currentTool ?? (step.status === "pending" ? "queued" : "thinking…");
|
|
323
|
+
return `${step.agent}: ${current}${step.currentPath ? ` ${shortenPath(step.currentPath)}` : ""}`;
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
const headline = [
|
|
327
|
+
`Waiting ${formatDuration(elapsedMs)} for ${runs.length} async run(s) and ${providerCount} provider item(s).`,
|
|
328
|
+
...activity,
|
|
329
|
+
].join(" · ");
|
|
330
|
+
return result([headline, runs.length > 0 ? formatAsyncRunList(runs) : ""].filter(Boolean).join("\n"));
|
|
331
|
+
}
|
|
332
|
+
|
|
310
333
|
const TRANSCRIPT_TAIL_BYTES = 128 * 1024;
|
|
311
334
|
const TRANSCRIPT_PREVIEW_LINES = 3;
|
|
312
335
|
const TRANSCRIPT_PREVIEW_WIDTH = 220;
|
|
@@ -466,6 +489,12 @@ export async function waitForSubagents(
|
|
|
466
489
|
const timeoutMs = params.timeoutMs !== undefined && params.timeoutMs > 0 ? params.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
467
490
|
const startedAt = now();
|
|
468
491
|
const waitForAll = params.id ? true : params.all === true;
|
|
492
|
+
if (params.nonBlocking && !params.id) {
|
|
493
|
+
return result("Non-blocking wait subscriptions require id so the registration can bind one exact run identity.", true);
|
|
494
|
+
}
|
|
495
|
+
if (params.nonBlocking && params.all) {
|
|
496
|
+
return result("nonBlocking cannot be combined with all; subscribe to one exact run id.", true);
|
|
497
|
+
}
|
|
469
498
|
|
|
470
499
|
let active: AsyncRunSummary[];
|
|
471
500
|
let foreground: ForegroundResumeRun[];
|
|
@@ -489,6 +518,17 @@ export async function waitForSubagents(
|
|
|
489
518
|
return result(`Ambiguous subagent run id prefix "${params.id}" matched ${matches.length} active runs: ${matches.map((candidate) => candidate.id).join(", ")}. Pass a longer id.`, true);
|
|
490
519
|
}
|
|
491
520
|
const selected = matches[0];
|
|
521
|
+
if (selected && params.nonBlocking) {
|
|
522
|
+
if (!deps.subscribe) {
|
|
523
|
+
return result("Non-blocking wait subscriptions require a long-lived interactive subagent runtime; this runtime can only use blocking subagent_wait calls.", true);
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const registration = deps.subscribe({ targetKind: selected.kind, runId: selected.id, requestedId: params.id, timeoutMs });
|
|
527
|
+
return result(`Armed wait subscription ${registration.token} for exact ${selected.kind} run ${selected.id}. Returning immediately; this session will be woken on completion, failure, attention, reconciliation failure, or timeout. Inspect armed subscriptions with subagent({ action: "status" }).`);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
return result(error instanceof Error ? error.message : String(error), true);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
492
532
|
if (selected?.kind === "foreground") {
|
|
493
533
|
return waitForDetachedForegroundRun(selected.run, signal, deps, startedAt, now, pollIntervalMs, timeoutMs);
|
|
494
534
|
}
|
|
@@ -528,6 +568,7 @@ export async function waitForSubagents(
|
|
|
528
568
|
...activeInitialRuns.map((run) => `${run.id} (${run.state})`),
|
|
529
569
|
...activeInitialProviderItems.map((item) => `${item.provider}/${item.id}`),
|
|
530
570
|
].join(", ");
|
|
571
|
+
deps.onUpdate?.(asyncWaitUpdate(activeInitialRuns, activeInitialProviderItems.length, now() - startedAt));
|
|
531
572
|
if (signal?.aborted) {
|
|
532
573
|
return result(`Wait aborted after ${formatDuration(now() - startedAt)}. Still active: ${stillActive}.`, true);
|
|
533
574
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { listAsyncRuns } from "./async-status.ts";
|
|
6
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
|
+
import {
|
|
8
|
+
DIRS,
|
|
9
|
+
INTERCOM_DETACH_REQUEST_EVENT,
|
|
10
|
+
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
11
|
+
SUBAGENT_CONTROL_EVENT,
|
|
12
|
+
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
13
|
+
SUBAGENT_FOREGROUND_COMPLETE_EVENT,
|
|
14
|
+
SUBAGENT_RESULT_INTERCOM_EVENT,
|
|
15
|
+
type SubagentState,
|
|
16
|
+
type WaitSubscriptionRecord,
|
|
17
|
+
} from "../../shared/types.ts";
|
|
18
|
+
|
|
19
|
+
const SUBSCRIPTION_VERSION = 1;
|
|
20
|
+
const RECONCILE_INTERVAL_MS = 1_000;
|
|
21
|
+
|
|
22
|
+
export interface ArmWaitSubscriptionInput {
|
|
23
|
+
targetKind: "async" | "foreground";
|
|
24
|
+
runId: string;
|
|
25
|
+
requestedId: string;
|
|
26
|
+
timeoutMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WaitSubscriptionManager {
|
|
30
|
+
arm(input: ArmWaitSubscriptionInput): WaitSubscriptionRecord;
|
|
31
|
+
restore(): void;
|
|
32
|
+
reconcile(): void;
|
|
33
|
+
dispose(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface WaitSubscriptionManagerOptions {
|
|
37
|
+
asyncDirRoot?: string;
|
|
38
|
+
resultsDir?: string;
|
|
39
|
+
subscriptionsDir?: string;
|
|
40
|
+
now?: () => number;
|
|
41
|
+
pollIntervalMs?: number;
|
|
42
|
+
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isNotFound(error: unknown): boolean {
|
|
46
|
+
return typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseRecord(value: unknown): WaitSubscriptionRecord | undefined {
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
51
|
+
const record = value as Partial<WaitSubscriptionRecord>;
|
|
52
|
+
if (record.version !== SUBSCRIPTION_VERSION
|
|
53
|
+
|| typeof record.token !== "string"
|
|
54
|
+
|| !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(record.token)
|
|
55
|
+
|| typeof record.sessionId !== "string"
|
|
56
|
+
|| (record.targetKind !== "async" && record.targetKind !== "foreground")
|
|
57
|
+
|| typeof record.runId !== "string"
|
|
58
|
+
|| typeof record.requestedId !== "string"
|
|
59
|
+
|| typeof record.createdAt !== "number"
|
|
60
|
+
|| typeof record.expiresAt !== "number") return undefined;
|
|
61
|
+
return record as WaitSubscriptionRecord;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function needsAttention(run: ReturnType<typeof listAsyncRuns>[number]): boolean {
|
|
65
|
+
return run.activityState === "needs_attention" || run.steps.some((step) => step.activityState === "needs_attention");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function subscriptionFile(dir: string, token: string): string {
|
|
69
|
+
return path.join(dir, `${token}.json`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatWaitSubscriptions(state: Pick<SubagentState, "waitSubscriptions">, now = Date.now()): string | undefined {
|
|
73
|
+
const subscriptions = [...(state.waitSubscriptions?.values() ?? [])].sort((left, right) => left.createdAt - right.createdAt);
|
|
74
|
+
if (subscriptions.length === 0) return undefined;
|
|
75
|
+
const lines = [`Armed wait subscriptions (${subscriptions.length}):`];
|
|
76
|
+
for (const record of subscriptions) {
|
|
77
|
+
lines.push(`- ${record.token}: ${record.targetKind} run ${record.runId}, timeout in ${Math.max(0, record.expiresAt - now)}ms`);
|
|
78
|
+
}
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createWaitSubscriptionManager(
|
|
83
|
+
pi: Pick<ExtensionAPI, "events" | "sendMessage">,
|
|
84
|
+
state: SubagentState,
|
|
85
|
+
options: WaitSubscriptionManagerOptions = {},
|
|
86
|
+
): WaitSubscriptionManager {
|
|
87
|
+
const asyncDirRoot = options.asyncDirRoot ?? DIRS.async;
|
|
88
|
+
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
89
|
+
const subscriptionsDir = options.subscriptionsDir ?? path.join(path.dirname(asyncDirRoot), "wait-subscriptions");
|
|
90
|
+
const now = options.now ?? Date.now;
|
|
91
|
+
const subscriptions = state.waitSubscriptions ?? new Map<string, WaitSubscriptionRecord>();
|
|
92
|
+
state.waitSubscriptions = subscriptions;
|
|
93
|
+
const unresolvedRestoredForegroundTokens = new Set<string>();
|
|
94
|
+
let disposed = false;
|
|
95
|
+
|
|
96
|
+
const remove = (record: WaitSubscriptionRecord) => {
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(subscriptionFile(subscriptionsDir, record.token));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!isNotFound(error)) throw error;
|
|
101
|
+
}
|
|
102
|
+
subscriptions.delete(record.token);
|
|
103
|
+
unresolvedRestoredForegroundTokens.delete(record.token);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const settle = (record: WaitSubscriptionRecord, outcome: string, detail: string) => {
|
|
107
|
+
if (disposed || state.currentSessionId !== record.sessionId) return;
|
|
108
|
+
try {
|
|
109
|
+
remove(record);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(`Failed to clear wait subscription '${record.token}'; it remains armed:`, error);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
pi.sendMessage({
|
|
116
|
+
customType: "subagent-wait-subscription",
|
|
117
|
+
content: `Wait subscription ${record.token} fired for run ${record.runId}: ${outcome}. ${detail}`,
|
|
118
|
+
display: true,
|
|
119
|
+
details: { token: record.token, runId: record.runId, outcome },
|
|
120
|
+
}, { triggerTurn: true });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`Failed to deliver wait subscription '${record.token}' after clearing it:`, error);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const reconcileRecord = (record: WaitSubscriptionRecord) => {
|
|
127
|
+
if (record.sessionId !== state.currentSessionId) return;
|
|
128
|
+
if (now() >= record.expiresAt) {
|
|
129
|
+
settle(record, "timed out", "The targeted run may still be active; inspect its status before taking follow-up action.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (record.targetKind === "foreground") {
|
|
133
|
+
const run = state.foregroundRuns?.get(record.runId);
|
|
134
|
+
if (!run && unresolvedRestoredForegroundTokens.has(record.token)) return;
|
|
135
|
+
if (run) unresolvedRestoredForegroundTokens.delete(record.token);
|
|
136
|
+
if (!run || run.sessionId !== record.sessionId) {
|
|
137
|
+
settle(record, "could not be reconciled", "The remembered foreground run disappeared before completion was confirmed.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const detached = run.children.filter((child) => child.status === "detached");
|
|
141
|
+
if (detached.some((child) => child.activityState === "needs_attention")) {
|
|
142
|
+
settle(record, "needs attention", "Reply to the pending supervisor request or inspect the run status.");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (detached.length === 0) {
|
|
146
|
+
const failed = run.children.some((child) => child.status === "failed");
|
|
147
|
+
settle(record, failed ? "failed" : "completed", "Inspect the run status for its final output.");
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const runs = listAsyncRuns(asyncDirRoot, {
|
|
153
|
+
sessionId: record.sessionId,
|
|
154
|
+
runId: record.runId,
|
|
155
|
+
resultsDir,
|
|
156
|
+
kill: options.kill,
|
|
157
|
+
now,
|
|
158
|
+
});
|
|
159
|
+
const run = runs.find((candidate) => candidate.id === record.runId);
|
|
160
|
+
if (!run) {
|
|
161
|
+
settle(record, "could not be reconciled", "The exact async run disappeared before a terminal result was confirmed.");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (needsAttention(run)) {
|
|
165
|
+
settle(record, "needs attention", "Inspect the run status and answer any pending supervisor request.");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (run.state !== "queued" && run.state !== "running") {
|
|
169
|
+
settle(record, run.state === "complete" ? "completed" : run.state, "Inspect the run status for its final output.");
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const reconcile = () => {
|
|
174
|
+
if (disposed || !state.currentSessionId) return;
|
|
175
|
+
for (const record of [...subscriptions.values()]) {
|
|
176
|
+
try {
|
|
177
|
+
reconcileRecord(record);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`Failed to reconcile wait subscription '${record.token}':`, error);
|
|
180
|
+
settle(record, "reconciliation failed", "The targeted run could not be reconciled. Inspect its status before taking follow-up action.");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const wakeChannels = [
|
|
186
|
+
INTERCOM_DETACH_REQUEST_EVENT,
|
|
187
|
+
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
188
|
+
SUBAGENT_FOREGROUND_COMPLETE_EVENT,
|
|
189
|
+
SUBAGENT_CONTROL_EVENT,
|
|
190
|
+
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
191
|
+
SUBAGENT_RESULT_INTERCOM_EVENT,
|
|
192
|
+
];
|
|
193
|
+
const unsubscribes = wakeChannels.map((channel) => pi.events.on(channel, reconcile));
|
|
194
|
+
const interval = setInterval(reconcile, options.pollIntervalMs ?? RECONCILE_INTERVAL_MS);
|
|
195
|
+
interval.unref?.();
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
arm(input) {
|
|
199
|
+
const sessionId = state.currentSessionId;
|
|
200
|
+
if (!sessionId) throw new Error("A wait subscription requires an active session identity.");
|
|
201
|
+
const createdAt = now();
|
|
202
|
+
const record: WaitSubscriptionRecord = {
|
|
203
|
+
version: SUBSCRIPTION_VERSION,
|
|
204
|
+
token: randomUUID(),
|
|
205
|
+
sessionId,
|
|
206
|
+
targetKind: input.targetKind,
|
|
207
|
+
runId: input.runId,
|
|
208
|
+
requestedId: input.requestedId,
|
|
209
|
+
createdAt,
|
|
210
|
+
expiresAt: createdAt + input.timeoutMs,
|
|
211
|
+
};
|
|
212
|
+
fs.mkdirSync(subscriptionsDir, { recursive: true });
|
|
213
|
+
writeAtomicJson(subscriptionFile(subscriptionsDir, record.token), record);
|
|
214
|
+
subscriptions.set(record.token, record);
|
|
215
|
+
return record;
|
|
216
|
+
},
|
|
217
|
+
restore() {
|
|
218
|
+
subscriptions.clear();
|
|
219
|
+
unresolvedRestoredForegroundTokens.clear();
|
|
220
|
+
if (!state.currentSessionId) return;
|
|
221
|
+
let files: string[];
|
|
222
|
+
try {
|
|
223
|
+
files = fs.readdirSync(subscriptionsDir).filter((file) => file.endsWith(".json"));
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (isNotFound(error)) return;
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
try {
|
|
230
|
+
const record = parseRecord(JSON.parse(fs.readFileSync(path.join(subscriptionsDir, file), "utf-8")));
|
|
231
|
+
if (!record || file !== `${record.token}.json`) continue;
|
|
232
|
+
if (record.sessionId === state.currentSessionId) {
|
|
233
|
+
subscriptions.set(record.token, record);
|
|
234
|
+
if (record.targetKind === "foreground" && !state.foregroundRuns?.has(record.runId)) unresolvedRestoredForegroundTokens.add(record.token);
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`Ignoring invalid wait subscription '${path.join(subscriptionsDir, file)}':`, error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
reconcile();
|
|
241
|
+
},
|
|
242
|
+
reconcile,
|
|
243
|
+
dispose() {
|
|
244
|
+
if (disposed) return;
|
|
245
|
+
disposed = true;
|
|
246
|
+
clearInterval(interval);
|
|
247
|
+
for (const unsubscribe of unsubscribes) {
|
|
248
|
+
try { unsubscribe(); } catch { /* best effort */ }
|
|
249
|
+
}
|
|
250
|
+
subscriptions.clear();
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -2,8 +2,9 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
|
|
|
2
2
|
import { SubagentWaitParams } from "../../extension/schemas.ts";
|
|
3
3
|
import type { Details, SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { resolveWaitToolConfig, waitForSubagents } from "./subagent-wait.ts";
|
|
5
|
+
import type { WaitSubscriptionManager } from "./wait-subscriptions.ts";
|
|
5
6
|
|
|
6
|
-
export function registerWaitTool(pi: ExtensionAPI, state: SubagentState, enabled = resolveWaitToolConfig().enabled): void {
|
|
7
|
+
export function registerWaitTool(pi: ExtensionAPI, state: SubagentState, enabled = resolveWaitToolConfig().enabled, subscriptions?: Pick<WaitSubscriptionManager, "arm">): void {
|
|
7
8
|
const tool: ToolDefinition<typeof SubagentWaitParams, Details> = {
|
|
8
9
|
name: "subagent_wait",
|
|
9
10
|
label: "Subagent Wait",
|
|
@@ -14,12 +15,19 @@ In an interactive chat, do not call this merely to wait: return control to the u
|
|
|
14
15
|
• { } — return when the first initially active async run or registered provider item finishes, or when a subagent needs attention.
|
|
15
16
|
• { all: true } — wait for every async run and provider item that was active when the call began.
|
|
16
17
|
• { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
|
|
18
|
+
• { 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.
|
|
17
19
|
• { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running.
|
|
18
20
|
|
|
19
|
-
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."}`,
|
|
21
|
+
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."}`,
|
|
20
22
|
parameters: SubagentWaitParams,
|
|
21
|
-
execute(_id, params, signal, onUpdate) {
|
|
22
|
-
return waitForSubagents(params, signal, {
|
|
23
|
+
execute(_id, params, signal, onUpdate, ctx) {
|
|
24
|
+
return waitForSubagents(params, signal, {
|
|
25
|
+
state,
|
|
26
|
+
events: pi.events,
|
|
27
|
+
enabled,
|
|
28
|
+
onUpdate,
|
|
29
|
+
...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
|
|
30
|
+
});
|
|
23
31
|
},
|
|
24
32
|
};
|
|
25
33
|
pi.registerTool(tool);
|
|
@@ -121,7 +121,7 @@ export async function steerAsyncRun(input: {
|
|
|
121
121
|
if (finalResult?.state === "delivered") {
|
|
122
122
|
return { content: [{ type: "text", text: `Steering delivered for async run ${status.runId} (request ${requestId}).` }], details: { mode: "management", results: [], steering: finalResult } };
|
|
123
123
|
}
|
|
124
|
-
const running = (finalStatus?.steps ?? status.steps).filter((step) => step.status === "running");
|
|
124
|
+
const running = (finalStatus?.steps ?? status.steps ?? []).filter((step) => step.status === "running");
|
|
125
125
|
const recoveryAllowed = status.mode === "single" && status.isNested !== true && running.length === 1 && Boolean(finalStatus?.steering) && (input.index === undefined || input.index === 0);
|
|
126
126
|
if (recoveryAllowed && finalResult?.state !== "scheduled" && input.recover) {
|
|
127
127
|
const appendSteeringNotice = (state: "failed" | "recovered", message: string): void => {
|
|
@@ -194,7 +194,7 @@ export async function steerAsyncRun(input: {
|
|
|
194
194
|
const revived = await input.recover(limits);
|
|
195
195
|
if (revived.isError || !revived.details.asyncId) throw new Error(revived.content[0]?.type === "text" ? revived.content[0].text : "Replacement launch failed; source run remains paused.");
|
|
196
196
|
const sourceStatus = readStatus(asyncDir);
|
|
197
|
-
const targetIndex = input.index ?? status.steps
|
|
197
|
+
const targetIndex = input.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
|
|
198
198
|
if (sourceStatus?.state === "paused" && sourceStatus.steering && targetIndex >= 0) {
|
|
199
199
|
updateSteeringTarget(sourceStatus.steering, requestId, targetIndex, "recovered", Date.now(), { replacementRunId: revived.details.asyncId });
|
|
200
200
|
const stepSteering = sourceStatus.steps?.[targetIndex]?.steering;
|
|
@@ -207,7 +207,7 @@ export async function steerAsyncRun(input: {
|
|
|
207
207
|
} catch (error) {
|
|
208
208
|
const reason = error instanceof Error ? error.message : String(error);
|
|
209
209
|
const failedStatus = readStatus(asyncDir);
|
|
210
|
-
const targetIndex = input.index ?? status.steps
|
|
210
|
+
const targetIndex = input.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
|
|
211
211
|
if (failedStatus && failedStatus.state !== "running" && failedStatus.state !== "queued" && failedStatus.steering && targetIndex >= 0) {
|
|
212
212
|
updateSteeringTarget(failedStatus.steering, requestId, targetIndex, "failed", Date.now(), { reason });
|
|
213
213
|
const stepSteering = failedStatus.steps?.[targetIndex]?.steering;
|
|
@@ -775,22 +775,26 @@ export class ChainClarifyComponent implements Component {
|
|
|
775
775
|
|
|
776
776
|
private handleEditInput(data: string): void {
|
|
777
777
|
const textWidth = this.width - 4; // Must match render: innerW - 2 = (width - 2) - 2
|
|
778
|
-
if (matchesKey(data, "shift+up") || matchesKey(data, "
|
|
778
|
+
if (matchesKey(data, "shift+up") || matchesKey(data, "pageUp")) {
|
|
779
779
|
const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
|
|
780
780
|
const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
|
|
781
781
|
const targetLine = Math.max(0, cursorPos.line - this.EDIT_VIEWPORT_HEIGHT);
|
|
782
|
+
const targetStart = starts[targetLine];
|
|
783
|
+
if (targetStart === undefined) return;
|
|
782
784
|
const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
|
|
783
|
-
this.editState = { ...this.editState, cursor:
|
|
785
|
+
this.editState = { ...this.editState, cursor: targetStart + targetCol };
|
|
784
786
|
this.tui.requestRender();
|
|
785
787
|
return;
|
|
786
788
|
}
|
|
787
789
|
|
|
788
|
-
if (matchesKey(data, "shift+down") || matchesKey(data, "
|
|
790
|
+
if (matchesKey(data, "shift+down") || matchesKey(data, "pageDown")) {
|
|
789
791
|
const { lines: wrapped, starts } = wrapText(this.editState.buffer, textWidth);
|
|
790
792
|
const cursorPos = getCursorDisplayPos(this.editState.cursor, starts);
|
|
791
793
|
const targetLine = Math.min(wrapped.length - 1, cursorPos.line + this.EDIT_VIEWPORT_HEIGHT);
|
|
794
|
+
const targetStart = starts[targetLine];
|
|
795
|
+
if (targetStart === undefined) return;
|
|
792
796
|
const targetCol = Math.min(cursorPos.col, wrapped[targetLine]?.length ?? 0);
|
|
793
|
-
this.editState = { ...this.editState, cursor:
|
|
797
|
+
this.editState = { ...this.editState, cursor: targetStart + targetCol };
|
|
794
798
|
this.tui.requestRender();
|
|
795
799
|
return;
|
|
796
800
|
}
|
|
@@ -44,7 +44,8 @@ import {
|
|
|
44
44
|
import { buildChainSummary } from "../../shared/formatters.ts";
|
|
45
45
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
|
|
46
46
|
import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
|
|
47
|
-
import {
|
|
47
|
+
import type { PermissionConfig } from "../shared/permissions.ts";
|
|
48
|
+
import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
|
|
48
49
|
import { recordRun } from "../shared/run-history.ts";
|
|
49
50
|
import {
|
|
50
51
|
cleanupWorktrees,
|
|
@@ -165,6 +166,7 @@ interface ParallelChainRunInput {
|
|
|
165
166
|
configToolBudget?: ToolBudgetConfig;
|
|
166
167
|
globalSemaphore?: Semaphore;
|
|
167
168
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
169
|
+
permissions?: PermissionConfig;
|
|
168
170
|
dynamic?: boolean;
|
|
169
171
|
}
|
|
170
172
|
|
|
@@ -228,30 +230,32 @@ function finalizeParallelWorktreeHandoff(input: {
|
|
|
228
230
|
results: SingleResult[];
|
|
229
231
|
}): { output: string; reference?: NonNullable<Details["parallelHandoff"]> } {
|
|
230
232
|
const diffs = diffWorktrees(input.worktreeSetup, input.agents, path.join(input.artifactsDir, "worktree-diffs", input.runId, `step-${input.stepIndex}`));
|
|
231
|
-
const cleanup = cleanupWorktrees(input.worktreeSetup);
|
|
232
233
|
const diffSummary = formatWorktreeDiffSummary(diffs);
|
|
234
|
+
const manifestPath = parallelHandoffPath(input.artifactsDir, input.runId);
|
|
235
|
+
const handoff = {
|
|
236
|
+
manifestPath,
|
|
237
|
+
runId: input.runId,
|
|
238
|
+
mode: "chain" as const,
|
|
239
|
+
source: "foreground" as const,
|
|
240
|
+
cwd: input.cwd,
|
|
241
|
+
stepIndex: input.stepIndex,
|
|
242
|
+
flatStartIndex: input.flatStartIndex,
|
|
243
|
+
setup: input.worktreeSetup,
|
|
244
|
+
diffs,
|
|
245
|
+
results: input.results.map((result) => ({
|
|
246
|
+
agent: result.agent,
|
|
247
|
+
status: result.stopped ? "stopped" as const : result.detached ? "detached" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
|
|
248
|
+
summary: getSingleResultOutput(result) || result.error || "(no output)",
|
|
249
|
+
...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
|
|
250
|
+
...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
|
|
251
|
+
...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
|
|
252
|
+
...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
|
|
253
|
+
})),
|
|
254
|
+
};
|
|
233
255
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
mode: "chain",
|
|
238
|
-
source: "foreground",
|
|
239
|
-
cwd: input.cwd,
|
|
240
|
-
stepIndex: input.stepIndex,
|
|
241
|
-
flatStartIndex: input.flatStartIndex,
|
|
242
|
-
setup: input.worktreeSetup,
|
|
243
|
-
diffs,
|
|
244
|
-
cleanup,
|
|
245
|
-
results: input.results.map((result) => ({
|
|
246
|
-
agent: result.agent,
|
|
247
|
-
status: result.stopped ? "stopped" : result.detached ? "detached" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
|
|
248
|
-
summary: getSingleResultOutput(result) || result.error || "(no output)",
|
|
249
|
-
...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
|
|
250
|
-
...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
|
|
251
|
-
...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
|
|
252
|
-
...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
|
|
253
|
-
})),
|
|
254
|
-
});
|
|
256
|
+
writeParallelHandoffGroup(handoff);
|
|
257
|
+
const cleanup = cleanupWorktrees(input.worktreeSetup, { kind: "preserve", capturedDiffs: diffs, handoffManifestPath: manifestPath });
|
|
258
|
+
const reference = writeParallelHandoffGroup({ ...handoff, cleanup });
|
|
255
259
|
const suffix = [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n");
|
|
256
260
|
return { output: suffix ? `${input.output}\n\n${suffix}` : input.output, reference };
|
|
257
261
|
} catch (error) {
|
|
@@ -381,6 +385,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
381
385
|
let result!: SingleResult;
|
|
382
386
|
try {
|
|
383
387
|
result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
|
|
388
|
+
permissions: input.permissions,
|
|
384
389
|
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
385
390
|
capabilityCeiling: input.capabilityCeiling,
|
|
386
391
|
context: input.contextForAgent?.(task.agent),
|
|
@@ -529,6 +534,7 @@ interface ChainExecutionParams {
|
|
|
529
534
|
toolBudget?: ResolvedToolBudget;
|
|
530
535
|
usageBudget?: UsageBudgetConfig;
|
|
531
536
|
configToolBudget?: ToolBudgetConfig;
|
|
537
|
+
permissions?: PermissionConfig;
|
|
532
538
|
/** Global cap on simultaneously-running tasks within this chain. Defaults to DEFAULT_GLOBAL_CONCURRENCY_LIMIT. */
|
|
533
539
|
globalConcurrencyLimit?: number;
|
|
534
540
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
@@ -627,10 +633,11 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
627
633
|
?? (isCheckpointStep(firstStep)
|
|
628
634
|
? undefined
|
|
629
635
|
: isParallelStep(firstStep)
|
|
630
|
-
? firstStep.parallel[0]!.task
|
|
636
|
+
? firstStep.parallel[0]!.task
|
|
631
637
|
: isDynamicParallelStep(firstStep)
|
|
632
|
-
? firstStep.parallel.task
|
|
633
|
-
: (firstStep as SequentialStep).task
|
|
638
|
+
? firstStep.parallel.task
|
|
639
|
+
: (firstStep as SequentialStep).task)
|
|
640
|
+
?? "";
|
|
634
641
|
try {
|
|
635
642
|
validateChainOutputBindings(chainSteps, { maxItems: params.dynamicFanoutMaxItems });
|
|
636
643
|
} catch (error) {
|
|
@@ -788,6 +795,18 @@ ${step.message}` : ""}` }],
|
|
|
788
795
|
}
|
|
789
796
|
|
|
790
797
|
try {
|
|
798
|
+
if (worktreeSetup) {
|
|
799
|
+
writePendingParallelHandoff({
|
|
800
|
+
manifestPath: parallelHandoffPath(artifactsDir, runId),
|
|
801
|
+
runId,
|
|
802
|
+
mode: "chain",
|
|
803
|
+
source: "foreground",
|
|
804
|
+
cwd: parallelCwd,
|
|
805
|
+
stepIndex,
|
|
806
|
+
flatStartIndex: globalTaskIndex,
|
|
807
|
+
setup: worktreeSetup,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
791
810
|
const agentNames = step.parallel.map((task) => task.agent);
|
|
792
811
|
const parallelBehaviors = resolveParallelBehaviors(step.parallel, agents, stepIndex, chainSkills)
|
|
793
812
|
.map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
|
|
@@ -854,6 +873,7 @@ ${step.message}` : ""}` }],
|
|
|
854
873
|
toolBudget: params.toolBudget,
|
|
855
874
|
configToolBudget: params.configToolBudget,
|
|
856
875
|
globalSemaphore,
|
|
876
|
+
permissions: params.permissions,
|
|
857
877
|
});
|
|
858
878
|
globalTaskIndex += step.parallel.length;
|
|
859
879
|
|
|
@@ -928,7 +948,7 @@ ${step.message}` : ""}` }],
|
|
|
928
948
|
.filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract ?? params.agentContract) && (task?.gateOn ?? step.gateOn) === "acceptance" && result.acceptance?.status === "rejected");
|
|
929
949
|
if (acceptanceFailures.length > 0) {
|
|
930
950
|
const acceptanceSummary = acceptanceFailures
|
|
931
|
-
.map(({ result, originalIndex }) => `- Task ${originalIndex + 1} (${result.agent}): ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
|
|
951
|
+
.map(({ result, originalIndex }) => `- Task ${originalIndex + 1} (${result.agent}): ${(result.acceptance ? acceptanceFailureMessage(result.acceptance) : undefined) ?? "acceptance rejected"}`)
|
|
932
952
|
.join("\n");
|
|
933
953
|
const errorMsg = `Parallel step ${stepIndex + 1} acceptance gate failed:\n${acceptanceSummary}`;
|
|
934
954
|
const summary = buildChainSummary(chainSteps, results, chainDir, "failed", { index: stepIndex, error: errorMsg });
|
|
@@ -1024,7 +1044,9 @@ ${step.message}` : ""}` }],
|
|
|
1024
1044
|
cwd: cwd ?? ctx.cwd,
|
|
1025
1045
|
reportOptional: isAgentContractV1(step.agentContract ?? params.agentContract),
|
|
1026
1046
|
});
|
|
1027
|
-
dynamicGroupStatuses[stepIndex]
|
|
1047
|
+
const dynamicGroupStatus = dynamicGroupStatuses[stepIndex];
|
|
1048
|
+
if (!dynamicGroupStatus) throw new Error(`Missing dynamic group status at step ${stepIndex}`);
|
|
1049
|
+
dynamicGroupStatus.acceptance = groupAcceptance;
|
|
1028
1050
|
const groupAcceptanceFailure = !isAgentContractV1(step.agentContract ?? params.agentContract) || step.gateOn === "acceptance" ? acceptanceFailureMessage(groupAcceptance) : undefined;
|
|
1029
1051
|
if (groupAcceptanceFailure) {
|
|
1030
1052
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
|
|
@@ -1112,6 +1134,7 @@ ${step.message}` : ""}` }],
|
|
|
1112
1134
|
toolBudget: params.toolBudget,
|
|
1113
1135
|
configToolBudget: params.configToolBudget,
|
|
1114
1136
|
globalSemaphore,
|
|
1137
|
+
permissions: params.permissions,
|
|
1115
1138
|
dynamic: true,
|
|
1116
1139
|
});
|
|
1117
1140
|
globalTaskIndex = dynamicStartIndex + reservedDynamicItems;
|
|
@@ -1171,7 +1194,7 @@ ${step.message}` : ""}` }],
|
|
|
1171
1194
|
.filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? dynamicParallelStep.agentContract ?? params.agentContract) && (task?.gateOn ?? dynamicParallelStep.gateOn) === "acceptance" && result.acceptance?.status === "rejected");
|
|
1172
1195
|
if (acceptanceFailures.length > 0) {
|
|
1173
1196
|
const acceptanceSummary = acceptanceFailures
|
|
1174
|
-
.map(({ result, originalIndex }) => `- Item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}): ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
|
|
1197
|
+
.map(({ result, originalIndex }) => `- Item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}): ${(result.acceptance ? acceptanceFailureMessage(result.acceptance) : undefined) ?? "acceptance rejected"}`)
|
|
1175
1198
|
.join("\n");
|
|
1176
1199
|
const errorMsg = `Dynamic step ${stepIndex + 1} acceptance gate failed:\n${acceptanceSummary}`;
|
|
1177
1200
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: errorMsg };
|
|
@@ -1217,7 +1240,9 @@ ${step.message}` : ""}` }],
|
|
|
1217
1240
|
cwd: cwd ?? ctx.cwd,
|
|
1218
1241
|
reportOptional: isAgentContractV1(step.agentContract ?? params.agentContract),
|
|
1219
1242
|
});
|
|
1220
|
-
dynamicGroupStatuses[stepIndex]
|
|
1243
|
+
const dynamicGroupStatus = dynamicGroupStatuses[stepIndex];
|
|
1244
|
+
if (!dynamicGroupStatus) throw new Error(`Missing dynamic group status at step ${stepIndex}`);
|
|
1245
|
+
dynamicGroupStatus.acceptance = groupAcceptance;
|
|
1221
1246
|
const groupAcceptanceFailure = effectiveGroupAcceptance.explicit && (!isAgentContractV1(step.agentContract ?? params.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(groupAcceptance) : undefined;
|
|
1222
1247
|
if (groupAcceptanceFailure) {
|
|
1223
1248
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
|
|
@@ -1340,6 +1365,7 @@ ${step.message}` : ""}` }],
|
|
|
1340
1365
|
dynamicGroupStatuses,
|
|
1341
1366
|
});
|
|
1342
1367
|
r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
|
|
1368
|
+
permissions: params.permissions,
|
|
1343
1369
|
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
1344
1370
|
capabilityCeiling: params.capabilityCeiling,
|
|
1345
1371
|
context: params.contextForAgent?.(seqStep.agent),
|