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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
5
|
+
import type { Details, SubagentRunMode } from "../shared/types.ts";
|
|
6
|
+
import { validateMissionLaunch } from "./actions.ts";
|
|
7
|
+
import type { MissionArtifact, MissionRecord, MissionRunLink, MissionRunMode, MissionStatus, MissionStoreConfig, MissionStoreLocation } from "./types.ts";
|
|
8
|
+
import { createMission, MissionNotFoundError, missionRecordPath, readMission, resolveMissionStoreLocation, updateMission, validateMissionId } from "./store.ts";
|
|
9
|
+
|
|
10
|
+
export const MISSION_BINDING_FILE = "mission.json";
|
|
11
|
+
|
|
12
|
+
export interface MissionLaunchParams {
|
|
13
|
+
missionId?: string;
|
|
14
|
+
mission?: unknown;
|
|
15
|
+
task?: string;
|
|
16
|
+
tasks?: Array<{ task?: string }>;
|
|
17
|
+
chain?: Array<{ task?: string; parallel?: Array<{ task?: string }> | { task?: string } }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MissionLaunchBinding {
|
|
21
|
+
missionId: string;
|
|
22
|
+
location: MissionStoreLocation;
|
|
23
|
+
autoCreated: boolean;
|
|
24
|
+
announceInContent?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface PersistedMissionBinding {
|
|
28
|
+
schemaVersion: 1;
|
|
29
|
+
missionId: string;
|
|
30
|
+
projectRoot: string;
|
|
31
|
+
missionDir: string;
|
|
32
|
+
globalIndexDir: string;
|
|
33
|
+
writeGlobalIndex: boolean;
|
|
34
|
+
retainTerminal?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function workflowGoal(params: MissionLaunchParams): string | undefined {
|
|
38
|
+
return params.task?.trim()
|
|
39
|
+
|| params.tasks?.find((task) => task.task?.trim())?.task?.trim()
|
|
40
|
+
|| params.chain?.find((step) => step.task?.trim())?.task?.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function conciseTitle(goal: string): string {
|
|
44
|
+
const firstLine = goal.split(/\r?\n/, 1)[0]?.trim() || goal.trim();
|
|
45
|
+
return firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function prepareMissionLaunch(input: {
|
|
49
|
+
params: MissionLaunchParams;
|
|
50
|
+
projectRoot: string;
|
|
51
|
+
config?: MissionStoreConfig;
|
|
52
|
+
ownerSessionId?: string;
|
|
53
|
+
}): MissionLaunchBinding | undefined {
|
|
54
|
+
if (input.params.missionId && input.params.mission !== undefined) throw new Error("Use missionId or mission, not both");
|
|
55
|
+
if (input.params.mission === false) return undefined;
|
|
56
|
+
const goal = workflowGoal(input.params);
|
|
57
|
+
const missionsEnabled = input.config?.enabled !== false;
|
|
58
|
+
const shouldCreate = input.params.mission !== undefined || (missionsEnabled && goal !== undefined);
|
|
59
|
+
if (!input.params.missionId && !shouldCreate) return undefined;
|
|
60
|
+
const location = resolveMissionStoreLocation({ projectRoot: input.projectRoot, ...(input.config ? { config: input.config } : {}) });
|
|
61
|
+
if (input.params.missionId) {
|
|
62
|
+
const missionId = validateMissionId(input.params.missionId);
|
|
63
|
+
readMission(location, missionId);
|
|
64
|
+
updateMission(location, missionId, { status: "active" });
|
|
65
|
+
return { missionId, location, autoCreated: false, announceInContent: true };
|
|
66
|
+
}
|
|
67
|
+
const mission = input.params.mission !== undefined ? validateMissionLaunch(input.params.mission) : undefined;
|
|
68
|
+
const title = mission?.title || conciseTitle(goal!);
|
|
69
|
+
const record = createMission(location, {
|
|
70
|
+
title,
|
|
71
|
+
goal: mission?.goal || goal || title,
|
|
72
|
+
status: "active",
|
|
73
|
+
...(mission?.labels ? { labels: mission.labels } : {}),
|
|
74
|
+
...(input.ownerSessionId ? { ownerSessionId: input.ownerSessionId } : {}),
|
|
75
|
+
}, new Date(), input.config?.retainTerminal);
|
|
76
|
+
return { missionId: record.id, location, autoCreated: true, announceInContent: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toolResultIsError(result: AgentToolResult<Details>): boolean {
|
|
80
|
+
return "isError" in result && result.isError === true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function missionRunModeForResult(mode: Details["mode"]): MissionRunMode {
|
|
84
|
+
return mode === "management" ? "external" : mode;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function runStatusForResult(result: AgentToolResult<Details>): string {
|
|
88
|
+
if (result.details.asyncDir) return "active";
|
|
89
|
+
if (result.details.results.some((child) => child.interrupted || child.detached)) return "paused";
|
|
90
|
+
if (toolResultIsError(result) || result.details.results.some((child) => child.exitCode !== 0)) return "failed";
|
|
91
|
+
return "completed";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function missionStatusForRun(record: MissionRecord, runId: string, runStatus: string): MissionStatus {
|
|
95
|
+
if (runStatus === "active" || runStatus === "queued" || runStatus === "running") return "active";
|
|
96
|
+
if (runStatus === "paused") return "waiting";
|
|
97
|
+
const otherActive = record.runs.some((run) => run.runId !== runId && (run.status === "active" || run.status === "queued" || run.status === "running"));
|
|
98
|
+
if (otherActive) return "active";
|
|
99
|
+
if (runStatus === "completed" || runStatus === "complete") return "completed";
|
|
100
|
+
if (runStatus === "stopped" || runStatus === "rejected" || runStatus === "cancelled") return "cancelled";
|
|
101
|
+
return "failed";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function artifactsForResult(result: AgentToolResult<Details>): MissionArtifact[] {
|
|
105
|
+
const artifacts: MissionArtifact[] = [];
|
|
106
|
+
if (result.details.asyncDir) {
|
|
107
|
+
artifacts.push(
|
|
108
|
+
{ kind: "status", path: path.join(result.details.asyncDir, "status.json") },
|
|
109
|
+
{ kind: "other", path: path.join(result.details.asyncDir, "events.jsonl"), description: "Lifecycle events" },
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
for (const child of result.details.results) {
|
|
113
|
+
if (child.artifactPaths?.outputPath) artifacts.push({ kind: "output", path: child.artifactPaths.outputPath });
|
|
114
|
+
if (child.savedOutputPath) artifacts.push({ kind: "output", path: child.savedOutputPath });
|
|
115
|
+
if (child.transcriptPath) artifacts.push({ kind: "other", path: child.transcriptPath, description: "Child transcript" });
|
|
116
|
+
if (child.structuredOutputPath) artifacts.push({ kind: "output", path: child.structuredOutputPath, description: "Structured output" });
|
|
117
|
+
}
|
|
118
|
+
if (result.details.parallelHandoff?.path) artifacts.push({ kind: "manifest", path: result.details.parallelHandoff.path });
|
|
119
|
+
return artifacts;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function firstText(result: AgentToolResult<Details>): string | undefined {
|
|
123
|
+
const text = result.content.find((item) => item.type === "text")?.text.trim();
|
|
124
|
+
if (!text) return undefined;
|
|
125
|
+
return text.length > 2000 ? `${text.slice(0, 1997)}...` : text;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function persistedBinding(binding: MissionLaunchBinding): PersistedMissionBinding {
|
|
129
|
+
return {
|
|
130
|
+
schemaVersion: 1,
|
|
131
|
+
missionId: binding.missionId,
|
|
132
|
+
projectRoot: binding.location.projectRoot,
|
|
133
|
+
missionDir: binding.location.missionDir,
|
|
134
|
+
globalIndexDir: binding.location.globalIndexDir,
|
|
135
|
+
writeGlobalIndex: binding.location.writeGlobalIndex,
|
|
136
|
+
...(binding.location.retainTerminal !== undefined ? { retainTerminal: binding.location.retainTerminal } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function writeAsyncBinding(asyncDir: string, binding: MissionLaunchBinding): void {
|
|
141
|
+
writePrivateAtomicJson(path.join(asyncDir, MISSION_BINDING_FILE), persistedBinding(binding));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function attachMissionToLaunchResult(input: {
|
|
145
|
+
binding: MissionLaunchBinding;
|
|
146
|
+
result: AgentToolResult<Details>;
|
|
147
|
+
}): AgentToolResult<Details> {
|
|
148
|
+
const runId = input.result.details.runId ?? input.result.details.asyncId;
|
|
149
|
+
if (!runId) {
|
|
150
|
+
const current = readMission(input.binding.location, input.binding.missionId);
|
|
151
|
+
const activeRunExists = current.runs.some((run) => run.status === "active" || run.status === "queued" || run.status === "running");
|
|
152
|
+
const mission = toolResultIsError(input.result)
|
|
153
|
+
? updateMission(input.binding.location, input.binding.missionId, {
|
|
154
|
+
status: activeRunExists ? "active" : "failed",
|
|
155
|
+
...(firstText(input.result) ? { summary: firstText(input.result)! } : {}),
|
|
156
|
+
})
|
|
157
|
+
: current;
|
|
158
|
+
return {
|
|
159
|
+
...input.result,
|
|
160
|
+
details: {
|
|
161
|
+
...input.result.details,
|
|
162
|
+
missionId: input.binding.missionId,
|
|
163
|
+
missionPath: missionRecordPath(input.binding.location, input.binding.missionId),
|
|
164
|
+
mission,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const runStatus = runStatusForResult(input.result);
|
|
169
|
+
const current = readMission(input.binding.location, input.binding.missionId);
|
|
170
|
+
const startedAt = new Date().toISOString();
|
|
171
|
+
const run: MissionRunLink = {
|
|
172
|
+
runId,
|
|
173
|
+
mode: missionRunModeForResult(input.result.details.mode),
|
|
174
|
+
status: runStatus,
|
|
175
|
+
startedAt,
|
|
176
|
+
...(input.result.details.asyncDir ? { asyncDir: input.result.details.asyncDir } : {}),
|
|
177
|
+
...(input.result.details.results.length === 1 && input.result.details.results[0]?.agent ? { agent: input.result.details.results[0].agent } : {}),
|
|
178
|
+
...(runStatus !== "active" ? { completedAt: startedAt } : {}),
|
|
179
|
+
};
|
|
180
|
+
let mission = updateMission(input.binding.location, input.binding.missionId, {
|
|
181
|
+
status: missionStatusForRun(current, runId, runStatus),
|
|
182
|
+
addRuns: [run],
|
|
183
|
+
addArtifacts: artifactsForResult(input.result),
|
|
184
|
+
...(firstText(input.result) && runStatus !== "active" ? { summary: firstText(input.result)! } : {}),
|
|
185
|
+
...(input.result.details.results.length === 1 && input.result.details.results[0]?.acceptance ? { acceptance: input.result.details.results[0].acceptance } : {}),
|
|
186
|
+
});
|
|
187
|
+
if (input.result.details.asyncDir) {
|
|
188
|
+
writeAsyncBinding(input.result.details.asyncDir, input.binding);
|
|
189
|
+
const statusPath = path.join(input.result.details.asyncDir, "status.json");
|
|
190
|
+
if (fs.existsSync(statusPath)) {
|
|
191
|
+
try {
|
|
192
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as { state?: unknown };
|
|
193
|
+
if (typeof status.state === "string" && !["queued", "running"].includes(status.state)) {
|
|
194
|
+
mission = syncMissionFromAsyncCompletion({
|
|
195
|
+
runId,
|
|
196
|
+
asyncDir: input.result.details.asyncDir,
|
|
197
|
+
mode: input.result.details.mode,
|
|
198
|
+
state: status.state,
|
|
199
|
+
...(firstText(input.result) ? { summary: firstText(input.result) } : {}),
|
|
200
|
+
}) ?? mission;
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error(`Failed to reconcile mission from terminal async status '${statusPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const lastTextIndex = input.result.content.findLastIndex((item) => item.type === "text");
|
|
208
|
+
const hasStructuredOutput = input.result.details.results.some((child) => child.structuredOutputPath !== undefined);
|
|
209
|
+
const textIsJson = lastTextIndex >= 0 && (() => {
|
|
210
|
+
try {
|
|
211
|
+
JSON.parse((input.result.content[lastTextIndex] as { type: "text"; text: string }).text);
|
|
212
|
+
return true;
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
return {
|
|
218
|
+
...input.result,
|
|
219
|
+
content: input.binding.announceInContent === true && lastTextIndex >= 0 && !hasStructuredOutput && !textIsJson
|
|
220
|
+
? input.result.content.map((item, index) => index === lastTextIndex && item.type === "text"
|
|
221
|
+
? { ...item, text: `${item.text}\nMission: ${mission.id} (${mission.status})` }
|
|
222
|
+
: item)
|
|
223
|
+
: input.result.content,
|
|
224
|
+
details: {
|
|
225
|
+
...input.result.details,
|
|
226
|
+
missionId: mission.id,
|
|
227
|
+
missionPath: missionRecordPath(input.binding.location, mission.id),
|
|
228
|
+
mission,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parsePersistedBinding(value: unknown, source: string): MissionLaunchBinding {
|
|
234
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${source} must be an object`);
|
|
235
|
+
const input = value as Record<string, unknown>;
|
|
236
|
+
if (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);
|
|
237
|
+
for (const field of ["projectRoot", "missionDir", "globalIndexDir"] as const) {
|
|
238
|
+
if (typeof input[field] !== "string" || !input[field].trim()) throw new Error(`${source}.${field} must be a non-empty string`);
|
|
239
|
+
}
|
|
240
|
+
if (typeof input.writeGlobalIndex !== "boolean") throw new Error(`${source}.writeGlobalIndex must be boolean`);
|
|
241
|
+
if (input.retainTerminal !== undefined && (!Number.isInteger(input.retainTerminal) || (input.retainTerminal as number) < 1)) throw new Error(`${source}.retainTerminal must be a positive integer`);
|
|
242
|
+
return {
|
|
243
|
+
missionId: validateMissionId(input.missionId, `${source}.missionId`),
|
|
244
|
+
autoCreated: false,
|
|
245
|
+
location: {
|
|
246
|
+
projectRoot: input.projectRoot as string,
|
|
247
|
+
missionDir: input.missionDir as string,
|
|
248
|
+
globalIndexDir: input.globalIndexDir as string,
|
|
249
|
+
writeGlobalIndex: input.writeGlobalIndex,
|
|
250
|
+
...(input.retainTerminal !== undefined ? { retainTerminal: input.retainTerminal as number } : {}),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function readMissionBinding(asyncDir: string): MissionLaunchBinding | undefined {
|
|
256
|
+
const bindingPath = path.join(asyncDir, MISSION_BINDING_FILE);
|
|
257
|
+
if (!fs.existsSync(bindingPath)) return undefined;
|
|
258
|
+
return parsePersistedBinding(JSON.parse(fs.readFileSync(bindingPath, "utf-8")), bindingPath);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function syncMissionFromAsyncCompletion(value: unknown): MissionRecord | undefined {
|
|
262
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
263
|
+
const event = value as Record<string, unknown>;
|
|
264
|
+
if (typeof event.asyncDir !== "string" || !event.asyncDir.trim()) return undefined;
|
|
265
|
+
const binding = readMissionBinding(event.asyncDir);
|
|
266
|
+
if (!binding) return undefined;
|
|
267
|
+
const runId = typeof event.runId === "string" ? event.runId : typeof event.id === "string" ? event.id : undefined;
|
|
268
|
+
if (!runId) throw new Error("Async mission completion is missing runId");
|
|
269
|
+
const runStatus = typeof event.state === "string" ? event.state : event.success === true ? "completed" : "failed";
|
|
270
|
+
let current: MissionRecord;
|
|
271
|
+
try {
|
|
272
|
+
current = readMission(binding.location, binding.missionId);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (!(error instanceof MissionNotFoundError)) throw error;
|
|
275
|
+
try {
|
|
276
|
+
fs.appendFileSync(path.join(event.asyncDir, "events.jsonl"), `${JSON.stringify({
|
|
277
|
+
type: "subagent.mission.sync.skipped",
|
|
278
|
+
ts: Date.now(),
|
|
279
|
+
runId,
|
|
280
|
+
missionId: binding.missionId,
|
|
281
|
+
reason: "mission-record-missing",
|
|
282
|
+
missionPath: missionRecordPath(binding.location, binding.missionId),
|
|
283
|
+
})}\n`, "utf-8");
|
|
284
|
+
} catch {
|
|
285
|
+
// Mission bookkeeping is secondary to preserving the completed async result.
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
const completedAt = new Date().toISOString();
|
|
290
|
+
const artifacts: MissionArtifact[] = [
|
|
291
|
+
{ kind: "status", path: path.join(event.asyncDir, "status.json") },
|
|
292
|
+
{ kind: "other", path: path.join(event.asyncDir, "events.jsonl"), description: "Lifecycle events" },
|
|
293
|
+
];
|
|
294
|
+
if (event.parallelHandoff && typeof event.parallelHandoff === "object" && typeof (event.parallelHandoff as { path?: unknown }).path === "string") {
|
|
295
|
+
artifacts.push({ kind: "manifest", path: (event.parallelHandoff as { path: string }).path });
|
|
296
|
+
}
|
|
297
|
+
if (Array.isArray(event.results)) {
|
|
298
|
+
for (const result of event.results) {
|
|
299
|
+
if (!result || typeof result !== "object") continue;
|
|
300
|
+
const child = result as Record<string, unknown>;
|
|
301
|
+
if (typeof child.artifactPath === "string") artifacts.push({ kind: "output", path: child.artifactPath });
|
|
302
|
+
if (child.artifactPaths && typeof child.artifactPaths === "object" && typeof (child.artifactPaths as { outputPath?: unknown }).outputPath === "string") {
|
|
303
|
+
artifacts.push({ kind: "output", path: (child.artifactPaths as { outputPath: string }).outputPath });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const summary = typeof event.summary === "string" && event.summary.trim() ? event.summary.slice(0, 2000) : undefined;
|
|
308
|
+
return updateMission(binding.location, binding.missionId, {
|
|
309
|
+
status: missionStatusForRun(current, runId, runStatus),
|
|
310
|
+
addRuns: [{ runId, mode: typeof event.mode === "string" && ["single", "parallel", "chain"].includes(event.mode) ? event.mode as SubagentRunMode : "external", asyncDir: event.asyncDir, status: runStatus, completedAt }],
|
|
311
|
+
addArtifacts: artifacts,
|
|
312
|
+
...(summary ? { summary } : {}),
|
|
313
|
+
});
|
|
314
|
+
}
|