@pi-unipi/subagents 2.6.1 → 2.9.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/README.md +89 -78
- package/agents/delegate.md +14 -0
- package/agents/oracle.md +78 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/worker.md +59 -0
- package/package.json +22 -20
- package/prompts/council.md +48 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +43 -0
- package/skills/council-mode/SKILL.md +230 -0
- package/skills/subagents/SKILL.md +49 -0
- package/skills/subagents/references/constraints-and-recipes.md +259 -0
- package/skills/subagents/references/execution-controls.md +454 -0
- package/skills/subagents/references/management-authoring-rpc.md +161 -0
- package/skills/subagents/references/multi-lane-orchestration.md +39 -0
- package/skills/subagents/references/prompting-and-roles.md +267 -0
- package/src/acceptance.ts +331 -0
- package/src/agent-manager.ts +405 -0
- package/src/agent-memory.ts +300 -0
- package/src/agent-overrides.ts +183 -0
- package/src/agent-runner.ts +271 -0
- package/src/async-runner.ts +417 -0
- package/src/authority-policy.ts +47 -0
- package/src/budgets.ts +285 -0
- package/src/child-safety.ts +140 -0
- package/src/config.ts +226 -0
- package/src/conversation-viewer.ts +281 -0
- package/src/core-compat.ts +94 -0
- package/src/custom-agents.ts +323 -0
- package/src/file-system-retry.ts +50 -0
- package/src/fleet-data.ts +61 -0
- package/src/fleet-view.ts +316 -0
- package/src/foreground-detach.ts +59 -0
- package/src/fork-context.ts +285 -0
- package/src/global.d.ts +7 -0
- package/src/guide.ts +129 -0
- package/src/index.ts +1149 -0
- package/src/mission-state.ts +133 -0
- package/src/mission-store.ts +434 -0
- package/src/model-resolver.ts +79 -0
- package/src/output-limits.ts +142 -0
- package/src/parity-types.ts +315 -0
- package/src/pi-args.ts +161 -0
- package/src/pi-spawn.ts +147 -0
- package/src/result-files.ts +232 -0
- package/src/result-watcher.ts +221 -0
- package/src/retained-children.ts +176 -0
- package/src/run-fanout-budget.ts +356 -0
- package/src/scheduled-runs.ts +351 -0
- package/src/schemas.ts +263 -0
- package/src/slash-commands.ts +77 -0
- package/src/supervisor-channel.ts +249 -0
- package/src/tool-handler.ts +1144 -0
- package/src/types.ts +238 -0
- package/src/widget.ts +447 -0
- package/src/workflow-script.ts +737 -0
- package/src/workflow-worker.ts +384 -0
- package/src/worktree.ts +614 -0
- package/dist/agent-manager.d.ts +0 -81
- package/dist/agent-manager.d.ts.map +0 -1
- package/dist/agent-manager.js +0 -295
- package/dist/agent-manager.js.map +0 -1
- package/dist/agent-runner.d.ts +0 -51
- package/dist/agent-runner.d.ts.map +0 -1
- package/dist/agent-runner.js +0 -262
- package/dist/agent-runner.js.map +0 -1
- package/dist/config.d.ts +0 -24
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -132
- package/dist/config.js.map +0 -1
- package/dist/conversation-viewer.d.ts +0 -40
- package/dist/conversation-viewer.d.ts.map +0 -1
- package/dist/conversation-viewer.js +0 -276
- package/dist/conversation-viewer.js.map +0 -1
- package/dist/core-compat.d.ts +0 -21
- package/dist/core-compat.d.ts.map +0 -1
- package/dist/core-compat.js +0 -86
- package/dist/core-compat.js.map +0 -1
- package/dist/custom-agents.d.ts +0 -14
- package/dist/custom-agents.d.ts.map +0 -1
- package/dist/custom-agents.js +0 -113
- package/dist/custom-agents.js.map +0 -1
- package/dist/file-lock.d.ts +0 -42
- package/dist/file-lock.d.ts.map +0 -1
- package/dist/file-lock.js +0 -91
- package/dist/file-lock.js.map +0 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -758
- package/dist/index.js.map +0 -1
- package/dist/model-resolver.d.ts +0 -19
- package/dist/model-resolver.d.ts.map +0 -1
- package/dist/model-resolver.js +0 -61
- package/dist/model-resolver.js.map +0 -1
- package/dist/types.d.ts +0 -98
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -47
- package/dist/types.js.map +0 -1
- package/dist/widget.d.ts +0 -56
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -396
- package/dist/widget.js.map +0 -1
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — spawn_helper parity handler
|
|
3
|
+
*
|
|
4
|
+
* Routes the extended spawn_helper surface: management actions, workflowScript
|
|
5
|
+
* execution, and legacy single-child launches (type/prompt aliases). Applies
|
|
6
|
+
* the Phase 2 libraries: agent resolution + aliases, enablement, budgets,
|
|
7
|
+
* depth guard, context policy, spawn budgets, timeout defaults, and output
|
|
8
|
+
* truncation. The render/notify plumbing stays in index.ts — this module only
|
|
9
|
+
* decides and executes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { AgentManager } from "./agent-manager.js";
|
|
15
|
+
import type { SubagentsConfig, AgentConfig } from "./types.js";
|
|
16
|
+
import { SUBAGENT_ACTIONS } from "./parity-types.js";
|
|
17
|
+
import { runWorkflowScript, type WorkflowScriptChildResult } from "./workflow-script.js";
|
|
18
|
+
import {
|
|
19
|
+
resolveTurnBudgetConfig,
|
|
20
|
+
validateToolBudgetConfig,
|
|
21
|
+
validateUsageBudgetConfig,
|
|
22
|
+
appendTurnBudgetSystemPrompt,
|
|
23
|
+
} from "./budgets.js";
|
|
24
|
+
import {
|
|
25
|
+
createRunFanoutBudget,
|
|
26
|
+
claimRunFanoutBatch,
|
|
27
|
+
getRunFanoutBudgetSnapshot,
|
|
28
|
+
formatRunFanoutBudget,
|
|
29
|
+
} from "./run-fanout-budget.js";
|
|
30
|
+
import { truncateOutput, resolveMaxOutput } from "./output-limits.js";
|
|
31
|
+
import {
|
|
32
|
+
resolveContext,
|
|
33
|
+
assertForegroundContextSupported,
|
|
34
|
+
resolveMaxSubagentDepth,
|
|
35
|
+
depthExceeded,
|
|
36
|
+
isFanoutChild,
|
|
37
|
+
withChildBoundaryInstructions,
|
|
38
|
+
} from "./child-safety.js";
|
|
39
|
+
import { listRetainedChildren, formatRetainedChildren, resolveResumeTarget } from "./retained-children.js";
|
|
40
|
+
import { ASYNC_DIR, TEMP_ROOT_DIR, RESULTS_DIR } from "./parity-types.js";
|
|
41
|
+
import { buildGuideText } from "./guide.js";
|
|
42
|
+
import { ScheduledRunManager } from "./scheduled-runs.js";
|
|
43
|
+
import {
|
|
44
|
+
validateAcceptanceInput,
|
|
45
|
+
normalizeAcceptanceInput,
|
|
46
|
+
normalizeGateAcceptance,
|
|
47
|
+
evaluateAcceptance,
|
|
48
|
+
stripAcceptanceReport,
|
|
49
|
+
} from "./acceptance.js";
|
|
50
|
+
import type { AcceptanceConfig } from "./acceptance.js";
|
|
51
|
+
import {
|
|
52
|
+
resolveMissionStoreLocation,
|
|
53
|
+
createMission,
|
|
54
|
+
readMission,
|
|
55
|
+
listMissions,
|
|
56
|
+
updateMission,
|
|
57
|
+
MissionNotFoundError,
|
|
58
|
+
type MissionStoreConfig,
|
|
59
|
+
type MissionStatus,
|
|
60
|
+
} from "./mission-store.js";
|
|
61
|
+
|
|
62
|
+
export interface HandlerDeps {
|
|
63
|
+
pi: ExtensionAPI;
|
|
64
|
+
manager: AgentManager;
|
|
65
|
+
config: SubagentsConfig;
|
|
66
|
+
/** Legacy single-child spawn (foreground). */
|
|
67
|
+
spawnForeground: (
|
|
68
|
+
ctx: ExtensionContext,
|
|
69
|
+
agentName: string,
|
|
70
|
+
prompt: string,
|
|
71
|
+
options: {
|
|
72
|
+
description?: string;
|
|
73
|
+
maxTurns?: number;
|
|
74
|
+
modelInput?: string;
|
|
75
|
+
thinkingLevel?: unknown;
|
|
76
|
+
signal?: AbortSignal;
|
|
77
|
+
},
|
|
78
|
+
) => Promise<{ ok: boolean; output: string; error?: string; toolUses: number; durationMs: number }>;
|
|
79
|
+
/** Legacy single-child spawn (background). */
|
|
80
|
+
spawnBackground: (
|
|
81
|
+
ctx: ExtensionContext,
|
|
82
|
+
agentName: string,
|
|
83
|
+
prompt: string,
|
|
84
|
+
options: {
|
|
85
|
+
description?: string;
|
|
86
|
+
maxTurns?: number;
|
|
87
|
+
modelInput?: string;
|
|
88
|
+
thinkingLevel?: unknown;
|
|
89
|
+
},
|
|
90
|
+
) => string;
|
|
91
|
+
/** Session-wide cumulative spawn accounting (maxSubagentSpawnsPerSession). */
|
|
92
|
+
spawnAccounting?: {
|
|
93
|
+
used(): number;
|
|
94
|
+
cap(): number | undefined;
|
|
95
|
+
consume(count: number): void;
|
|
96
|
+
};
|
|
97
|
+
/** Depth env for the recursion guard. */
|
|
98
|
+
env?: NodeJS.ProcessEnv;
|
|
99
|
+
/** Retained-children dir override (defaults to our async temp root). */
|
|
100
|
+
retainedDir?: string;
|
|
101
|
+
/** Mission store config (from OUR subagents.json). */
|
|
102
|
+
missionConfig?: MissionStoreConfig;
|
|
103
|
+
/** Project root for mission records. */
|
|
104
|
+
projectRoot?: string;
|
|
105
|
+
/** Async process runner (Phase 3). When present, background launches and
|
|
106
|
+
* fork contexts run as child pi processes; when absent they error with
|
|
107
|
+
* guidance (test environments). */
|
|
108
|
+
runAsync?: (launch: {
|
|
109
|
+
agentName: string;
|
|
110
|
+
task: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
model?: string;
|
|
113
|
+
thinking?: string | false;
|
|
114
|
+
context: "fresh" | "fork";
|
|
115
|
+
timeoutMs?: number;
|
|
116
|
+
maxTurns?: number;
|
|
117
|
+
/** Resume: continue a retained child in its stored session. */
|
|
118
|
+
resumeSessionFile?: string;
|
|
119
|
+
/** Worktree isolation: launch the child in a managed git worktree. */
|
|
120
|
+
worktree?: boolean;
|
|
121
|
+
}) => Promise<{
|
|
122
|
+
runId: string;
|
|
123
|
+
status: string;
|
|
124
|
+
output?: string;
|
|
125
|
+
error?: string;
|
|
126
|
+
}>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function textResult(msg: string, details?: unknown) {
|
|
130
|
+
return { content: [{ type: "text" as const, text: msg }], details };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve the effective acceptance config: explicit gate shorthand wins over
|
|
135
|
+
* acceptance; agent-level defaultAcceptance fills gaps. Returns undefined for
|
|
136
|
+
* no gate. Errors are visible strings.
|
|
137
|
+
*/
|
|
138
|
+
function resolveEffectiveAcceptance(
|
|
139
|
+
args: Record<string, unknown>,
|
|
140
|
+
): { ok: true; acceptance?: AcceptanceConfig } | { ok: false; error: string } {
|
|
141
|
+
if (args.gate !== undefined && args.acceptance !== undefined) {
|
|
142
|
+
return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
|
|
143
|
+
}
|
|
144
|
+
if (args.gate !== undefined) {
|
|
145
|
+
const normalized = normalizeGateAcceptance(args.gate);
|
|
146
|
+
if (!normalized) return { ok: false, error: "gate must be a non-empty command string." };
|
|
147
|
+
return { ok: true, acceptance: normalized };
|
|
148
|
+
}
|
|
149
|
+
if (args.acceptance === undefined) return { ok: true, acceptance: undefined };
|
|
150
|
+
const errors = validateAcceptanceInput(args.acceptance);
|
|
151
|
+
if (errors.length > 0) return { ok: false, error: errors.join(" ") };
|
|
152
|
+
return { ok: true, acceptance: normalizeAcceptanceInput(args.acceptance) };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Post-run acceptance evaluation wrapper. */
|
|
156
|
+
async function evaluateRunAcceptance(
|
|
157
|
+
acceptance: AcceptanceConfig | undefined,
|
|
158
|
+
output: string,
|
|
159
|
+
cwd: string,
|
|
160
|
+
): Promise<{ output: string; ledger?: Awaited<ReturnType<typeof evaluateAcceptance>> }> {
|
|
161
|
+
if (!acceptance || acceptance.level === "auto") return { output };
|
|
162
|
+
const ledger = await evaluateAcceptance({ acceptance, output, cwd });
|
|
163
|
+
const cleanOutput = stripAcceptanceReport(output);
|
|
164
|
+
if (ledger.status === "rejected") {
|
|
165
|
+
return { output: cleanOutput, ledger };
|
|
166
|
+
}
|
|
167
|
+
return { output: cleanOutput, ledger };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Parse + validate everything, resolve the agent, or return an error result. */
|
|
171
|
+
function preflight(
|
|
172
|
+
deps: HandlerDeps,
|
|
173
|
+
args: Record<string, unknown>,
|
|
174
|
+
): { ok: true; agent: AgentConfig; agentName: string; prompt: string } | { ok: false; error: string } {
|
|
175
|
+
const agentNameRaw = (args.agent ?? args.type) as string | undefined;
|
|
176
|
+
const prompt = (args.task ?? args.prompt) as string | undefined;
|
|
177
|
+
|
|
178
|
+
if (!agentNameRaw || typeof agentNameRaw !== "string") {
|
|
179
|
+
return { ok: false, error: "spawn_helper requires an agent (or legacy type) parameter." };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const agentName = deps.manager.resolveAlias(agentNameRaw);
|
|
183
|
+
const agent = deps.manager.getAgentConfig(agentName);
|
|
184
|
+
if (!agent) {
|
|
185
|
+
const known = deps.manager.getKnownTypes().join(", ");
|
|
186
|
+
return { ok: false, error: `Unknown agent type "${agentNameRaw}". Known types: ${known}` };
|
|
187
|
+
}
|
|
188
|
+
if (!deps.manager.isTypeEnabled(agentName)) {
|
|
189
|
+
return { ok: false, error: `Agent type "${agentName}" is disabled by configuration.` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Depth guard
|
|
193
|
+
const env = deps.env ?? process.env;
|
|
194
|
+
const maxDepth = resolveMaxSubagentDepth(agent, deps.config, env);
|
|
195
|
+
if (depthExceeded(env, maxDepth)) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
error: `Subagent depth cap reached (maxSubagentDepth ${maxDepth}). This child cannot spawn further subagents for its assigned fanout.`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!prompt || typeof prompt !== "string" || !prompt.trim()) {
|
|
203
|
+
return { ok: false, error: "spawn_helper requires a task (or legacy prompt) parameter." };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { ok: true, agent, agentName, prompt };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Main entry: route a spawn_helper invocation.
|
|
211
|
+
* Returns a tool result; never throws.
|
|
212
|
+
*/
|
|
213
|
+
export async function handleSpawnHelper(
|
|
214
|
+
deps: HandlerDeps,
|
|
215
|
+
ctx: ExtensionContext,
|
|
216
|
+
args: Record<string, unknown>,
|
|
217
|
+
signal?: AbortSignal,
|
|
218
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details?: unknown }> {
|
|
219
|
+
try {
|
|
220
|
+
// ---- Management actions ----
|
|
221
|
+
const action = args.action as string | undefined;
|
|
222
|
+
if (action !== undefined) {
|
|
223
|
+
return await handleAction(deps, ctx, action, args);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---- workflowScript execution ----
|
|
227
|
+
const workflowScript = args.workflowScript as string | undefined;
|
|
228
|
+
if (workflowScript !== undefined) {
|
|
229
|
+
return await handleWorkflowScript(deps, ctx, workflowScript, args, signal);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---- Single-child launch (legacy + reference params) ----
|
|
233
|
+
return await handleSingleChild(deps, ctx, args, signal);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return textResult(
|
|
236
|
+
`spawn_helper failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
237
|
+
{ status: "error", error: error instanceof Error ? error.message : String(error) },
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// Management actions
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
async function handleAction(
|
|
247
|
+
deps: HandlerDeps,
|
|
248
|
+
_ctx: ExtensionContext,
|
|
249
|
+
action: string,
|
|
250
|
+
args: Record<string, unknown>,
|
|
251
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details?: unknown }> {
|
|
252
|
+
if (!(SUBAGENT_ACTIONS as readonly string[]).includes(action)) {
|
|
253
|
+
return textResult(
|
|
254
|
+
`Unknown action "${action}". Supported actions: ${SUBAGENT_ACTIONS.join(", ")}.`,
|
|
255
|
+
{ status: "error" },
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
switch (action) {
|
|
260
|
+
case "list": {
|
|
261
|
+
const types = deps.manager.getKnownTypes();
|
|
262
|
+
const lines = types.map((type) => {
|
|
263
|
+
const agent = deps.manager.getAgentConfig(type);
|
|
264
|
+
const enabled = deps.manager.isTypeEnabled(type);
|
|
265
|
+
const scope = agent?.source ?? "config";
|
|
266
|
+
const desc = agent?.description ?? "";
|
|
267
|
+
return `- ${type}${agent?.aliases?.length ? ` (aliases: ${agent.aliases.join(", ")})` : ""} [${scope}]${enabled ? "" : " [disabled]"}: ${desc}`;
|
|
268
|
+
});
|
|
269
|
+
return textResult(`Available subagents:\n${lines.join("\n")}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case "get": {
|
|
273
|
+
const target = (args.agent ?? args.type ?? args.id) as string | undefined;
|
|
274
|
+
if (!target) return textResult("action 'get' requires an agent name.");
|
|
275
|
+
const agentName = deps.manager.resolveAlias(target);
|
|
276
|
+
const agent = deps.manager.getAgentConfig(agentName);
|
|
277
|
+
if (!agent) return textResult(`Unknown agent "${target}".`);
|
|
278
|
+
return textResult(
|
|
279
|
+
[
|
|
280
|
+
`Agent: ${agent.name}`,
|
|
281
|
+
...(agent.aliases?.length ? [`Aliases: ${agent.aliases.join(", ")}`] : []),
|
|
282
|
+
`Description: ${agent.description}`,
|
|
283
|
+
`Source: ${agent.source ?? "builtin"}`,
|
|
284
|
+
`Tools: ${agent.builtinToolNames?.join(", ") || "(all builtins)"}`,
|
|
285
|
+
...(agent.disallowedTools?.length ? [`Disallowed: ${agent.disallowedTools.join(", ")}`] : []),
|
|
286
|
+
`Prompt mode: ${agent.promptMode}`,
|
|
287
|
+
...(agent.model ? [`Model: ${agent.model}`] : []),
|
|
288
|
+
...(agent.thinking !== undefined ? [`Thinking: ${String(agent.thinking)}`] : []),
|
|
289
|
+
...(agent.defaultContext ? [`Default context: ${agent.defaultContext}`] : []),
|
|
290
|
+
...(agent.timeoutMs ? [`Timeout: ${agent.timeoutMs}ms`] : []),
|
|
291
|
+
`Enabled: ${deps.manager.isTypeEnabled(agentName)}`,
|
|
292
|
+
].join("\n"),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case "status": {
|
|
297
|
+
const accounting = deps.spawnAccounting;
|
|
298
|
+
const sessionCap = accounting?.cap();
|
|
299
|
+
const used = accounting?.used() ?? 0;
|
|
300
|
+
const lines = [
|
|
301
|
+
`Active agents: ${deps.manager.listAgents().filter((a) => a.status === "running" || a.status === "queued").length}`,
|
|
302
|
+
`Max concurrent: ${deps.manager.getMaxConcurrent()}`,
|
|
303
|
+
];
|
|
304
|
+
if (sessionCap !== undefined) {
|
|
305
|
+
lines.push(
|
|
306
|
+
`Session spawn budget: ${used}/${sessionCap} used, ${Math.max(0, sessionCap - used)} remaining`,
|
|
307
|
+
);
|
|
308
|
+
} else {
|
|
309
|
+
lines.push("Session spawn budget: unlimited");
|
|
310
|
+
}
|
|
311
|
+
const running = deps.manager.listAgents().filter((a) => a.status === "running" || a.status === "queued");
|
|
312
|
+
for (const record of running) {
|
|
313
|
+
lines.push(`- ${record.id} [${record.type}] ${record.status}: ${record.description}`);
|
|
314
|
+
}
|
|
315
|
+
return textResult(lines.join("\n"));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case "children.list": {
|
|
319
|
+
const retained = listRetainedChildren(deps.retainedDir ?? ASYNC_DIR);
|
|
320
|
+
if (retained.length === 0) {
|
|
321
|
+
// Fall back to in-process completed agents from this session.
|
|
322
|
+
const recent = deps.manager
|
|
323
|
+
.listAgents()
|
|
324
|
+
.filter((a) => a.status !== "running" && a.status !== "queued")
|
|
325
|
+
.slice(-10);
|
|
326
|
+
if (recent.length === 0) return textResult("No completed children in this session yet.");
|
|
327
|
+
return textResult(
|
|
328
|
+
recent.map((r) => `- ${r.id} [${r.type}] ${r.status}: ${r.description}`).join("\n"),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return textResult(formatRetainedChildren(retained));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
case "resume": {
|
|
335
|
+
const id = args.id as string | undefined;
|
|
336
|
+
const message = args.message as string | undefined;
|
|
337
|
+
if (!id) return textResult("action 'resume' requires an id (retained run id or prefix).");
|
|
338
|
+
if (!message || !message.trim()) return textResult("action 'resume' requires a non-empty follow-up message.");
|
|
339
|
+
if (!deps.runAsync) {
|
|
340
|
+
return textResult("action 'resume' requires the background process runner, which is unavailable in this host.", { status: "error" });
|
|
341
|
+
}
|
|
342
|
+
const target = resolveResumeTarget(deps.retainedDir ?? ASYNC_DIR, id);
|
|
343
|
+
if (!target.ok) return textResult(target.error, { status: "error" });
|
|
344
|
+
const result = await deps.runAsync({
|
|
345
|
+
agentName: target.agent,
|
|
346
|
+
task: message.trim(),
|
|
347
|
+
description: `resume ${target.runId.slice(-8)}`,
|
|
348
|
+
context: "fresh",
|
|
349
|
+
resumeSessionFile: target.sessionFile,
|
|
350
|
+
});
|
|
351
|
+
return textResult(
|
|
352
|
+
`Resumed ${target.runId} as ${result.runId} (agent: ${target.agent}, stored session contract kept).\nUse get_helper_result to retrieve results.`,
|
|
353
|
+
{ status: "background", runId: result.runId },
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
case "stop": {
|
|
358
|
+
const id = args.id as string | undefined;
|
|
359
|
+
if (!id) return textResult("action 'stop' requires an id.");
|
|
360
|
+
const record = deps.manager.getRecord(id);
|
|
361
|
+
if (!record) return textResult(`Run not found: "${id}".`);
|
|
362
|
+
record.abortController?.abort();
|
|
363
|
+
return textResult(`Stop requested for ${id} (${record.type}, ${record.status}).`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
case "grant-spawn-budget": {
|
|
367
|
+
const accounting = deps.spawnAccounting;
|
|
368
|
+
if (!accounting) return textResult("Spawn budget accounting is unavailable in this host.");
|
|
369
|
+
const cap = accounting.cap();
|
|
370
|
+
if (cap === undefined) return textResult("Session spawn budget is unlimited; nothing to grant.");
|
|
371
|
+
const additional = args.additional as number | undefined;
|
|
372
|
+
if (typeof additional !== "number" || !Number.isInteger(additional) || additional < 1) {
|
|
373
|
+
return textResult("action 'grant-spawn-budget' requires a positive integer 'additional'.");
|
|
374
|
+
}
|
|
375
|
+
// Grants require interactive confirmation — delegated to the host via pi.
|
|
376
|
+
return textResult(
|
|
377
|
+
`grant-spawn-budget requires native user confirmation; requested +${additional} against cap ${cap}. ` +
|
|
378
|
+
`This action needs the interactive parent; run it from the root session.`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
case "doctor": {
|
|
383
|
+
const lines: string[] = [
|
|
384
|
+
"Subagents doctor report",
|
|
385
|
+
"",
|
|
386
|
+
"Runtime",
|
|
387
|
+
`- cwd: ${process.cwd()}`,
|
|
388
|
+
`- async runner: ${deps.runAsync ? "available (child pi processes)" : "unavailable"}`,
|
|
389
|
+
"",
|
|
390
|
+
"Filesystem",
|
|
391
|
+
`- temp root: ${TEMP_ROOT_DIR}${existsSync(TEMP_ROOT_DIR) ? "" : " (created on first run)"}`,
|
|
392
|
+
`- async runs: ${ASYNC_DIR}`,
|
|
393
|
+
`- results: ${RESULTS_DIR}`,
|
|
394
|
+
"",
|
|
395
|
+
"Discovery",
|
|
396
|
+
`- known agent types: ${deps.manager.getKnownTypes().length}`,
|
|
397
|
+
...deps.manager.getKnownTypes().map((type) => {
|
|
398
|
+
const agent = deps.manager.getAgentConfig(type);
|
|
399
|
+
const enabled = deps.manager.isTypeEnabled(type);
|
|
400
|
+
return ` - ${type} [${agent?.source ?? "config"}]${enabled ? "" : " [disabled]"}${agent?.aliases?.length ? ` (aliases: ${agent.aliases.join(", ")})` : ""}`;
|
|
401
|
+
}),
|
|
402
|
+
"",
|
|
403
|
+
"Budgets",
|
|
404
|
+
`- run timeout default: ${deps.config.timeoutMs ?? "30min (default)"}`,
|
|
405
|
+
`- tool timeout: ${deps.config.toolTimeoutMs ?? "env/known-fast defaults"}`,
|
|
406
|
+
`- spawn caps: run=${deps.config.maxSubagentSpawnsPerRun ?? 64}, session=${deps.config.maxSubagentSpawnsPerSession ?? "unlimited"}, active-async=${deps.config.maxActiveAsyncRunsPerSession ?? "unlimited"}`,
|
|
407
|
+
`- depth cap: ${resolveMaxSubagentDepth(undefined, deps.config, deps.env ?? process.env)}`,
|
|
408
|
+
"",
|
|
409
|
+
"Concurrency",
|
|
410
|
+
`- maxConcurrent: ${deps.config.maxConcurrent}`,
|
|
411
|
+
`- active in-process: ${deps.manager.listAgents().filter((a) => a.status === "running" || a.status === "queued").length}`,
|
|
412
|
+
`- retained children: ${listRetainedChildren(deps.retainedDir ?? ASYNC_DIR).length}`,
|
|
413
|
+
];
|
|
414
|
+
return textResult(lines.join("\n"));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
case "guide": {
|
|
418
|
+
const topic = (args.topic as string | undefined) ?? "overview";
|
|
419
|
+
return textResult(buildGuideText(topic));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
case "mission.create": {
|
|
423
|
+
const location = resolveMissionStoreLocation({
|
|
424
|
+
projectRoot: deps.projectRoot ?? process.cwd(),
|
|
425
|
+
config: deps.missionConfig,
|
|
426
|
+
});
|
|
427
|
+
const missionInput = (args.mission ?? {}) as Record<string, unknown>;
|
|
428
|
+
const title = (missionInput.title ?? args.name) as string | undefined;
|
|
429
|
+
if (!title || typeof title !== "string" || !title.trim()) {
|
|
430
|
+
return textResult("action 'mission.create' requires mission.title (or name).", { status: "error" });
|
|
431
|
+
}
|
|
432
|
+
const objective = (missionInput.objective as string | undefined) ?? `${title}`;
|
|
433
|
+
try {
|
|
434
|
+
const record = createMission(location, {
|
|
435
|
+
title: title.trim(),
|
|
436
|
+
objective: objective.trim(),
|
|
437
|
+
...(missionInput.goal === true ? { goal: true } : {}),
|
|
438
|
+
...(missionInput.budget ? { budget: missionInput.budget as { tokens: number } } : {}),
|
|
439
|
+
...(typeof missionInput.labels === "object" ? { labels: missionInput.labels as string[] } : {}),
|
|
440
|
+
});
|
|
441
|
+
return textResult(
|
|
442
|
+
`Mission created: ${record.id}\nTitle: ${record.title}\nStatus: ${record.status}\nAttach runs with missionId: ${record.id}`,
|
|
443
|
+
{ status: "completed", missionId: record.id },
|
|
444
|
+
);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return textResult(`mission.create failed: ${error instanceof Error ? error.message : String(error)}`, { status: "error" });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
case "mission.list": {
|
|
451
|
+
const location = resolveMissionStoreLocation({
|
|
452
|
+
projectRoot: deps.projectRoot ?? process.cwd(),
|
|
453
|
+
config: deps.missionConfig,
|
|
454
|
+
});
|
|
455
|
+
const { records, warnings } = listMissions(location);
|
|
456
|
+
if (records.length === 0) return textResult("No missions for this project.");
|
|
457
|
+
const lines = records.map((r) => `- ${r.id} [${r.status}] ${r.title}${r.summary ? ` — ${r.summary.slice(0, 80)}` : ""}`);
|
|
458
|
+
return textResult(
|
|
459
|
+
[`Missions (${records.length}):`, ...lines, ...(warnings.length ? ["", ...warnings] : [])].join("\n"),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
case "mission.show":
|
|
464
|
+
case "mission.update":
|
|
465
|
+
case "mission.resolve-decision":
|
|
466
|
+
case "mission.attach-run":
|
|
467
|
+
case "mission.close": {
|
|
468
|
+
const missionId = args.missionId as string | undefined;
|
|
469
|
+
if (!missionId) return textResult(`action '${action}' requires missionId.`, { status: "error" });
|
|
470
|
+
const location = resolveMissionStoreLocation({
|
|
471
|
+
projectRoot: deps.projectRoot ?? process.cwd(),
|
|
472
|
+
config: deps.missionConfig,
|
|
473
|
+
});
|
|
474
|
+
try {
|
|
475
|
+
if (action === "mission.show") {
|
|
476
|
+
const record = readMission(location, missionId);
|
|
477
|
+
return textResult(
|
|
478
|
+
[
|
|
479
|
+
`Mission ${record.id} [${record.status}]`,
|
|
480
|
+
`Title: ${record.title}`,
|
|
481
|
+
`Objective: ${record.objective}`,
|
|
482
|
+
...(record.goal ? [`Goal: ${record.goal.status}${record.budget ? ` (${record.usage?.tokens ?? 0}/${record.budget.tokens} tokens)` : ""}`] : []),
|
|
483
|
+
`Runs: ${record.runs.length} | Decisions: ${record.decisions.filter((d) => d.status === "open").length} open / ${record.decisions.length} total`,
|
|
484
|
+
...(record.summary ? [`Summary: ${record.summary}`] : []),
|
|
485
|
+
...(record.runs.length ? ["", "Runs:", ...record.runs.map((r) => `- ${r.runId}${r.agent ? ` (${r.agent})` : ""} ${r.status ?? ""}`)] : []),
|
|
486
|
+
].join("\n"),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (action === "mission.close") {
|
|
490
|
+
const summary = args.summary as string | undefined;
|
|
491
|
+
const status: MissionStatus = (args.missionStatus as MissionStatus | undefined) ?? (summary ? "completed" : "cancelled");
|
|
492
|
+
const record = updateMission(location, missionId, { status, ...(summary ? { summary } : {}) });
|
|
493
|
+
return textResult(`Mission ${record.id} closed as ${record.status}.`, { status: "completed", missionId: record.id });
|
|
494
|
+
}
|
|
495
|
+
if (action === "mission.attach-run") {
|
|
496
|
+
const runId = (args.runId ?? args.id) as string | undefined;
|
|
497
|
+
if (!runId) return textResult("mission.attach-run requires runId (or id).", { status: "error" });
|
|
498
|
+
const record = updateMission(location, missionId, {
|
|
499
|
+
addRun: { runId, ...(typeof args.missionStatus === "string" ? { status: args.missionStatus } : {}) },
|
|
500
|
+
});
|
|
501
|
+
return textResult(`Run ${runId} attached to mission ${record.id}.`, { status: "completed", missionId: record.id });
|
|
502
|
+
}
|
|
503
|
+
if (action === "mission.resolve-decision") {
|
|
504
|
+
const decisionId = args.id as string | undefined;
|
|
505
|
+
const resolution = args.message as string | undefined;
|
|
506
|
+
if (!decisionId || !resolution) {
|
|
507
|
+
return textResult("mission.resolve-decision requires id (decision id) and message (resolution).", { status: "error" });
|
|
508
|
+
}
|
|
509
|
+
const record = updateMission(location, missionId, { resolveDecision: { id: decisionId, resolution } });
|
|
510
|
+
return textResult(`Decision ${decisionId} resolved on mission ${record.id}.`, { status: "completed" });
|
|
511
|
+
}
|
|
512
|
+
// mission.update: generic field updates
|
|
513
|
+
const record = updateMission(location, missionId, {
|
|
514
|
+
...(typeof args.summary === "string" ? { summary: args.summary } : {}),
|
|
515
|
+
...(typeof args.missionStatus === "string" ? { status: args.missionStatus as MissionStatus } : {}),
|
|
516
|
+
});
|
|
517
|
+
return textResult(`Mission ${record.id} updated.`, { status: "completed", missionId: record.id });
|
|
518
|
+
} catch (error) {
|
|
519
|
+
if (error instanceof MissionNotFoundError) return textResult(error.message, { status: "error" });
|
|
520
|
+
return textResult(`${action} failed: ${error instanceof Error ? error.message : String(error)}`, { status: "error" });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
case "schedule.create":
|
|
525
|
+
case "schedule.list":
|
|
526
|
+
case "schedule.show":
|
|
527
|
+
case "schedule.history":
|
|
528
|
+
case "schedule.pause":
|
|
529
|
+
case "schedule.resume":
|
|
530
|
+
case "schedule.run":
|
|
531
|
+
case "schedule.run-due":
|
|
532
|
+
case "schedule.delete": {
|
|
533
|
+
if (!deps.runAsync) {
|
|
534
|
+
return textResult("Schedule actions require the background process runner, which is unavailable in this host.", { status: "error" });
|
|
535
|
+
}
|
|
536
|
+
const manager = new ScheduledRunManager(deps.projectRoot ?? process.cwd(), {
|
|
537
|
+
storeRoot: deps.config.scheduledRuns?.storeRoot,
|
|
538
|
+
maxPending: deps.config.scheduledRuns?.maxPending,
|
|
539
|
+
launch: async (record) => {
|
|
540
|
+
const result = await deps.runAsync!({
|
|
541
|
+
agentName: record.agent,
|
|
542
|
+
task: record.task,
|
|
543
|
+
description: `schedule: ${record.name}`,
|
|
544
|
+
context: "fresh",
|
|
545
|
+
timeoutMs: record.timeoutMs,
|
|
546
|
+
});
|
|
547
|
+
return result.runId;
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
switch (action) {
|
|
553
|
+
case "schedule.create": {
|
|
554
|
+
const record = manager.create({
|
|
555
|
+
name: (args.name as string | undefined) ?? `schedule-${Date.now().toString(36)}`,
|
|
556
|
+
agent: (args.agent ?? args.type) as string,
|
|
557
|
+
task: (args.task ?? args.prompt) as string,
|
|
558
|
+
at: args.at as string | undefined,
|
|
559
|
+
every: args.every as string | undefined,
|
|
560
|
+
catchUp: args.catchUp as "none" | "latest" | undefined,
|
|
561
|
+
timeoutMs: args.timeoutMs as number | undefined,
|
|
562
|
+
});
|
|
563
|
+
const next = record.trigger.kind === "once"
|
|
564
|
+
? new Date(record.trigger.atMs).toISOString()
|
|
565
|
+
: record.trigger.nextRunAt;
|
|
566
|
+
return textResult(
|
|
567
|
+
`Schedule created: ${record.id}\nName: ${record.name}\nAgent: ${record.agent}\nTrigger: ${record.trigger.kind === "once" ? `once at ${next}` : `every ${record.trigger.every}`}\nNext run: ${next}`,
|
|
568
|
+
{ status: "completed", scheduleId: record.id },
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
case "schedule.list": {
|
|
572
|
+
const all = manager.list();
|
|
573
|
+
if (all.length === 0) return textResult("No schedules for this project.");
|
|
574
|
+
return textResult(
|
|
575
|
+
[
|
|
576
|
+
`Schedules (${all.length}):`,
|
|
577
|
+
...all.map((r) => {
|
|
578
|
+
const next = r.trigger.kind === "once" ? r.trigger.atMs : r.trigger.nextRunAt;
|
|
579
|
+
return `- ${r.id} "${r.name}" [${r.paused ? "paused" : "active"}] ${r.trigger.kind} → ${r.agent}: ${r.trigger.kind === "once" ? new Date(next).toISOString() : `every ${r.trigger.every}`}`;
|
|
580
|
+
}),
|
|
581
|
+
].join("\n"),
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
case "schedule.show": {
|
|
585
|
+
const id = args.id as string | undefined;
|
|
586
|
+
if (!id) return textResult("schedule.show requires id.", { status: "error" });
|
|
587
|
+
const record = manager.show(id);
|
|
588
|
+
if (!record) return textResult(`No schedule matches "${id}".`, { status: "error" });
|
|
589
|
+
const history = manager.readHistory(id);
|
|
590
|
+
return textResult(
|
|
591
|
+
[
|
|
592
|
+
`Schedule ${record.id} "${record.name}" [${record.paused ? "paused" : "active"}]`,
|
|
593
|
+
`Agent: ${record.agent}`,
|
|
594
|
+
`Task: ${record.task.slice(0, 200)}`,
|
|
595
|
+
`Trigger: ${record.trigger.kind === "once" ? `once at ${record.trigger.at}` : `every ${record.trigger.every}`}`,
|
|
596
|
+
`Overlap: ${record.overlap} | catchUp: ${record.catchUp}`,
|
|
597
|
+
`Runs: ${history.length}`,
|
|
598
|
+
...history.slice(-5).map((r) => `- ${r.id} ${r.state}${r.error ? ` (${r.error})` : ""}`),
|
|
599
|
+
].join("\n"),
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
case "schedule.history": {
|
|
603
|
+
const id = args.id as string | undefined;
|
|
604
|
+
if (!id) return textResult("schedule.history requires id.", { status: "error" });
|
|
605
|
+
const history = manager.readHistory(id);
|
|
606
|
+
if (history.length === 0) return textResult("No runs recorded for this schedule.");
|
|
607
|
+
return textResult(
|
|
608
|
+
["Schedule runs (newest last):", ...history.map((r) => `- ${r.id} ${r.state} ${r.plannedAt}${r.error ? ` (${r.error})` : ""}`)].join("\n"),
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
case "schedule.pause":
|
|
612
|
+
case "schedule.resume": {
|
|
613
|
+
const id = args.id as string | undefined;
|
|
614
|
+
if (!id) return textResult(`${action} requires id.`, { status: "error" });
|
|
615
|
+
const record = manager.setPaused(id, action === "schedule.pause");
|
|
616
|
+
return textResult(`Schedule ${record.id} "${record.name}" ${record.paused ? "paused" : "resumed"}.`);
|
|
617
|
+
}
|
|
618
|
+
case "schedule.run": {
|
|
619
|
+
const id = args.id as string | undefined;
|
|
620
|
+
if (!id) return textResult("schedule.run requires id.", { status: "error" });
|
|
621
|
+
const run = await manager.runNow(id);
|
|
622
|
+
return textResult(
|
|
623
|
+
run.state === "completed"
|
|
624
|
+
? `Schedule ran: async run ${run.asyncRunId}`
|
|
625
|
+
: `Schedule run ${run.state}${run.error ? `: ${run.error}` : ""}`,
|
|
626
|
+
{ status: run.state === "completed" ? "completed" : "error" },
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
case "schedule.run-due": {
|
|
630
|
+
const runs = await manager.runDue();
|
|
631
|
+
return textResult(`${runs.length} due schedule(s) executed.`);
|
|
632
|
+
}
|
|
633
|
+
case "schedule.delete": {
|
|
634
|
+
const id = args.id as string | undefined;
|
|
635
|
+
if (!id) return textResult("schedule.delete requires id.", { status: "error" });
|
|
636
|
+
const removed = manager.delete(id);
|
|
637
|
+
return textResult(removed ? `Schedule ${id} deleted.` : `No schedule matches "${id}".`, { status: removed ? "completed" : "error" });
|
|
638
|
+
}
|
|
639
|
+
default:
|
|
640
|
+
return textResult(`Unknown schedule action.`, { status: "error" });
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
return textResult(`${action} failed: ${error instanceof Error ? error.message : String(error)}`, { status: "error" });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
default:
|
|
648
|
+
// Actions that belong to later phases report their status clearly.
|
|
649
|
+
return textResult(
|
|
650
|
+
`action "${action}" is recognized but not yet implemented in this build (planned phase).`,
|
|
651
|
+
{ status: "error" },
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ============================================================================
|
|
657
|
+
// workflowScript execution (foreground)
|
|
658
|
+
// ============================================================================
|
|
659
|
+
|
|
660
|
+
async function handleWorkflowScript(
|
|
661
|
+
deps: HandlerDeps,
|
|
662
|
+
ctx: ExtensionContext,
|
|
663
|
+
script: string,
|
|
664
|
+
args: Record<string, unknown>,
|
|
665
|
+
signal: AbortSignal | undefined,
|
|
666
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details?: unknown }> {
|
|
667
|
+
const asyncRequested = (args.async as boolean | undefined) ?? deps.config.asyncByDefault ?? true;
|
|
668
|
+
if (asyncRequested) {
|
|
669
|
+
return handleAsyncWorkflowScript(deps, ctx, script, args, signal);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Validate call-level budgets up front (reference behavior).
|
|
673
|
+
if (args.turnBudget !== undefined) {
|
|
674
|
+
const { error } = resolveTurnBudgetConfig(args.turnBudget);
|
|
675
|
+
if (error) return textResult(error, { status: "error" });
|
|
676
|
+
}
|
|
677
|
+
if (args.toolBudget !== undefined) {
|
|
678
|
+
const { error } = validateToolBudgetConfig(args.toolBudget);
|
|
679
|
+
if (error) return textResult(error, { status: "error" });
|
|
680
|
+
}
|
|
681
|
+
if (args.usageBudget !== undefined) {
|
|
682
|
+
const { error } = validateUsageBudgetConfig(args.usageBudget);
|
|
683
|
+
if (error) return textResult(error, { status: "error" });
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Spawn budget: one fanout budget per top-level workflow run.
|
|
687
|
+
const perRunCap = deps.config.maxSubagentSpawnsPerRun ?? 64;
|
|
688
|
+
const budget = createRunFanoutBudget(`workflow-${Date.now()}`, perRunCap);
|
|
689
|
+
|
|
690
|
+
const sessionAccounting = deps.spawnAccounting;
|
|
691
|
+
const sessionCap = sessionAccounting?.cap();
|
|
692
|
+
|
|
693
|
+
const admit = (calls: Array<{ key: string; params: Record<string, unknown> }>): void => {
|
|
694
|
+
if (calls.length === 0) return;
|
|
695
|
+
// Per-run budget: atomic group admission.
|
|
696
|
+
claimRunFanoutBatch(budget, calls.map((call) => call.key));
|
|
697
|
+
// Session budget: cumulative.
|
|
698
|
+
if (sessionCap !== undefined && sessionAccounting) {
|
|
699
|
+
const used = sessionAccounting.used();
|
|
700
|
+
if (used + calls.length > sessionCap) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`Session spawn budget exhausted: ${used}/${sessionCap} used, ${calls.length} requested. ` +
|
|
703
|
+
`Use spawn_helper ({ action: "grant-spawn-budget", additional: N }) from the interactive parent to extend.`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
sessionAccounting.consume(calls.length);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const launch = async (
|
|
711
|
+
key: string,
|
|
712
|
+
params: Record<string, unknown>,
|
|
713
|
+
childSignal: AbortSignal,
|
|
714
|
+
): Promise<WorkflowScriptChildResult> => {
|
|
715
|
+
const agentNameRaw = params.agent as string | undefined;
|
|
716
|
+
const task = params.task as string | undefined;
|
|
717
|
+
if (!agentNameRaw || !task) {
|
|
718
|
+
return { key, ok: false, output: "runs.run requires agent and task.", error: "missing agent/task", artifactPaths: [] };
|
|
719
|
+
}
|
|
720
|
+
const agentName = deps.manager.resolveAlias(agentNameRaw);
|
|
721
|
+
const agent = deps.manager.getAgentConfig(agentName);
|
|
722
|
+
if (!agent) {
|
|
723
|
+
return { key, ok: false, output: `Unknown agent "${agentNameRaw}".`, error: "unknown agent", artifactPaths: [] };
|
|
724
|
+
}
|
|
725
|
+
if (!deps.manager.isTypeEnabled(agentName)) {
|
|
726
|
+
return { key, ok: false, output: `Agent "${agentName}" is disabled.`, error: "disabled", artifactPaths: [] };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Context policy (foreground = fresh only).
|
|
730
|
+
const contextMode = resolveContext(params.context as string | undefined, agent, deps.config);
|
|
731
|
+
if (contextMode === "fork") {
|
|
732
|
+
return {
|
|
733
|
+
key,
|
|
734
|
+
ok: false,
|
|
735
|
+
output: 'context "fork" requires background execution; foreground workflow children are fresh-context.',
|
|
736
|
+
error: "fork unavailable in foreground",
|
|
737
|
+
artifactPaths: [],
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Turn budget: wrap-up block in the child prompt.
|
|
742
|
+
let childTask = task;
|
|
743
|
+
const turnBudgetResult = resolveTurnBudgetConfig(params.turnBudget ?? args.turnBudget);
|
|
744
|
+
if (turnBudgetResult.turnBudget) {
|
|
745
|
+
childTask = appendTurnBudgetSystemPrompt(task, turnBudgetResult.turnBudget);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Boundary instructions (plain or fanout child).
|
|
749
|
+
childTask = withChildBoundaryInstructions(childTask, agent);
|
|
750
|
+
|
|
751
|
+
const result = await deps.spawnForeground(ctx, agentName, childTask, {
|
|
752
|
+
description: (params.label as string | undefined) ?? key,
|
|
753
|
+
maxTurns: (params.maxTurns as number | undefined) ?? (turnBudgetResult.turnBudget?.maxTurns),
|
|
754
|
+
modelInput: (params.model as string | undefined) ?? agent.model,
|
|
755
|
+
thinkingLevel: agent.thinking,
|
|
756
|
+
signal: childSignal,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
key,
|
|
761
|
+
ok: result.ok,
|
|
762
|
+
output: result.output,
|
|
763
|
+
...(result.error ? { error: result.error } : {}),
|
|
764
|
+
...(agentName ? { agent: agentName } : {}),
|
|
765
|
+
artifactPaths: [],
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const status = async (keyOrRunId: string): Promise<WorkflowScriptChildResult> => {
|
|
770
|
+
const record = deps.manager.getRecord(keyOrRunId);
|
|
771
|
+
if (record) {
|
|
772
|
+
return {
|
|
773
|
+
key: keyOrRunId,
|
|
774
|
+
ok: record.status === "completed",
|
|
775
|
+
output: record.result ?? record.status,
|
|
776
|
+
...(record.error ? { error: record.error } : {}),
|
|
777
|
+
artifactPaths: [],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return { key: keyOrRunId, ok: true, output: "ok", artifactPaths: [] };
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
try {
|
|
784
|
+
const result = await runWorkflowScript({
|
|
785
|
+
script,
|
|
786
|
+
timeoutMs: args.timeoutMs as number | undefined,
|
|
787
|
+
signal,
|
|
788
|
+
admit,
|
|
789
|
+
launch,
|
|
790
|
+
status,
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const maxOutput = resolveMaxOutput(args.maxOutput as never, deps.config);
|
|
794
|
+
const summary =
|
|
795
|
+
typeof result.value === "string"
|
|
796
|
+
? result.value
|
|
797
|
+
: JSON.stringify(result.value, null, 2) ?? "null";
|
|
798
|
+
const truncated = truncateOutput(summary, maxOutput);
|
|
799
|
+
return textResult(truncated.text, {
|
|
800
|
+
status: "completed",
|
|
801
|
+
children: result.children.length,
|
|
802
|
+
fanout: formatRunFanoutBudget(getRunFanoutBudgetSnapshot(budget)),
|
|
803
|
+
truncated: truncated.truncated,
|
|
804
|
+
});
|
|
805
|
+
} catch (error) {
|
|
806
|
+
if (error instanceof Error && error.name === "WorkflowScriptError") {
|
|
807
|
+
const partial = (error as { partial?: { children?: unknown[] } }).partial;
|
|
808
|
+
return textResult(
|
|
809
|
+
`Workflow failed: ${error.message}`,
|
|
810
|
+
{ status: "error", children: partial?.children?.length ?? 0 },
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ============================================================================
|
|
818
|
+
// Single-child launch (legacy + reference)
|
|
819
|
+
// ============================================================================
|
|
820
|
+
|
|
821
|
+
async function handleSingleChild(
|
|
822
|
+
deps: HandlerDeps,
|
|
823
|
+
ctx: ExtensionContext,
|
|
824
|
+
args: Record<string, unknown>,
|
|
825
|
+
signal: AbortSignal | undefined,
|
|
826
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details?: unknown }> {
|
|
827
|
+
const pre = preflight(deps, args);
|
|
828
|
+
if (!pre.ok) return textResult(pre.error, { status: "error" });
|
|
829
|
+
|
|
830
|
+
const { agent, agentName, prompt } = pre;
|
|
831
|
+
const asyncRequested =
|
|
832
|
+
(args.async as boolean | undefined) ??
|
|
833
|
+
(args.run_in_background as boolean | undefined) ??
|
|
834
|
+
agent.runInBackground ??
|
|
835
|
+
false;
|
|
836
|
+
|
|
837
|
+
// Context policy. EXPLICIT fork never silently downgrades (reference rule);
|
|
838
|
+
// implicit fork (agent/config default) falls back to fresh when the in-process
|
|
839
|
+
// path cannot fork (reference: implicit fork needs a persisted parent session,
|
|
840
|
+
// else fresh).
|
|
841
|
+
const explicitContext = args.context as string | undefined;
|
|
842
|
+
const contextMode = resolveContext(explicitContext, agent, deps.config);
|
|
843
|
+
if (contextMode === "fork" && (explicitContext === "fork" || deps.config.defaultSubagentContext === "fork")) {
|
|
844
|
+
if (!asyncRequested) {
|
|
845
|
+
return textResult(
|
|
846
|
+
'context: "fork" requires background execution (run_in_background: true); the in-process foreground path is fresh-context only.',
|
|
847
|
+
{ status: "error" },
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (!deps.runAsync) {
|
|
851
|
+
return textResult(
|
|
852
|
+
'context: "fork" requires the background process runner, which is unavailable in this host. Re-run without context, or use context: "fresh".',
|
|
853
|
+
{ status: "error" },
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Budgets: validate + decorate.
|
|
859
|
+
let childPrompt = prompt;
|
|
860
|
+
const turnBudgetResult = resolveTurnBudgetConfig(args.turnBudget);
|
|
861
|
+
if (turnBudgetResult.error) return textResult(turnBudgetResult.error, { status: "error" });
|
|
862
|
+
if (turnBudgetResult.turnBudget) {
|
|
863
|
+
childPrompt = appendTurnBudgetSystemPrompt(childPrompt, turnBudgetResult.turnBudget);
|
|
864
|
+
}
|
|
865
|
+
const toolBudgetResult = validateToolBudgetConfig(args.toolBudget);
|
|
866
|
+
if (toolBudgetResult.error) return textResult(toolBudgetResult.error, { status: "error" });
|
|
867
|
+
const usageBudgetResult = validateUsageBudgetConfig(args.usageBudget);
|
|
868
|
+
if (usageBudgetResult.error) return textResult(usageBudgetResult.error, { status: "error" });
|
|
869
|
+
|
|
870
|
+
// Acceptance gates (gate shorthand + acceptance object).
|
|
871
|
+
const acceptanceResolved = resolveEffectiveAcceptance(args);
|
|
872
|
+
if (!acceptanceResolved.ok) return textResult(acceptanceResolved.error, { status: "error" });
|
|
873
|
+
|
|
874
|
+
// Boundary instructions (unless the agent is a fanout child).
|
|
875
|
+
if (!isFanoutChild(agent)) {
|
|
876
|
+
childPrompt = withChildBoundaryInstructions(childPrompt, agent);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Session spawn budget.
|
|
880
|
+
const sessionAccounting = deps.spawnAccounting;
|
|
881
|
+
if (sessionAccounting) {
|
|
882
|
+
const cap = sessionAccounting.cap();
|
|
883
|
+
if (cap !== undefined && sessionAccounting.used() >= cap) {
|
|
884
|
+
return textResult(
|
|
885
|
+
`Session spawn budget exhausted: ${sessionAccounting.used()}/${cap} used. ` +
|
|
886
|
+
`Use spawn_helper ({ action: "grant-spawn-budget", additional: N }) from the interactive parent to extend.`,
|
|
887
|
+
{ status: "error" },
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
sessionAccounting.consume(1);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const description = (args.description as string | undefined) ?? `${agentName} task`;
|
|
894
|
+
|
|
895
|
+
if (asyncRequested) {
|
|
896
|
+
// Fork context requires the process runner; fresh background runs prefer
|
|
897
|
+
// it too when available (survives parent exit), else fall back in-process.
|
|
898
|
+
const wantsProcessRunner = contextMode === "fork" || deps.config.asyncByDefault === true;
|
|
899
|
+
if (deps.runAsync && (contextMode === "fork" || wantsProcessRunner)) {
|
|
900
|
+
const result = await deps.runAsync({
|
|
901
|
+
agentName,
|
|
902
|
+
task: childPrompt,
|
|
903
|
+
description,
|
|
904
|
+
model: (args.model as string | undefined) ?? agent.model,
|
|
905
|
+
thinking: (args.thinking as string | undefined) ?? (typeof agent.thinking === "string" ? agent.thinking : undefined),
|
|
906
|
+
context: contextMode,
|
|
907
|
+
timeoutMs: (args.timeoutMs as number | undefined) ?? agent.timeoutMs,
|
|
908
|
+
maxTurns: (args.max_turns as number | undefined) ?? (args.maxTurns as number | undefined) ?? turnBudgetResult.turnBudget?.maxTurns,
|
|
909
|
+
});
|
|
910
|
+
return textResult(
|
|
911
|
+
`Agent started in background (process mode).\nRun ID: ${result.runId}\nType: ${agentName}\nDescription: ${description}\n\nYou will be notified when this agent completes.\nUse get_helper_result with the run ID to retrieve results.`,
|
|
912
|
+
{ status: "background", runId: result.runId, agentId: result.runId },
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
const id = deps.spawnBackground(ctx, agentName, childPrompt, {
|
|
916
|
+
description,
|
|
917
|
+
maxTurns: (args.max_turns as number | undefined) ?? (args.maxTurns as number | undefined) ?? turnBudgetResult.turnBudget?.maxTurns,
|
|
918
|
+
modelInput: (args.model as string | undefined) ?? agent.model,
|
|
919
|
+
thinkingLevel: args.thinking ?? agent.thinking,
|
|
920
|
+
});
|
|
921
|
+
return textResult(
|
|
922
|
+
`Agent started in background.\nID: ${id}\nType: ${agentName}\nDescription: ${description}\n\nYou will be notified when this agent completes.\nUse get_helper_result to retrieve full results.`,
|
|
923
|
+
{ status: "background", agentId: id },
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const result = await deps.spawnForeground(ctx, agentName, childPrompt, {
|
|
928
|
+
description,
|
|
929
|
+
maxTurns: (args.max_turns as number | undefined) ?? (args.maxTurns as number | undefined) ?? turnBudgetResult.turnBudget?.maxTurns,
|
|
930
|
+
modelInput: (args.model as string | undefined) ?? agent.model,
|
|
931
|
+
thinkingLevel: args.thinking ?? agent.thinking,
|
|
932
|
+
signal,
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
if (!result.ok) {
|
|
936
|
+
return textResult(`Agent failed: ${result.error ?? result.output}`, {
|
|
937
|
+
status: "error",
|
|
938
|
+
error: result.error,
|
|
939
|
+
toolUses: result.toolUses,
|
|
940
|
+
durationMs: result.durationMs,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Acceptance gate evaluation (host-side verify commands + report checks).
|
|
945
|
+
let finalOutput = result.output;
|
|
946
|
+
let ledgerDetails: unknown;
|
|
947
|
+
if (acceptanceResolved.acceptance) {
|
|
948
|
+
const evaluated = await evaluateRunAcceptance(acceptanceResolved.acceptance, result.output, process.cwd());
|
|
949
|
+
finalOutput = evaluated.output;
|
|
950
|
+
ledgerDetails = evaluated.ledger;
|
|
951
|
+
if (evaluated.ledger?.status === "rejected") {
|
|
952
|
+
return textResult(
|
|
953
|
+
`Agent completed but acceptance rejected:\n${evaluated.ledger.failureMessage}\n\nOutput:\n${finalOutput.slice(0, 2000)}`,
|
|
954
|
+
{ status: "error", acceptance: evaluated.ledger.status, failureMessage: evaluated.ledger.failureMessage },
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Output truncation with maxOutput config (reference default 200KB/5000 lines).
|
|
960
|
+
const maxOutput = resolveMaxOutput(args.maxOutput as never, deps.config);
|
|
961
|
+
const truncated = truncateOutput(finalOutput, maxOutput);
|
|
962
|
+
|
|
963
|
+
return textResult(
|
|
964
|
+
`Agent completed in ${(result.durationMs / 1000).toFixed(1)}s (${result.toolUses} tool uses).\n\n${truncated.text}`,
|
|
965
|
+
{
|
|
966
|
+
status: "completed",
|
|
967
|
+
toolUses: result.toolUses,
|
|
968
|
+
durationMs: result.durationMs,
|
|
969
|
+
...(ledgerDetails ? { acceptance: (ledgerDetails as { status: string }).status } : {}),
|
|
970
|
+
truncated: truncated.truncated,
|
|
971
|
+
originalBytes: truncated.originalBytes,
|
|
972
|
+
},
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
// ============================================================================
|
|
978
|
+
// Async workflowScript (process-backed children)
|
|
979
|
+
// ============================================================================
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Run a workflow with every child launched through the process runner
|
|
983
|
+
* (child pi processes). The workflow runtime is identical; only the launch
|
|
984
|
+
* transport differs. The tool call blocks until the workflow settles — the
|
|
985
|
+
* parent gets the full result. (Fully detached async workflows with live
|
|
986
|
+
* status polling land with the Phase 4 fleet work.)
|
|
987
|
+
*/
|
|
988
|
+
async function handleAsyncWorkflowScript(
|
|
989
|
+
deps: HandlerDeps,
|
|
990
|
+
ctx: ExtensionContext,
|
|
991
|
+
script: string,
|
|
992
|
+
args: Record<string, unknown>,
|
|
993
|
+
signal: AbortSignal | undefined,
|
|
994
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details?: unknown }> {
|
|
995
|
+
if (!deps.runAsync) {
|
|
996
|
+
return textResult(
|
|
997
|
+
"Async workflowScript requires the background process runner, which is unavailable in this host. " +
|
|
998
|
+
"Re-run with async: false to execute the workflow in-process in the foreground.",
|
|
999
|
+
{ status: "error" },
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Validate call-level budgets up front.
|
|
1004
|
+
if (args.turnBudget !== undefined) {
|
|
1005
|
+
const { error } = resolveTurnBudgetConfig(args.turnBudget);
|
|
1006
|
+
if (error) return textResult(error, { status: "error" });
|
|
1007
|
+
}
|
|
1008
|
+
if (args.toolBudget !== undefined) {
|
|
1009
|
+
const { error } = validateToolBudgetConfig(args.toolBudget);
|
|
1010
|
+
if (error) return textResult(error, { status: "error" });
|
|
1011
|
+
}
|
|
1012
|
+
if (args.usageBudget !== undefined) {
|
|
1013
|
+
const { error } = validateUsageBudgetConfig(args.usageBudget);
|
|
1014
|
+
if (error) return textResult(error, { status: "error" });
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const perRunCap = deps.config.maxSubagentSpawnsPerRun ?? 64;
|
|
1018
|
+
const budget = createRunFanoutBudget(`workflow-async-${Date.now()}`, perRunCap);
|
|
1019
|
+
const sessionAccounting = deps.spawnAccounting;
|
|
1020
|
+
const sessionCap = sessionAccounting?.cap();
|
|
1021
|
+
|
|
1022
|
+
const admit = (calls: Array<{ key: string; params: Record<string, unknown> }>): void => {
|
|
1023
|
+
if (calls.length === 0) return;
|
|
1024
|
+
claimRunFanoutBatch(budget, calls.map((call) => call.key));
|
|
1025
|
+
if (sessionCap !== undefined && sessionAccounting) {
|
|
1026
|
+
const used = sessionAccounting.used();
|
|
1027
|
+
if (used + calls.length > sessionCap) {
|
|
1028
|
+
throw new Error(
|
|
1029
|
+
`Session spawn budget exhausted: ${used}/${sessionCap} used, ${calls.length} requested.`,
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
sessionAccounting.consume(calls.length);
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
const launch = async (
|
|
1037
|
+
key: string,
|
|
1038
|
+
params: Record<string, unknown>,
|
|
1039
|
+
childSignal: AbortSignal,
|
|
1040
|
+
): Promise<WorkflowScriptChildResult> => {
|
|
1041
|
+
const agentNameRaw = params.agent as string | undefined;
|
|
1042
|
+
const task = params.task as string | undefined;
|
|
1043
|
+
if (!agentNameRaw || !task) {
|
|
1044
|
+
return { key, ok: false, output: "runs.run requires agent and task.", error: "missing agent/task", artifactPaths: [] };
|
|
1045
|
+
}
|
|
1046
|
+
const agentName = deps.manager.resolveAlias(agentNameRaw);
|
|
1047
|
+
const agent = deps.manager.getAgentConfig(agentName);
|
|
1048
|
+
if (!agent) {
|
|
1049
|
+
return { key, ok: false, output: `Unknown agent "${agentNameRaw}".`, error: "unknown agent", artifactPaths: [] };
|
|
1050
|
+
}
|
|
1051
|
+
if (!deps.manager.isTypeEnabled(agentName)) {
|
|
1052
|
+
return { key, ok: false, output: `Agent "${agentName}" is disabled.`, error: "disabled", artifactPaths: [] };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Context policy: fork children get branched sessions via the process runner.
|
|
1056
|
+
const contextMode = resolveContext(params.context as string | undefined, agent, deps.config);
|
|
1057
|
+
|
|
1058
|
+
let childTask = task;
|
|
1059
|
+
const turnBudgetResult = resolveTurnBudgetConfig(params.turnBudget ?? args.turnBudget);
|
|
1060
|
+
if (turnBudgetResult.turnBudget) {
|
|
1061
|
+
childTask = appendTurnBudgetSystemPrompt(task, turnBudgetResult.turnBudget);
|
|
1062
|
+
}
|
|
1063
|
+
childTask = withChildBoundaryInstructions(childTask, agent);
|
|
1064
|
+
|
|
1065
|
+
try {
|
|
1066
|
+
const worktreeRequested =
|
|
1067
|
+
(params.worktree as boolean | undefined) ??
|
|
1068
|
+
(args.worktree as boolean | undefined) ??
|
|
1069
|
+
(args.isolation === "worktree" ? true : undefined);
|
|
1070
|
+
const result = await deps.runAsync!({
|
|
1071
|
+
agentName,
|
|
1072
|
+
task: childTask,
|
|
1073
|
+
description: (params.label as string | undefined) ?? key,
|
|
1074
|
+
model: (params.model as string | undefined) ?? agent.model,
|
|
1075
|
+
thinking: typeof agent.thinking === "string" ? agent.thinking : undefined,
|
|
1076
|
+
context: contextMode,
|
|
1077
|
+
timeoutMs: (params.timeoutMs as number | undefined) ?? agent.timeoutMs,
|
|
1078
|
+
maxTurns: (params.maxTurns as number | undefined) ?? turnBudgetResult.turnBudget?.maxTurns,
|
|
1079
|
+
...(worktreeRequested === true ? { worktree: true } : {}),
|
|
1080
|
+
});
|
|
1081
|
+
void childSignal; // process-level abort handled by the runner's controller
|
|
1082
|
+
return {
|
|
1083
|
+
key,
|
|
1084
|
+
ok: result.status === "completed",
|
|
1085
|
+
output: result.output ?? result.error ?? result.status,
|
|
1086
|
+
...(result.error ? { error: result.error } : {}),
|
|
1087
|
+
agent: agentName,
|
|
1088
|
+
...(result.runId ? { runId: result.runId } : {}),
|
|
1089
|
+
artifactPaths: [],
|
|
1090
|
+
};
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1093
|
+
return { key, ok: false, output: text, error: text, artifactPaths: [] };
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
const status = async (keyOrRunId: string): Promise<WorkflowScriptChildResult> => {
|
|
1098
|
+
const record = deps.manager.getRecord(keyOrRunId);
|
|
1099
|
+
if (record) {
|
|
1100
|
+
return {
|
|
1101
|
+
key: keyOrRunId,
|
|
1102
|
+
ok: record.status === "completed",
|
|
1103
|
+
output: record.result ?? record.status,
|
|
1104
|
+
...(record.error ? { error: record.error } : {}),
|
|
1105
|
+
artifactPaths: [],
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
return { key: keyOrRunId, ok: true, output: "ok", artifactPaths: [] };
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
try {
|
|
1112
|
+
const result = await runWorkflowScript({
|
|
1113
|
+
script,
|
|
1114
|
+
timeoutMs: args.timeoutMs as number | undefined,
|
|
1115
|
+
signal,
|
|
1116
|
+
admit,
|
|
1117
|
+
launch,
|
|
1118
|
+
status,
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
const maxOutput = resolveMaxOutput(args.maxOutput as never, deps.config);
|
|
1122
|
+
const summary =
|
|
1123
|
+
typeof result.value === "string"
|
|
1124
|
+
? result.value
|
|
1125
|
+
: JSON.stringify(result.value, null, 2) ?? "null";
|
|
1126
|
+
const truncated = truncateOutput(summary, maxOutput);
|
|
1127
|
+
return textResult(truncated.text, {
|
|
1128
|
+
status: "completed",
|
|
1129
|
+
mode: "async-workflow",
|
|
1130
|
+
children: result.children.length,
|
|
1131
|
+
fanout: formatRunFanoutBudget(getRunFanoutBudgetSnapshot(budget)),
|
|
1132
|
+
truncated: truncated.truncated,
|
|
1133
|
+
});
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
if (error instanceof Error && error.name === "WorkflowScriptError") {
|
|
1136
|
+
const partial = (error as { partial?: { children?: unknown[] } }).partial;
|
|
1137
|
+
return textResult(
|
|
1138
|
+
`Workflow failed: ${error.message}`,
|
|
1139
|
+
{ status: "error", mode: "async-workflow", children: partial?.children?.length ?? 0 },
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
throw error;
|
|
1143
|
+
}
|
|
1144
|
+
}
|