pi-subagents 0.45.2 → 0.47.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 +47 -0
- package/README.md +2 -0
- package/docs/agents.md +342 -0
- package/docs/configuration.md +328 -0
- package/docs/extension-api.md +308 -0
- package/docs/missions.md +119 -0
- package/docs/models.md +192 -0
- package/docs/observability.md +174 -0
- package/docs/tool-reference.md +343 -0
- package/docs/watchdog.md +176 -0
- package/docs/workflows.md +163 -0
- package/package.json +4 -2
- package/skills/pi-subagents/references/execution-controls.md +6 -6
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/src/agents/agents.ts +17 -8
- package/src/agents/frontmatter.ts +7 -3
- package/src/agents/skills.ts +2 -9
- package/src/api/project-panes.ts +30 -0
- package/src/extension/config.ts +18 -1
- package/src/extension/fanout-child.ts +5 -4
- package/src/extension/index.ts +66 -19
- package/src/extension/rpc.ts +3 -6
- package/src/extension/schemas.ts +28 -7
- package/src/extension/subagent-guide.ts +39 -0
- package/src/extension/tool-description.ts +30 -12
- package/src/inspectors/herdr/project-panes.ts +459 -63
- package/src/missions/actions.ts +25 -2
- package/src/missions/lifecycle.ts +21 -2
- package/src/missions/store.ts +79 -2
- package/src/missions/types.ts +33 -0
- package/src/missions/workflow-state.ts +19 -13
- package/src/runs/background/async-execution.ts +17 -6
- package/src/runs/background/async-job-tracker.ts +15 -0
- package/src/runs/background/async-resume.ts +19 -3
- package/src/runs/background/async-status.ts +6 -1
- package/src/runs/background/completion-replay.ts +267 -0
- package/src/runs/background/control-channel.ts +36 -0
- package/src/runs/background/result-watcher.ts +28 -6
- package/src/runs/background/scheduled-runs.ts +2 -1
- package/src/runs/background/stale-run-reconciler.ts +2 -21
- package/src/runs/background/subagent-runner.ts +47 -6
- package/src/runs/background/wait-completions.ts +39 -5
- package/src/runs/background/wait-subscriptions.ts +18 -3
- package/src/runs/foreground/async-steering-action.ts +1 -1
- package/src/runs/foreground/chain-execution.ts +3 -0
- package/src/runs/foreground/execution.ts +7 -0
- package/src/runs/foreground/foreground-history.ts +137 -0
- package/src/runs/foreground/subagent-executor.ts +403 -54
- package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +8 -4
- package/src/runs/shared/model-scope.ts +12 -2
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/worktree.ts +3 -2
- package/src/shared/artifacts.ts +14 -14
- package/src/shared/display-text.ts +100 -0
- package/src/shared/fork-context.ts +13 -0
- package/src/shared/formatters.ts +4 -6
- package/src/shared/prompt-resources.ts +51 -0
- package/src/shared/settings.ts +15 -2
- package/src/shared/types.ts +41 -2
- package/src/shared/utf8.ts +11 -0
- package/src/shared/utils.ts +43 -33
- package/src/slash/prompt-workflows.ts +2 -15
- package/src/slash/slash-commands.ts +22 -2
- package/src/tui/fleet-status.ts +22 -12
- package/src/tui/fleet.ts +135 -25
- package/src/tui/render.ts +150 -33
- package/src/watchdog/change-signature.ts +4 -3
- package/src/workflows/scripted-workflow.ts +167 -10
|
@@ -160,7 +160,7 @@ function persistedBinding(binding: MissionLaunchBinding): PersistedMissionBindin
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function
|
|
163
|
+
export function writeMissionAsyncBinding(asyncDir: string, binding: MissionLaunchBinding): void {
|
|
164
164
|
writePrivateAtomicJson(path.join(asyncDir, MISSION_BINDING_FILE), persistedBinding(binding));
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -210,7 +210,7 @@ export function attachMissionToLaunchResult(input: {
|
|
|
210
210
|
...(input.result.details.results.length === 1 && input.result.details.results[0]?.acceptance ? { acceptance: input.result.details.results[0].acceptance } : {}),
|
|
211
211
|
});
|
|
212
212
|
if (input.result.details.asyncDir) {
|
|
213
|
-
|
|
213
|
+
writeMissionAsyncBinding(input.result.details.asyncDir, input.binding);
|
|
214
214
|
const statusPath = path.join(input.result.details.asyncDir, "status.json");
|
|
215
215
|
if (fs.existsSync(statusPath)) {
|
|
216
216
|
try {
|
|
@@ -337,10 +337,29 @@ export function syncMissionFromAsyncCompletion(value: unknown): MissionRecord |
|
|
|
337
337
|
return total + (usageFromUnknown((result as { tokens?: unknown }).tokens)?.tokens ?? 0);
|
|
338
338
|
}, 0) }
|
|
339
339
|
: undefined);
|
|
340
|
+
const workflowRunId = typeof event.parentWorkflowRunId === "string" && event.parentWorkflowRunId.trim() ? event.parentWorkflowRunId.trim() : undefined;
|
|
341
|
+
const workflowKey = typeof event.workflowKey === "string" && event.workflowKey.trim() ? event.workflowKey.trim() : undefined;
|
|
342
|
+
const workflowChildStatus = runStatus === "complete" || runStatus === "completed" || event.success === true
|
|
343
|
+
? "completed"
|
|
344
|
+
: runStatus === "paused"
|
|
345
|
+
? "paused"
|
|
346
|
+
: runStatus === "stopped"
|
|
347
|
+
? "stopped"
|
|
348
|
+
: "failed";
|
|
349
|
+
const workflowChildTerminal = !["running", "queued", "active", "paused"].includes(workflowChildStatus);
|
|
340
350
|
return updateMission(binding.location, binding.missionId, {
|
|
341
351
|
status: missionStatusForRun(current, runId, runStatus),
|
|
342
352
|
addRuns: [{ runId, mode: typeof event.mode === "string" && ["single", "parallel", "chain", "workflow"].includes(event.mode) ? event.mode as SubagentRunMode : "external", asyncDir: event.asyncDir, status: runStatus, completedAt, ...(usage && usage.tokens > 0 ? { usage } : {}) }],
|
|
343
353
|
addArtifacts: artifacts,
|
|
354
|
+
...(workflowRunId && workflowKey ? { upsertWorkflowChildren: [{
|
|
355
|
+
workflowRunId,
|
|
356
|
+
key: workflowKey,
|
|
357
|
+
runId,
|
|
358
|
+
status: workflowChildStatus,
|
|
359
|
+
artifactPaths: artifacts.map((artifact) => artifact.path),
|
|
360
|
+
...(workflowChildTerminal ? { completedAt } : {}),
|
|
361
|
+
heartbeat: { status: workflowChildStatus, ...(summary ? { message: summary } : {}) },
|
|
362
|
+
}] } : {}),
|
|
344
363
|
...(summary ? { summary } : {}),
|
|
345
364
|
});
|
|
346
365
|
}
|
package/src/missions/store.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { getProjectSubagentsDir } from "../shared/artifacts.ts";
|
|
5
6
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
6
7
|
import { getAgentDir } from "../shared/utils.ts";
|
|
7
8
|
import {
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
type MissionTokenBudget,
|
|
28
29
|
type MissionTokenUsage,
|
|
29
30
|
type MissionUpdateInput,
|
|
31
|
+
type MissionWorkflowChild,
|
|
30
32
|
} from "./types.ts";
|
|
31
33
|
|
|
32
34
|
const MISSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
@@ -149,6 +151,33 @@ function parseDecision(value: unknown, label: string): MissionDecision {
|
|
|
149
151
|
};
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
function parseWorkflowChild(value: unknown, label: string): MissionWorkflowChild {
|
|
155
|
+
const input = asObject(value, label);
|
|
156
|
+
const artifactPaths = input.artifactPaths === undefined ? [] : stringArray(input.artifactPaths, `${label}.artifactPaths`);
|
|
157
|
+
const heartbeat = input.heartbeat === undefined ? undefined : asObject(input.heartbeat, `${label}.heartbeat`);
|
|
158
|
+
return {
|
|
159
|
+
workflowRunId: requiredString(input.workflowRunId, `${label}.workflowRunId`),
|
|
160
|
+
key: validateMissionId(input.key, `${label}.key`),
|
|
161
|
+
status: requiredString(input.status, `${label}.status`),
|
|
162
|
+
startedAt: timestamp(input.startedAt, `${label}.startedAt`),
|
|
163
|
+
updatedAt: timestamp(input.updatedAt, `${label}.updatedAt`),
|
|
164
|
+
artifactPaths,
|
|
165
|
+
...(optionalString(input.runId, `${label}.runId`) ? { runId: input.runId as string } : {}),
|
|
166
|
+
...(optionalString(input.agent, `${label}.agent`) ? { agent: input.agent as string } : {}),
|
|
167
|
+
...(optionalString(input.task, `${label}.task`) ? { task: input.task as string } : {}),
|
|
168
|
+
...(optionalString(input.label, `${label}.label`) ? { label: input.label as string } : {}),
|
|
169
|
+
...(optionalString(input.phase, `${label}.phase`) ? { phase: input.phase as string } : {}),
|
|
170
|
+
...(input.completedAt !== undefined ? { completedAt: timestamp(input.completedAt, `${label}.completedAt`) } : {}),
|
|
171
|
+
...(optionalString(input.sessionPath, `${label}.sessionPath`) ? { sessionPath: input.sessionPath as string } : {}),
|
|
172
|
+
...(heartbeat ? { heartbeat: {
|
|
173
|
+
updatedAt: timestamp(heartbeat.updatedAt, `${label}.heartbeat.updatedAt`),
|
|
174
|
+
...(optionalString(heartbeat.status, `${label}.heartbeat.status`) ? { status: heartbeat.status as string } : {}),
|
|
175
|
+
...(optionalString(heartbeat.phase, `${label}.heartbeat.phase`) ? { phase: heartbeat.phase as string } : {}),
|
|
176
|
+
...(optionalString(heartbeat.message, `${label}.heartbeat.message`) ? { message: heartbeat.message as string } : {}),
|
|
177
|
+
} } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
152
181
|
function parseArtifact(value: unknown, label: string): MissionArtifact {
|
|
153
182
|
const input = asObject(value, label);
|
|
154
183
|
const kind = requiredString(input.kind, `${label}.kind`) as MissionArtifactKind;
|
|
@@ -186,10 +215,12 @@ export function parseMissionRecord(value: unknown, source = "mission record"): M
|
|
|
186
215
|
const input = asObject(value, source);
|
|
187
216
|
if (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);
|
|
188
217
|
if (!Array.isArray(input.runs)) throw new Error(`${source}.runs must be an array`);
|
|
218
|
+
if (input.workflowChildren !== undefined && !Array.isArray(input.workflowChildren)) throw new Error(`${source}.workflowChildren must be an array`);
|
|
189
219
|
if (!Array.isArray(input.decisions)) throw new Error(`${source}.decisions must be an array`);
|
|
190
220
|
if (!Array.isArray(input.artifacts)) throw new Error(`${source}.artifacts must be an array`);
|
|
191
221
|
if (input.receipts !== undefined && !Array.isArray(input.receipts)) throw new Error(`${source}.receipts must be an array`);
|
|
192
222
|
const runs = input.runs as unknown[];
|
|
223
|
+
const workflowChildren = (input.workflowChildren ?? []) as unknown[];
|
|
193
224
|
const decisions = input.decisions as unknown[];
|
|
194
225
|
const artifacts = input.artifacts as unknown[];
|
|
195
226
|
const receipts = (input.receipts ?? []) as unknown[];
|
|
@@ -211,6 +242,7 @@ export function parseMissionRecord(value: unknown, source = "mission record"): M
|
|
|
211
242
|
createdAt: timestamp(input.createdAt, `${source}.createdAt`),
|
|
212
243
|
updatedAt: timestamp(input.updatedAt, `${source}.updatedAt`),
|
|
213
244
|
runs: runs.map((item, index) => parseRunLink(item, `${source}.runs[${index}]`)),
|
|
245
|
+
workflowChildren: workflowChildren.map((item, index) => parseWorkflowChild(item, `${source}.workflowChildren[${index}]`)),
|
|
214
246
|
decisions: decisions.map((item, index) => parseDecision(item, `${source}.decisions[${index}]`)),
|
|
215
247
|
artifacts: artifacts.map((item, index) => parseArtifact(item, `${source}.artifacts[${index}]`)),
|
|
216
248
|
receipts: receipts.map((item, index) => parseReceipt(item, `${source}.receipts[${index}]`)),
|
|
@@ -259,7 +291,7 @@ export function resolveMissionStoreLocation(input: {
|
|
|
259
291
|
const projectRoot = path.resolve(input.projectRoot);
|
|
260
292
|
const missionDir = input.config?.directory
|
|
261
293
|
? expandConfiguredPath(input.config.directory, projectRoot)
|
|
262
|
-
: path.join(projectRoot, "
|
|
294
|
+
: path.join(getProjectSubagentsDir(projectRoot), "missions");
|
|
263
295
|
const globalIndexDir = input.config?.globalIndexDir
|
|
264
296
|
? expandConfiguredPath(input.config.globalIndexDir, projectRoot)
|
|
265
297
|
: path.join(input.agentDir ?? getAgentDir(), "missions", "index");
|
|
@@ -346,6 +378,7 @@ export function createMission(location: MissionStoreLocation, input: MissionCrea
|
|
|
346
378
|
updatedAt: createdAt,
|
|
347
379
|
cwd: location.projectRoot,
|
|
348
380
|
runs: [],
|
|
381
|
+
workflowChildren: [],
|
|
349
382
|
decisions: [],
|
|
350
383
|
artifacts: [],
|
|
351
384
|
receipts: [],
|
|
@@ -412,6 +445,28 @@ export function updateMission(location: MissionStoreLocation, missionId: string,
|
|
|
412
445
|
if (existingIndex === -1) runs.push(run);
|
|
413
446
|
else runs[existingIndex] = { ...runs[existingIndex]!, ...run };
|
|
414
447
|
}
|
|
448
|
+
const workflowChildren = [...current.workflowChildren];
|
|
449
|
+
for (const candidate of update.upsertWorkflowChildren ?? []) {
|
|
450
|
+
const nowIso = now.toISOString();
|
|
451
|
+
const parsed = parseWorkflowChild({
|
|
452
|
+
...candidate,
|
|
453
|
+
startedAt: candidate.startedAt ?? nowIso,
|
|
454
|
+
updatedAt: nowIso,
|
|
455
|
+
artifactPaths: candidate.artifactPaths ?? [],
|
|
456
|
+
...(candidate.heartbeat ? { heartbeat: { ...candidate.heartbeat, updatedAt: nowIso } } : {}),
|
|
457
|
+
}, "mission.update.upsertWorkflowChildren[]");
|
|
458
|
+
const existingIndex = workflowChildren.findIndex((child) => child.workflowRunId === parsed.workflowRunId && child.key === parsed.key);
|
|
459
|
+
if (existingIndex === -1) workflowChildren.push(parsed);
|
|
460
|
+
else {
|
|
461
|
+
const existing = workflowChildren[existingIndex]!;
|
|
462
|
+
workflowChildren[existingIndex] = parseWorkflowChild({
|
|
463
|
+
...existing,
|
|
464
|
+
...parsed,
|
|
465
|
+
startedAt: existing.startedAt,
|
|
466
|
+
artifactPaths: [...new Set([...existing.artifactPaths, ...parsed.artifactPaths])],
|
|
467
|
+
}, "mission.update.upsertWorkflowChildren[]");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
415
470
|
const artifacts = [...current.artifacts];
|
|
416
471
|
for (const candidate of update.addArtifacts ?? []) {
|
|
417
472
|
const artifact = parseArtifact(candidate, "mission.update.addArtifacts[]");
|
|
@@ -439,6 +494,18 @@ export function updateMission(location: MissionStoreLocation, missionId: string,
|
|
|
439
494
|
...(decision.recommendation ? { recommendation: requiredString(decision.recommendation, "mission.update.addDecisions[].recommendation") } : {}),
|
|
440
495
|
})),
|
|
441
496
|
];
|
|
497
|
+
if (update.resolveDecision) {
|
|
498
|
+
const decisionId = validateMissionId(update.resolveDecision.id, "mission.update.resolveDecision.id");
|
|
499
|
+
const decisionIndex = decisions.findIndex((decision) => decision.id === decisionId);
|
|
500
|
+
if (decisionIndex === -1) throw new Error(`Decision '${decisionId}' was not found in mission '${missionId}'`);
|
|
501
|
+
if (decisions[decisionIndex]!.status === "resolved") throw new Error(`Decision '${decisionId}' is already resolved`);
|
|
502
|
+
decisions[decisionIndex] = {
|
|
503
|
+
...decisions[decisionIndex]!,
|
|
504
|
+
status: "resolved",
|
|
505
|
+
resolvedAt: createdAt,
|
|
506
|
+
resolution: requiredString(update.resolveDecision.resolution, "mission.update.resolveDecision.resolution").trim(),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
442
509
|
const budget = update.budget !== undefined ? parseBudget(update.budget, "mission.update.budget") : current.budget;
|
|
443
510
|
const usage = update.usage !== undefined
|
|
444
511
|
? parseUsage(update.usage, "mission.update.usage")
|
|
@@ -452,10 +519,20 @@ export function updateMission(location: MissionStoreLocation, missionId: string,
|
|
|
452
519
|
? { status: "active" }
|
|
453
520
|
: goal;
|
|
454
521
|
}
|
|
522
|
+
const hasOpenDecisions = decisions.some((decision) => decision.status === "open");
|
|
523
|
+
const requestedStatus = update.status !== undefined ? missionStatus(update.status, "mission.update.status") : undefined;
|
|
524
|
+
const candidateStatus = requestedStatus
|
|
525
|
+
?? (update.addDecisions?.length && current.status === "active"
|
|
526
|
+
? "needs_decision"
|
|
527
|
+
: update.resolveDecision && current.status === "needs_decision" && !hasOpenDecisions
|
|
528
|
+
? "active"
|
|
529
|
+
: current.status);
|
|
530
|
+
const decisionStatus = hasOpenDecisions && (candidateStatus === "active" || candidateStatus === "completed") ? "needs_decision" : candidateStatus;
|
|
455
531
|
const next: MissionRecord = {
|
|
456
532
|
...current,
|
|
457
533
|
updatedAt: createdAt,
|
|
458
534
|
runs,
|
|
535
|
+
workflowChildren,
|
|
459
536
|
artifacts,
|
|
460
537
|
receipts,
|
|
461
538
|
decisions,
|
|
@@ -463,7 +540,7 @@ export function updateMission(location: MissionStoreLocation, missionId: string,
|
|
|
463
540
|
...(update.objective !== undefined ? { objective: requiredString(update.objective, "mission.update.objective").trim() } : {}),
|
|
464
541
|
...(budget ? { budget } : {}),
|
|
465
542
|
...(goal ? { goal, usage } : {}),
|
|
466
|
-
|
|
543
|
+
status: decisionStatus,
|
|
467
544
|
...(update.summary !== undefined ? { summary: requiredString(update.summary, "mission.update.summary") } : {}),
|
|
468
545
|
...(update.labels !== undefined ? { labels: stringArray(update.labels, "mission.update.labels") } : {}),
|
|
469
546
|
...(update.acceptance !== undefined ? { acceptance: update.acceptance } : {}),
|
package/src/missions/types.ts
CHANGED
|
@@ -51,6 +51,36 @@ export interface MissionDecision {
|
|
|
51
51
|
resolution?: string;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export interface MissionChildHeartbeat {
|
|
55
|
+
updatedAt: string;
|
|
56
|
+
status?: string;
|
|
57
|
+
phase?: string;
|
|
58
|
+
message?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MissionWorkflowChild {
|
|
62
|
+
workflowRunId: string;
|
|
63
|
+
key: string;
|
|
64
|
+
status: string;
|
|
65
|
+
startedAt: string;
|
|
66
|
+
updatedAt: string;
|
|
67
|
+
runId?: string;
|
|
68
|
+
agent?: string;
|
|
69
|
+
task?: string;
|
|
70
|
+
label?: string;
|
|
71
|
+
phase?: string;
|
|
72
|
+
completedAt?: string;
|
|
73
|
+
sessionPath?: string;
|
|
74
|
+
artifactPaths: string[];
|
|
75
|
+
heartbeat?: MissionChildHeartbeat;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type MissionWorkflowChildUpdate = Pick<MissionWorkflowChild, "workflowRunId" | "key" | "status"> & Partial<Omit<MissionWorkflowChild, "workflowRunId" | "key" | "status" | "startedAt" | "updatedAt" | "artifactPaths" | "heartbeat">> & {
|
|
79
|
+
startedAt?: string;
|
|
80
|
+
artifactPaths?: string[];
|
|
81
|
+
heartbeat?: Omit<MissionChildHeartbeat, "updatedAt"> & { updatedAt?: string };
|
|
82
|
+
};
|
|
83
|
+
|
|
54
84
|
export interface MissionArtifact {
|
|
55
85
|
kind: MissionArtifactKind;
|
|
56
86
|
path: string;
|
|
@@ -80,6 +110,7 @@ export interface MissionRecord {
|
|
|
80
110
|
cwd?: string;
|
|
81
111
|
ownerSessionId?: string;
|
|
82
112
|
runs: MissionRunLink[];
|
|
113
|
+
workflowChildren: MissionWorkflowChild[];
|
|
83
114
|
decisions: MissionDecision[];
|
|
84
115
|
artifacts: MissionArtifact[];
|
|
85
116
|
receipts: MissionReceipt[];
|
|
@@ -151,7 +182,9 @@ export interface MissionUpdateInput {
|
|
|
151
182
|
labels?: string[];
|
|
152
183
|
acceptance?: unknown;
|
|
153
184
|
addRuns?: MissionRunLink[];
|
|
185
|
+
upsertWorkflowChildren?: MissionWorkflowChildUpdate[];
|
|
154
186
|
addArtifacts?: MissionArtifact[];
|
|
155
187
|
addDecisions?: Array<Omit<MissionDecision, "id" | "status" | "createdAt">>;
|
|
188
|
+
resolveDecision?: { id: string; resolution: string };
|
|
156
189
|
addReceipts?: Array<Omit<MissionReceipt, "createdAt">>;
|
|
157
190
|
}
|
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
6
|
-
import { DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS, waitForFileSystemRetry } from "../shared/file-system-retry.ts";
|
|
6
|
+
import { DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS, isRetryableFileSystemError, waitForFileSystemRetry } from "../shared/file-system-retry.ts";
|
|
7
7
|
import { assertWorkflowJsonValue } from "../workflows/scripted-workflow.ts";
|
|
8
8
|
import type { MissionStoreLocation } from "./types.ts";
|
|
9
9
|
import { validateMissionId } from "./store.ts";
|
|
@@ -162,24 +162,30 @@ function withStateFileLock<T>(filePath: string, operation: () => T): T {
|
|
|
162
162
|
waitForStateLock(DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS[attempt], lockPath);
|
|
163
163
|
continue;
|
|
164
164
|
}
|
|
165
|
+
let acquired = false;
|
|
165
166
|
try {
|
|
166
|
-
|
|
167
|
-
owner = { pid: process.pid, token: randomUUID(), createdAt: Date.now(), ...(CURRENT_PROCESS_KEY ? { processKey: CURRENT_PROCESS_KEY } : {}) };
|
|
168
|
-
try {
|
|
169
|
-
fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify(owner), { encoding: "utf-8", mode: 0o600 });
|
|
170
|
-
} catch (error) {
|
|
171
|
-
removeOwnedStateLock(lockPath, owner);
|
|
172
|
-
owner = undefined;
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
break;
|
|
167
|
+
acquired = tryMakeDirectory(lockPath, 0o700);
|
|
176
168
|
} catch (error) {
|
|
177
|
-
if ((error
|
|
178
|
-
|
|
169
|
+
if (isRetryableFileSystemError(error)) {
|
|
170
|
+
waitForStateLock(DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS[attempt], lockPath);
|
|
171
|
+
continue;
|
|
179
172
|
}
|
|
173
|
+
throw new Error(`Failed to acquire mission state lock '${lockPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
+
}
|
|
175
|
+
if (!acquired) {
|
|
180
176
|
if (reclaimStaleStateLock(lockPath, reclaimPath)) continue;
|
|
181
177
|
waitForStateLock(DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS[attempt], lockPath);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
owner = { pid: process.pid, token: randomUUID(), createdAt: Date.now(), ...(CURRENT_PROCESS_KEY ? { processKey: CURRENT_PROCESS_KEY } : {}) };
|
|
181
|
+
try {
|
|
182
|
+
fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify(owner), { encoding: "utf-8", mode: 0o600 });
|
|
183
|
+
} catch (error) {
|
|
184
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
185
|
+
owner = undefined;
|
|
186
|
+
throw error;
|
|
182
187
|
}
|
|
188
|
+
break;
|
|
183
189
|
}
|
|
184
190
|
try {
|
|
185
191
|
return operation();
|
|
@@ -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, resolveChainPath, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
18
|
+
import { buildChainInstructions, isCheckpointStep, isDynamicParallelStep, isParallelStep, resolveChainPath, resolveExistingReadPaths, 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";
|
|
@@ -128,6 +128,8 @@ interface AsyncExecutionContext {
|
|
|
128
128
|
interactive?: boolean;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
export const DEFAULT_ASYNC_TIMEOUT_MS = 30 * 60 * 1000;
|
|
132
|
+
|
|
131
133
|
interface AsyncChainParams {
|
|
132
134
|
chain: ChainStep[];
|
|
133
135
|
task?: string;
|
|
@@ -668,6 +670,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
668
670
|
if (resolvedToolBudget.error) throw new AsyncStartValidationError(resolvedToolBudget.error);
|
|
669
671
|
const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
|
|
670
672
|
const instructionCwd = behaviorCwd ?? stepCwd;
|
|
673
|
+
const readExistenceCwd = behaviorCwd ? stepCwd : instructionCwd;
|
|
671
674
|
let behavior = suppressProgressForReadOnlyTask(resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills), s.task, originalTask);
|
|
672
675
|
const inheritedRelativeParallelOutput = parallelOutputNamespace && s.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output);
|
|
673
676
|
if (inheritedRelativeParallelOutput && parallelOutputNamespace.taskIndex !== undefined) {
|
|
@@ -698,7 +701,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
698
701
|
}
|
|
699
702
|
systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: stepCwd, agentName: a.name });
|
|
700
703
|
|
|
701
|
-
const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false);
|
|
704
|
+
const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false, undefined, readExistenceCwd);
|
|
702
705
|
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
703
706
|
if (behavior.progress) progressInstructionCreated = true;
|
|
704
707
|
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
|
|
@@ -776,6 +779,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
776
779
|
outputMode: behavior.outputMode,
|
|
777
780
|
sessionFile,
|
|
778
781
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
|
|
782
|
+
timeoutMs: a.defaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS,
|
|
779
783
|
waitToolEnabled: params.waitToolEnabled,
|
|
780
784
|
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
781
785
|
explicit: s.acceptance,
|
|
@@ -1197,6 +1201,10 @@ export function executeAsyncChain(
|
|
|
1197
1201
|
/**
|
|
1198
1202
|
* Execute a single agent asynchronously
|
|
1199
1203
|
*/
|
|
1204
|
+
export function workflowAwaitedAsyncResultPath(asyncDir: string): string {
|
|
1205
|
+
return path.join(asyncDir, "workflow-result.json");
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1200
1208
|
export function executeAsyncSingle(
|
|
1201
1209
|
id: string,
|
|
1202
1210
|
params: AsyncSingleParams,
|
|
@@ -1292,8 +1300,9 @@ export function executeAsyncSingle(
|
|
|
1292
1300
|
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
1293
1301
|
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
1294
1302
|
const reads = params.reads !== undefined ? params.reads : agentConfig.defaultReads ?? false;
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1303
|
+
const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, runnerCwd) : [];
|
|
1304
|
+
const readsInstruction = readPaths.length > 0
|
|
1305
|
+
? `[Read from: ${readPaths.join(", ")}]\n\n`
|
|
1297
1306
|
: "";
|
|
1298
1307
|
const taskText = readsInstruction + taskWithOutputInstruction;
|
|
1299
1308
|
const primaryModel = externalRunner ? undefined : resolveSubagentModelOverride(
|
|
@@ -1395,7 +1404,7 @@ export function executeAsyncSingle(
|
|
|
1395
1404
|
...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
|
|
1396
1405
|
...(controlConfig ? { controlConfig } : {}),
|
|
1397
1406
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1398
|
-
...(initialTurnBudget ? { initialTurnBudget } : {}),
|
|
1407
|
+
...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
|
|
1399
1408
|
...(resolvedToolBudget.budget ? { initialToolBudget: resolvedToolBudget.budget } : {}),
|
|
1400
1409
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
|
|
1401
1410
|
...(maxOutput ? { maxOutput } : {}),
|
|
@@ -1456,7 +1465,9 @@ export function executeAsyncSingle(
|
|
|
1456
1465
|
...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
|
|
1457
1466
|
},
|
|
1458
1467
|
],
|
|
1459
|
-
resultPath:
|
|
1468
|
+
resultPath: params.parentWorkflowRunId !== undefined && params.revivalLease !== undefined
|
|
1469
|
+
? workflowAwaitedAsyncResultPath(asyncDir)
|
|
1470
|
+
: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(DIRS.results, `${id}.json`),
|
|
1460
1471
|
cwd: runnerCwd,
|
|
1461
1472
|
placeholder: "{previous}",
|
|
1462
1473
|
maxOutput,
|
|
@@ -57,6 +57,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
57
57
|
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
58
58
|
const steeringNoticeSeen = new Map<string, number>();
|
|
59
59
|
const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
|
|
60
|
+
if (state.widgetsSuspended) return;
|
|
60
61
|
renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
|
|
61
62
|
(ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
62
63
|
};
|
|
@@ -73,6 +74,19 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
73
74
|
throw error;
|
|
74
75
|
}
|
|
75
76
|
};
|
|
77
|
+
const requestLastWidgetRender = () => {
|
|
78
|
+
const ctx = state.lastUiContext;
|
|
79
|
+
if (!ctx || state.widgetsSuspended || options.widgetEnabled === false) return;
|
|
80
|
+
try {
|
|
81
|
+
if (ctx.hasUI) (ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof Error && error.message.includes("extension ctx is stale")) {
|
|
84
|
+
state.lastUiContext = null;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
76
90
|
const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
|
|
77
91
|
const restoredControlEventCursor = (asyncDir: string) => {
|
|
78
92
|
try {
|
|
@@ -400,6 +414,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
400
414
|
}
|
|
401
415
|
|
|
402
416
|
if (widgetChanged) rerenderLastWidget();
|
|
417
|
+
else if (Array.from(state.asyncJobs.values()).some((job) => job.status === "running")) requestLastWidgetRender();
|
|
403
418
|
}, pollIntervalMs);
|
|
404
419
|
state.poller.unref?.();
|
|
405
420
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { DIRS, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
3
|
+
import { DIRS, type AcceptanceInput, type AsyncStatus, type ResolvedTurnBudget, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
4
4
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
6
6
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
@@ -273,6 +273,23 @@ function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): Ac
|
|
|
273
273
|
return value as AcceptanceInput;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function normalizeRecoveryTurnBudget(value: unknown, descriptorPath: string): ResolvedTurnBudget {
|
|
277
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
278
|
+
const {
|
|
279
|
+
outcome: _outcome,
|
|
280
|
+
turnCount: _turnCount,
|
|
281
|
+
wrapUpRequestedAtTurn: _wrapUpRequestedAtTurn,
|
|
282
|
+
terminationDeferredAtTurn: _terminationDeferredAtTurn,
|
|
283
|
+
exceededAtTurn: _exceededAtTurn,
|
|
284
|
+
...publicTurnBudget
|
|
285
|
+
} = value as Record<string, unknown>;
|
|
286
|
+
value = publicTurnBudget;
|
|
287
|
+
}
|
|
288
|
+
const result = resolveTurnBudgetConfig(value, "recoveryDescriptor.initialTurnBudget");
|
|
289
|
+
if (result.error || !result.turnBudget) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error ?? "recoveryDescriptor.initialTurnBudget is invalid."}`);
|
|
290
|
+
return result.turnBudget;
|
|
291
|
+
}
|
|
292
|
+
|
|
276
293
|
export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
|
|
277
294
|
if (!asyncDir) return undefined;
|
|
278
295
|
const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
|
|
@@ -329,8 +346,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
329
346
|
}
|
|
330
347
|
if (parsed.absoluteDeadlineAt !== undefined && (!Number.isFinite(parsed.absoluteDeadlineAt) || (parsed.absoluteDeadlineAt as number) <= 0)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': absoluteDeadlineAt must be a positive timestamp.`);
|
|
331
348
|
if (parsed.initialTurnBudget !== undefined) {
|
|
332
|
-
|
|
333
|
-
if (result.error) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error}`);
|
|
349
|
+
parsed.initialTurnBudget = normalizeRecoveryTurnBudget(parsed.initialTurnBudget, descriptorPath);
|
|
334
350
|
}
|
|
335
351
|
if (parsed.initialToolBudget !== undefined) {
|
|
336
352
|
const result = validateToolBudgetConfig(parsed.initialToolBudget, "recoveryDescriptor.initialToolBudget");
|
|
@@ -4,7 +4,7 @@ import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "
|
|
|
4
4
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
5
|
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type ChainCheckpointState } from "../../shared/types.ts";
|
|
6
6
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
|
|
7
|
-
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
+
import { pruneStatusCacheForAsyncRoot, readStatus } from "../../shared/utils.ts";
|
|
8
8
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
9
9
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
10
10
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
@@ -370,6 +370,7 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
370
370
|
|
|
371
371
|
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
372
372
|
let entries: string[];
|
|
373
|
+
let scannedCompleteRoot = false;
|
|
373
374
|
try {
|
|
374
375
|
if (options.runId !== undefined) {
|
|
375
376
|
const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
|
|
@@ -383,6 +384,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
383
384
|
: [];
|
|
384
385
|
} else {
|
|
385
386
|
entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
|
|
387
|
+
scannedCompleteRoot = true;
|
|
386
388
|
}
|
|
387
389
|
} catch (error) {
|
|
388
390
|
if (isNotFoundError(error)) return [];
|
|
@@ -392,6 +394,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
392
394
|
}
|
|
393
395
|
|
|
394
396
|
if (options.entryLimit !== undefined) {
|
|
397
|
+
scannedCompleteRoot = false;
|
|
395
398
|
const limit = Math.max(0, Math.floor(options.entryLimit));
|
|
396
399
|
entries = entries
|
|
397
400
|
.map((entry) => {
|
|
@@ -410,6 +413,8 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
410
413
|
.map((candidate) => candidate.entry);
|
|
411
414
|
}
|
|
412
415
|
|
|
416
|
+
if (scannedCompleteRoot) pruneStatusCacheForAsyncRoot(asyncDirRoot, entries);
|
|
417
|
+
|
|
413
418
|
const allowedStates = options.states ? new Set(options.states) : undefined;
|
|
414
419
|
const runs: AsyncRunSummary[] = [];
|
|
415
420
|
// Route resolution for every run shares a single index built from the
|