pi-subagents 0.29.0 → 0.31.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 +43 -0
- package/README.md +125 -19
- package/agents/context-builder.md +3 -3
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/scout.md +1 -1
- package/package.json +7 -7
- package/skills/pi-subagents/SKILL.md +30 -0
- package/src/agents/agent-management.ts +189 -8
- package/src/agents/agent-serializer.ts +35 -12
- package/src/agents/agents.ts +243 -24
- package/src/agents/frontmatter.ts +66 -2
- package/src/agents/proactive-skills.ts +191 -0
- package/src/agents/skills.ts +117 -20
- package/src/extension/doctor.ts +20 -0
- package/src/extension/fanout-child.ts +2 -1
- package/src/extension/index.ts +50 -5
- package/src/extension/schemas.ts +40 -79
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/runs/background/async-execution.ts +180 -67
- package/src/runs/background/async-job-tracker.ts +56 -11
- package/src/runs/background/async-resume.ts +53 -5
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/chain-append.ts +282 -0
- package/src/runs/background/chain-root-attachment.ts +161 -0
- package/src/runs/background/result-watcher.ts +11 -2
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/stale-run-reconciler.ts +9 -4
- package/src/runs/background/subagent-runner.ts +158 -11
- package/src/runs/foreground/chain-execution.ts +26 -2
- package/src/runs/foreground/execution.ts +114 -8
- package/src/runs/foreground/subagent-executor.ts +611 -87
- package/src/runs/shared/acceptance.ts +285 -34
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +5 -3
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +13 -1
- package/src/runs/shared/pi-args.ts +12 -3
- package/src/runs/shared/single-output.ts +15 -1
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +17 -2
- package/src/shared/utils.ts +19 -1
- package/src/slash/prompt-template-bridge.ts +26 -3
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +34 -4
- package/src/tui/render.ts +265 -13
package/src/extension/schemas.ts
CHANGED
|
@@ -5,13 +5,38 @@
|
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { SUBAGENT_ACTIONS } from "../shared/types.ts";
|
|
7
7
|
|
|
8
|
+
function keepTopLevelParameterDescriptions<T>(schema: T): T {
|
|
9
|
+
return pruneNestedDescriptions(schema, []) as T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pruneNestedDescriptions(value: unknown, path: string[]): unknown {
|
|
13
|
+
if (!value || typeof value !== "object") return value;
|
|
14
|
+
|
|
15
|
+
const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
|
16
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
17
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
18
|
+
if (!descriptor) continue;
|
|
19
|
+
if (key === "description" && !isTopLevelParameterDescription(path)) continue;
|
|
20
|
+
if ("value" in descriptor) {
|
|
21
|
+
const nextPath = typeof key === "string" ? [...path, key] : path;
|
|
22
|
+
descriptor.value = pruneNestedDescriptions(descriptor.value, nextPath);
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(result, key, descriptor);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isTopLevelParameterDescription(path: string[]): boolean {
|
|
30
|
+
return path.length === 2 && path[0] === "properties";
|
|
31
|
+
}
|
|
32
|
+
|
|
8
33
|
const SkillOverride = Type.Unsafe({
|
|
9
34
|
anyOf: [
|
|
10
35
|
{ type: "array", items: { type: "string" } },
|
|
11
36
|
{ type: "boolean" },
|
|
12
37
|
{ type: "string" },
|
|
13
38
|
],
|
|
14
|
-
description: "Skill name(s) to
|
|
39
|
+
description: "Skill name(s) to make available (comma-separated), array of strings, or boolean (false disables, true uses default)",
|
|
15
40
|
});
|
|
16
41
|
|
|
17
42
|
const OutputOverride = Type.Unsafe({
|
|
@@ -41,72 +66,11 @@ const JsonSchemaObject = Type.Unsafe({
|
|
|
41
66
|
description: "JSON Schema object for strict structured output. Non-object roots are rejected.",
|
|
42
67
|
});
|
|
43
68
|
|
|
44
|
-
const AcceptanceEvidenceKind = Type.String({
|
|
45
|
-
enum: [
|
|
46
|
-
"changed-files",
|
|
47
|
-
"tests-added",
|
|
48
|
-
"commands-run",
|
|
49
|
-
"validation-output",
|
|
50
|
-
"residual-risks",
|
|
51
|
-
"no-staged-files",
|
|
52
|
-
"diff-summary",
|
|
53
|
-
"review-findings",
|
|
54
|
-
"manual-notes",
|
|
55
|
-
],
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const AcceptanceGateSchema = Type.Object({
|
|
59
|
-
id: Type.String(),
|
|
60
|
-
must: Type.String(),
|
|
61
|
-
evidence: Type.Optional(Type.Array(AcceptanceEvidenceKind)),
|
|
62
|
-
severity: Type.Optional(Type.String({ enum: ["required", "recommended"] })),
|
|
63
|
-
}, { additionalProperties: false });
|
|
64
|
-
|
|
65
|
-
const AcceptanceVerifyCommandSchema = Type.Object({
|
|
66
|
-
id: Type.String(),
|
|
67
|
-
command: Type.String(),
|
|
68
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
69
|
-
cwd: Type.Optional(Type.String()),
|
|
70
|
-
env: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: { type: "string" } })),
|
|
71
|
-
allowFailure: Type.Optional(Type.Boolean()),
|
|
72
|
-
}, { additionalProperties: false });
|
|
73
|
-
|
|
74
|
-
const AcceptanceReviewGateSchema = Type.Object({
|
|
75
|
-
agent: Type.Optional(Type.String()),
|
|
76
|
-
focus: Type.Optional(Type.String()),
|
|
77
|
-
required: Type.Optional(Type.Boolean()),
|
|
78
|
-
}, { additionalProperties: false });
|
|
79
|
-
|
|
80
69
|
const AcceptanceOverride = Type.Unsafe({
|
|
81
70
|
anyOf: [
|
|
82
71
|
{ type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
|
|
83
|
-
{
|
|
84
|
-
{
|
|
85
|
-
type: "object",
|
|
86
|
-
properties: {
|
|
87
|
-
level: { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
|
|
88
|
-
criteria: {
|
|
89
|
-
type: "array",
|
|
90
|
-
items: {
|
|
91
|
-
anyOf: [
|
|
92
|
-
{ type: "string" },
|
|
93
|
-
AcceptanceGateSchema,
|
|
94
|
-
],
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
evidence: { type: "array", items: AcceptanceEvidenceKind },
|
|
98
|
-
verify: { type: "array", items: AcceptanceVerifyCommandSchema },
|
|
99
|
-
review: {
|
|
100
|
-
anyOf: [
|
|
101
|
-
{ const: false },
|
|
102
|
-
AcceptanceReviewGateSchema,
|
|
103
|
-
],
|
|
104
|
-
},
|
|
105
|
-
stopRules: { type: "array", items: { type: "string" } },
|
|
106
|
-
reason: { type: "string" },
|
|
107
|
-
},
|
|
108
|
-
additionalProperties: false,
|
|
109
|
-
},
|
|
72
|
+
{ type: "boolean", enum: [false] },
|
|
73
|
+
{ type: "object", additionalProperties: true },
|
|
110
74
|
],
|
|
111
75
|
description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
|
|
112
76
|
});
|
|
@@ -211,11 +175,6 @@ const ChainItem = Type.Object({
|
|
|
211
175
|
}, {
|
|
212
176
|
description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
|
|
213
177
|
additionalProperties: false,
|
|
214
|
-
allOf: [
|
|
215
|
-
{ if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
|
|
216
|
-
{ if: { required: ["collect"] }, then: { required: ["expand", "parallel"], properties: { parallel: { type: "object" } } } },
|
|
217
|
-
{ not: { required: ["expand"], properties: { parallel: { type: "array", items: {} } } } },
|
|
218
|
-
],
|
|
219
178
|
});
|
|
220
179
|
|
|
221
180
|
const ControlOverrides = Type.Object({
|
|
@@ -233,7 +192,7 @@ const ControlOverrides = Type.Object({
|
|
|
233
192
|
})),
|
|
234
193
|
});
|
|
235
194
|
|
|
236
|
-
|
|
195
|
+
const SubagentParamsSchema = Type.Object({
|
|
237
196
|
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
|
|
238
197
|
task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
|
|
239
198
|
// Management action (when present, tool operates in management mode)
|
|
@@ -242,10 +201,10 @@ export const SubagentParams = Type.Object({
|
|
|
242
201
|
description: "Management/control action. Omit for execution mode."
|
|
243
202
|
})),
|
|
244
203
|
id: Type.Optional(Type.String({
|
|
245
|
-
description: "Run id or prefix for action='status', action='interrupt', or action='
|
|
204
|
+
description: "Run id or prefix for action='status', action='interrupt', action='resume', or action='append-step'."
|
|
246
205
|
})),
|
|
247
206
|
runId: Type.Optional(Type.String({
|
|
248
|
-
description: "Target run ID for action='interrupt' or action='
|
|
207
|
+
description: "Target run ID for action='interrupt', action='resume', or action='append-step'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
|
|
249
208
|
})),
|
|
250
209
|
dir: Type.Optional(Type.String({
|
|
251
210
|
description: "Async run directory for action='status' or action='resume'."
|
|
@@ -262,22 +221,22 @@ export const SubagentParams = Type.Object({
|
|
|
262
221
|
{ type: "object", additionalProperties: true },
|
|
263
222
|
{ type: "string" },
|
|
264
223
|
],
|
|
265
|
-
description: "Agent
|
|
224
|
+
description: "Agent/chain config for create/update. Object or JSON string; presence of steps creates a chain."
|
|
266
225
|
})),
|
|
267
226
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, outputMode?, reads?, progress?}, ...]" })),
|
|
268
227
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
|
|
269
228
|
worktree: Type.Optional(Type.Boolean({
|
|
270
|
-
description: "Create isolated git worktrees for
|
|
271
|
-
"Prevents filesystem conflicts. Requires clean git state. " +
|
|
272
|
-
"Per-worktree diffs included in output."
|
|
229
|
+
description: "Create isolated git worktrees for parallel tasks; requires clean git state."
|
|
273
230
|
})),
|
|
274
|
-
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential
|
|
231
|
+
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential steps; each result becomes {previous}. append-step takes one tail step and may use {chain_dir}/{outputs.name}." })),
|
|
275
232
|
context: Type.Optional(Type.String({
|
|
276
233
|
enum: ["fresh", "fork"],
|
|
277
|
-
description: "'fresh' or 'fork' to branch from parent session. If omitted,
|
|
234
|
+
description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.",
|
|
278
235
|
})),
|
|
279
|
-
chainDir: Type.Optional(Type.String({ description: "Persistent
|
|
236
|
+
chainDir: Type.Optional(Type.String({ description: "Persistent chain artifact directory; defaults to user-scoped temp storage." })),
|
|
280
237
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
238
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Foreground timeout ms; alias of maxRuntimeMs." })),
|
|
239
|
+
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias of timeoutMs for foreground timeout." })),
|
|
281
240
|
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
|
|
282
241
|
cwd: Type.Optional(Type.String()),
|
|
283
242
|
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
|
|
@@ -302,3 +261,5 @@ export const SubagentParams = Type.Object({
|
|
|
302
261
|
model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
|
|
303
262
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
304
263
|
});
|
|
264
|
+
|
|
265
|
+
export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
|
|
@@ -4,10 +4,9 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { AgentConfig } from "../agents/agents.ts";
|
|
6
6
|
import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
|
|
7
|
-
import { getAgentDir } from "../shared/utils.ts";
|
|
7
|
+
import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
8
8
|
|
|
9
9
|
const PI_INTERCOM_PACKAGE_NAME = "pi-intercom";
|
|
10
|
-
const CONFIG_DIR = ".pi";
|
|
11
10
|
|
|
12
11
|
function defaultAgentDir(): string {
|
|
13
12
|
return getAgentDir();
|
|
@@ -179,7 +178,7 @@ function packageEntryAllowsExtensions(entry: unknown): boolean {
|
|
|
179
178
|
function findNearestProjectConfigDir(cwd: string): string | undefined {
|
|
180
179
|
let current = path.resolve(cwd);
|
|
181
180
|
while (true) {
|
|
182
|
-
const configDir =
|
|
181
|
+
const configDir = getProjectConfigDir(current);
|
|
183
182
|
if (fs.existsSync(path.join(configDir, "settings.json"))) return configDir;
|
|
184
183
|
const parent = path.dirname(current);
|
|
185
184
|
if (parent === current) return undefined;
|
|
@@ -11,7 +11,7 @@ import { createRequire } from "node:module";
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
13
13
|
import { applyThinkingSuffix } from "../shared/pi-args.ts";
|
|
14
|
-
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
14
|
+
import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
15
15
|
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
16
16
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
17
17
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
resolveChildMaxSubagentDepth,
|
|
41
41
|
} from "../../shared/types.ts";
|
|
42
42
|
import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
|
|
43
|
+
import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
43
44
|
|
|
44
45
|
const require = createRequire(import.meta.url);
|
|
45
46
|
const piPackageRoot = resolvePiPackageRoot();
|
|
@@ -94,6 +95,8 @@ interface AsyncExecutionContext {
|
|
|
94
95
|
pi: ExtensionAPI;
|
|
95
96
|
cwd: string;
|
|
96
97
|
currentSessionId: string;
|
|
98
|
+
/** Parent session id used by permission-system ask forwarding. */
|
|
99
|
+
parentSessionId?: string;
|
|
97
100
|
currentModelProvider?: string;
|
|
98
101
|
currentModel?: ParentModel;
|
|
99
102
|
}
|
|
@@ -101,6 +104,7 @@ interface AsyncExecutionContext {
|
|
|
101
104
|
interface AsyncChainParams {
|
|
102
105
|
chain: ChainStep[];
|
|
103
106
|
task?: string;
|
|
107
|
+
attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
|
|
104
108
|
resultMode?: Exclude<SubagentRunMode, "single">;
|
|
105
109
|
agents: AgentConfig[];
|
|
106
110
|
ctx: AsyncExecutionContext;
|
|
@@ -113,6 +117,7 @@ interface AsyncChainParams {
|
|
|
113
117
|
sessionRoot?: string;
|
|
114
118
|
chainSkills?: string[];
|
|
115
119
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
120
|
+
progressDir?: string;
|
|
116
121
|
dynamicFanoutMaxItems?: number;
|
|
117
122
|
maxSubagentDepth: number;
|
|
118
123
|
worktreeSetupHook?: string;
|
|
@@ -157,6 +162,34 @@ interface AsyncExecutionResult {
|
|
|
157
162
|
isError?: boolean;
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
export interface AsyncRunnerStepBuildParams {
|
|
166
|
+
chain: ChainStep[];
|
|
167
|
+
task?: string;
|
|
168
|
+
attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
|
|
169
|
+
resultMode?: SubagentRunMode;
|
|
170
|
+
agents: AgentConfig[];
|
|
171
|
+
ctx: AsyncExecutionContext;
|
|
172
|
+
availableModels?: AvailableModelInfo[];
|
|
173
|
+
cwd?: string;
|
|
174
|
+
chainSkills?: string[];
|
|
175
|
+
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
176
|
+
progressDir?: string;
|
|
177
|
+
dynamicFanoutMaxItems?: number;
|
|
178
|
+
maxSubagentDepth: number;
|
|
179
|
+
asyncDir: string;
|
|
180
|
+
validateOutputBindings?: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type AsyncRunnerStepBuildResult =
|
|
184
|
+
| {
|
|
185
|
+
steps: RunnerStep[];
|
|
186
|
+
runnerCwd: string;
|
|
187
|
+
workflowGraph: ReturnType<typeof buildWorkflowGraphSnapshot>;
|
|
188
|
+
eventChain: ChainStep[];
|
|
189
|
+
originalTask?: string;
|
|
190
|
+
}
|
|
191
|
+
| { error: string };
|
|
192
|
+
|
|
160
193
|
export function formatAsyncStartedMessage(headline: string): string {
|
|
161
194
|
return [
|
|
162
195
|
headline,
|
|
@@ -174,6 +207,14 @@ export function isAsyncAvailable(): boolean {
|
|
|
174
207
|
return jitiCliPath !== undefined;
|
|
175
208
|
}
|
|
176
209
|
|
|
210
|
+
function resolveAsyncRunnerNodeCommand(): string {
|
|
211
|
+
const basename = path.basename(process.execPath).toLowerCase();
|
|
212
|
+
if (basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe") {
|
|
213
|
+
return process.execPath;
|
|
214
|
+
}
|
|
215
|
+
return process.platform === "win32" ? "node.exe" : "node";
|
|
216
|
+
}
|
|
217
|
+
|
|
177
218
|
/**
|
|
178
219
|
* Spawn the async runner process
|
|
179
220
|
*/
|
|
@@ -195,8 +236,9 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
|
|
|
195
236
|
const cfgPath = getAsyncConfigPath(suffix);
|
|
196
237
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg));
|
|
197
238
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
239
|
+
const nodeCommand = resolveAsyncRunnerNodeCommand();
|
|
198
240
|
|
|
199
|
-
const proc = spawn(
|
|
241
|
+
const proc = spawn(nodeCommand, [jitiCliPath, runner, cfgPath], {
|
|
200
242
|
cwd,
|
|
201
243
|
detached: true,
|
|
202
244
|
stdio: "ignore",
|
|
@@ -225,36 +267,29 @@ const UNAVAILABLE_SUBAGENT_SKILL_ERROR = "Skills not found: pi-subagents";
|
|
|
225
267
|
class UnavailableSubagentSkillError extends Error {}
|
|
226
268
|
class AsyncStartValidationError extends Error {}
|
|
227
269
|
|
|
228
|
-
|
|
229
|
-
* Execute a chain asynchronously
|
|
230
|
-
*/
|
|
231
|
-
export function executeAsyncChain(
|
|
232
|
-
id: string,
|
|
233
|
-
params: AsyncChainParams,
|
|
234
|
-
): AsyncExecutionResult {
|
|
270
|
+
export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildParams): AsyncRunnerStepBuildResult {
|
|
235
271
|
const {
|
|
236
272
|
chain,
|
|
237
273
|
agents,
|
|
238
274
|
ctx,
|
|
239
275
|
cwd,
|
|
240
|
-
maxOutput,
|
|
241
|
-
artifactsDir,
|
|
242
|
-
artifactConfig,
|
|
243
|
-
shareEnabled,
|
|
244
|
-
sessionRoot,
|
|
245
276
|
sessionFilesByFlatIndex,
|
|
246
277
|
maxSubagentDepth,
|
|
247
|
-
|
|
248
|
-
worktreeSetupHookTimeoutMs,
|
|
249
|
-
controlConfig,
|
|
250
|
-
controlIntercomTarget,
|
|
251
|
-
childIntercomTarget,
|
|
252
|
-
nestedRoute,
|
|
278
|
+
asyncDir,
|
|
253
279
|
} = params;
|
|
254
280
|
const resultMode = params.resultMode ?? "chain";
|
|
255
281
|
const chainSkills = params.chainSkills ?? [];
|
|
256
282
|
const availableModels = params.availableModels;
|
|
257
283
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
284
|
+
const progressDir = params.progressDir ?? runnerCwd;
|
|
285
|
+
const graphChain: ChainStep[] = params.attachRoot
|
|
286
|
+
? [{
|
|
287
|
+
agent: params.attachRoot.agent,
|
|
288
|
+
task: `Attach async root ${params.attachRoot.runId}`,
|
|
289
|
+
label: params.attachRoot.label ?? `Attached root ${params.attachRoot.runId}`,
|
|
290
|
+
...(params.attachRoot.outputName ? { as: params.attachRoot.outputName } : {}),
|
|
291
|
+
}, ...chain]
|
|
292
|
+
: chain;
|
|
258
293
|
const firstStep = chain[0];
|
|
259
294
|
const originalTask = params.task ?? (firstStep
|
|
260
295
|
? (isParallelStep(firstStep)
|
|
@@ -264,46 +299,28 @@ export function executeAsyncChain(
|
|
|
264
299
|
: (firstStep as SequentialStep).task)
|
|
265
300
|
: undefined);
|
|
266
301
|
try {
|
|
267
|
-
|
|
302
|
+
if (params.validateOutputBindings !== false) {
|
|
303
|
+
validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
|
|
304
|
+
}
|
|
268
305
|
} catch (error) {
|
|
269
|
-
if (error instanceof ChainOutputValidationError) return
|
|
306
|
+
if (error instanceof ChainOutputValidationError) return { error: error.message };
|
|
270
307
|
throw error;
|
|
271
308
|
}
|
|
272
|
-
const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps:
|
|
309
|
+
const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
|
|
273
310
|
|
|
274
311
|
for (const s of chain) {
|
|
275
312
|
const stepAgents = isParallelStep(s)
|
|
276
313
|
? s.parallel.map((t) => t.agent)
|
|
277
314
|
: isDynamicParallelStep(s)
|
|
278
315
|
? [s.parallel.agent]
|
|
279
|
-
|
|
316
|
+
: [(s as SequentialStep).agent];
|
|
280
317
|
for (const agentName of stepAgents) {
|
|
281
318
|
if (!agents.find((x) => x.name === agentName)) {
|
|
282
|
-
return {
|
|
283
|
-
content: [{ type: "text", text: `Unknown agent: ${agentName}` }],
|
|
284
|
-
isError: true,
|
|
285
|
-
details: { mode: resultMode, results: [] },
|
|
286
|
-
};
|
|
319
|
+
return { error: `Unknown agent: ${agentName}` };
|
|
287
320
|
}
|
|
288
321
|
}
|
|
289
322
|
}
|
|
290
323
|
|
|
291
|
-
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
292
|
-
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
293
|
-
const asyncDir = inheritedNestedRoute
|
|
294
|
-
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
295
|
-
: path.join(ASYNC_DIR, id);
|
|
296
|
-
try {
|
|
297
|
-
fs.mkdirSync(asyncDir, { recursive: true });
|
|
298
|
-
} catch (error) {
|
|
299
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
300
|
-
return {
|
|
301
|
-
content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
|
|
302
|
-
isError: true,
|
|
303
|
-
details: { mode: resultMode, results: [] },
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
324
|
let progressInstructionCreated = false;
|
|
308
325
|
const buildStepOverrides = (s: SequentialStep): StepOverrides => {
|
|
309
326
|
const stepSkillInput = normalizeSkillInput(s.skill);
|
|
@@ -334,8 +351,9 @@ export function executeAsyncChain(
|
|
|
334
351
|
const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false);
|
|
335
352
|
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
336
353
|
if (behavior.progress) progressInstructionCreated = true;
|
|
337
|
-
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false },
|
|
354
|
+
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
|
|
338
355
|
const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
|
|
356
|
+
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath);
|
|
339
357
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
|
|
340
358
|
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
341
359
|
let taskTemplate = s.task ?? "{previous}";
|
|
@@ -347,6 +365,7 @@ export function executeAsyncChain(
|
|
|
347
365
|
const primaryModel = resolveSubagentModelOverride(requestedModel, ctx.currentModel, availableModels, ctx.currentModelProvider);
|
|
348
366
|
const model = applyThinkingSuffix(primaryModel, a.thinking);
|
|
349
367
|
return {
|
|
368
|
+
parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
|
|
350
369
|
agent: s.agent,
|
|
351
370
|
task,
|
|
352
371
|
phase: s.phase,
|
|
@@ -361,6 +380,7 @@ export function executeAsyncChain(
|
|
|
361
380
|
),
|
|
362
381
|
tools: a.tools,
|
|
363
382
|
extensions: a.extensions,
|
|
383
|
+
subagentOnlyExtensions: a.subagentOnlyExtensions,
|
|
364
384
|
mcpDirectTools: a.mcpDirectTools,
|
|
365
385
|
completionGuard: a.completionGuard,
|
|
366
386
|
systemPrompt,
|
|
@@ -392,9 +412,8 @@ export function executeAsyncChain(
|
|
|
392
412
|
return sessionFile;
|
|
393
413
|
};
|
|
394
414
|
|
|
395
|
-
let steps: RunnerStep[];
|
|
396
415
|
try {
|
|
397
|
-
|
|
416
|
+
const builtSteps = chain.map((s, stepIndex) => {
|
|
398
417
|
if (isParallelStep(s)) {
|
|
399
418
|
const parallelBehaviors = s.parallel.map((task) => {
|
|
400
419
|
const agent = agents.find((candidate) => candidate.name === task.agent)!;
|
|
@@ -402,7 +421,7 @@ export function executeAsyncChain(
|
|
|
402
421
|
});
|
|
403
422
|
const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
|
|
404
423
|
if (progressPrecreated) {
|
|
405
|
-
if (!s.worktree) writeInitialProgressFile(
|
|
424
|
+
if (!s.worktree || params.progressDir) writeInitialProgressFile(progressDir);
|
|
406
425
|
progressInstructionCreated = true;
|
|
407
426
|
}
|
|
408
427
|
return {
|
|
@@ -427,7 +446,7 @@ export function executeAsyncChain(
|
|
|
427
446
|
const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agent, buildStepOverrides(s.parallel), chainSkills), s.parallel.task, originalTask);
|
|
428
447
|
const progressPrecreated = behavior.progress;
|
|
429
448
|
if (progressPrecreated) {
|
|
430
|
-
writeInitialProgressFile(
|
|
449
|
+
writeInitialProgressFile(progressDir);
|
|
431
450
|
progressInstructionCreated = true;
|
|
432
451
|
}
|
|
433
452
|
return {
|
|
@@ -450,12 +469,103 @@ export function executeAsyncChain(
|
|
|
450
469
|
}
|
|
451
470
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
452
471
|
});
|
|
472
|
+
const steps = params.attachRoot
|
|
473
|
+
? [{
|
|
474
|
+
agent: params.attachRoot.agent,
|
|
475
|
+
task: "",
|
|
476
|
+
label: params.attachRoot.label ?? `Attached root ${params.attachRoot.runId}`,
|
|
477
|
+
outputName: params.attachRoot.outputName,
|
|
478
|
+
importAsyncRoot: {
|
|
479
|
+
runId: params.attachRoot.runId,
|
|
480
|
+
asyncDir: params.attachRoot.asyncDir,
|
|
481
|
+
resultPath: params.attachRoot.resultPath,
|
|
482
|
+
index: params.attachRoot.index,
|
|
483
|
+
},
|
|
484
|
+
inheritProjectContext: false,
|
|
485
|
+
inheritSkills: false,
|
|
486
|
+
}, ...builtSteps]
|
|
487
|
+
: builtSteps;
|
|
488
|
+
return { steps, runnerCwd, workflowGraph, eventChain: graphChain, ...(originalTask !== undefined ? { originalTask } : {}) };
|
|
453
489
|
} catch (error) {
|
|
454
|
-
if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return
|
|
490
|
+
if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return { error: error.message };
|
|
455
491
|
throw error;
|
|
456
492
|
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Execute a chain asynchronously
|
|
497
|
+
*/
|
|
498
|
+
export function executeAsyncChain(
|
|
499
|
+
id: string,
|
|
500
|
+
params: AsyncChainParams,
|
|
501
|
+
): AsyncExecutionResult {
|
|
502
|
+
const {
|
|
503
|
+
chain,
|
|
504
|
+
agents,
|
|
505
|
+
ctx,
|
|
506
|
+
cwd,
|
|
507
|
+
maxOutput,
|
|
508
|
+
artifactsDir,
|
|
509
|
+
artifactConfig,
|
|
510
|
+
shareEnabled,
|
|
511
|
+
sessionRoot,
|
|
512
|
+
sessionFilesByFlatIndex,
|
|
513
|
+
maxSubagentDepth,
|
|
514
|
+
worktreeSetupHook,
|
|
515
|
+
worktreeSetupHookTimeoutMs,
|
|
516
|
+
controlConfig,
|
|
517
|
+
controlIntercomTarget,
|
|
518
|
+
childIntercomTarget,
|
|
519
|
+
nestedRoute,
|
|
520
|
+
} = params;
|
|
521
|
+
const resultMode = params.resultMode ?? "chain";
|
|
522
|
+
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
523
|
+
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
524
|
+
const asyncDir = inheritedNestedRoute
|
|
525
|
+
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
526
|
+
: path.join(ASYNC_DIR, id);
|
|
527
|
+
try {
|
|
528
|
+
fs.mkdirSync(asyncDir, { recursive: true });
|
|
529
|
+
} catch (error) {
|
|
530
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
531
|
+
return {
|
|
532
|
+
content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
|
|
533
|
+
isError: true,
|
|
534
|
+
details: { mode: resultMode, results: [] },
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const built = buildAsyncRunnerSteps(id, {
|
|
539
|
+
chain,
|
|
540
|
+
task: params.task,
|
|
541
|
+
attachRoot: params.attachRoot,
|
|
542
|
+
resultMode,
|
|
543
|
+
agents,
|
|
544
|
+
ctx,
|
|
545
|
+
availableModels: params.availableModels,
|
|
546
|
+
cwd,
|
|
547
|
+
chainSkills: params.chainSkills,
|
|
548
|
+
sessionFilesByFlatIndex,
|
|
549
|
+
progressDir: params.progressDir ?? (resultMode === "parallel" ? path.join(asyncDir, "progress") : undefined),
|
|
550
|
+
dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
|
|
551
|
+
maxSubagentDepth,
|
|
552
|
+
asyncDir,
|
|
553
|
+
});
|
|
554
|
+
if ("error" in built) {
|
|
555
|
+
try {
|
|
556
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
557
|
+
} catch {
|
|
558
|
+
// Best-effort cleanup for validation failures before the runner is spawned.
|
|
559
|
+
}
|
|
560
|
+
return formatAsyncStartError(resultMode, built.error);
|
|
561
|
+
}
|
|
562
|
+
const { steps, runnerCwd, workflowGraph, eventChain } = built;
|
|
457
563
|
let childTargetIndex = 0;
|
|
458
564
|
const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
|
|
565
|
+
if (!("parallel" in step) && step.importAsyncRoot) {
|
|
566
|
+
childTargetIndex++;
|
|
567
|
+
return [undefined];
|
|
568
|
+
}
|
|
459
569
|
if ("parallel" in step) {
|
|
460
570
|
if (!Array.isArray(step.parallel)) {
|
|
461
571
|
childTargetIndex++;
|
|
@@ -513,17 +623,17 @@ export function executeAsyncChain(
|
|
|
513
623
|
}
|
|
514
624
|
|
|
515
625
|
if (spawnResult.pid) {
|
|
516
|
-
const
|
|
517
|
-
const firstAgents = isParallelStep(
|
|
518
|
-
?
|
|
519
|
-
: isDynamicParallelStep(
|
|
520
|
-
? [
|
|
521
|
-
: [(
|
|
626
|
+
const eventFirstStep = eventChain[0];
|
|
627
|
+
const firstAgents = isParallelStep(eventFirstStep)
|
|
628
|
+
? eventFirstStep.parallel.map((t) => t.agent)
|
|
629
|
+
: isDynamicParallelStep(eventFirstStep)
|
|
630
|
+
? [eventFirstStep.parallel.agent]
|
|
631
|
+
: [(eventFirstStep as SequentialStep).agent];
|
|
522
632
|
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
523
633
|
const flatAgents: string[] = [];
|
|
524
634
|
let flatStepStart = 0;
|
|
525
|
-
for (let stepIndex = 0; stepIndex <
|
|
526
|
-
const step =
|
|
635
|
+
for (let stepIndex = 0; stepIndex < eventChain.length; stepIndex++) {
|
|
636
|
+
const step = eventChain[stepIndex]!;
|
|
527
637
|
if (isParallelStep(step)) {
|
|
528
638
|
parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
|
|
529
639
|
flatAgents.push(...step.parallel.map((task) => task.agent));
|
|
@@ -561,7 +671,7 @@ export function executeAsyncChain(
|
|
|
561
671
|
state: "running",
|
|
562
672
|
agent: firstAgents[0],
|
|
563
673
|
agents: flatAgents,
|
|
564
|
-
chainStepCount:
|
|
674
|
+
chainStepCount: eventChain.length,
|
|
565
675
|
parallelGroups,
|
|
566
676
|
startedAt: now,
|
|
567
677
|
lastUpdate: now,
|
|
@@ -578,15 +688,15 @@ export function executeAsyncChain(
|
|
|
578
688
|
mode: resultMode,
|
|
579
689
|
agent: firstAgents[0],
|
|
580
690
|
agents: flatAgents,
|
|
581
|
-
task: isParallelStep(
|
|
582
|
-
?
|
|
583
|
-
: isDynamicParallelStep(
|
|
584
|
-
?
|
|
585
|
-
: (
|
|
586
|
-
chain:
|
|
691
|
+
task: isParallelStep(eventFirstStep)
|
|
692
|
+
? eventFirstStep.parallel[0]?.task?.slice(0, 50)
|
|
693
|
+
: isDynamicParallelStep(eventFirstStep)
|
|
694
|
+
? eventFirstStep.parallel.task?.slice(0, 50)
|
|
695
|
+
: (eventFirstStep as SequentialStep).task?.slice(0, 50),
|
|
696
|
+
chain: eventChain.map((s) =>
|
|
587
697
|
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
|
|
588
698
|
),
|
|
589
|
-
chainStepCount:
|
|
699
|
+
chainStepCount: eventChain.length,
|
|
590
700
|
parallelGroups,
|
|
591
701
|
workflowGraph,
|
|
592
702
|
cwd: runnerCwd,
|
|
@@ -663,6 +773,7 @@ export function executeAsyncSingle(
|
|
|
663
773
|
|
|
664
774
|
const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
|
|
665
775
|
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd);
|
|
776
|
+
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath);
|
|
666
777
|
const outputMode = params.outputMode ?? "inline";
|
|
667
778
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
668
779
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
@@ -681,6 +792,7 @@ export function executeAsyncSingle(
|
|
|
681
792
|
id,
|
|
682
793
|
steps: [
|
|
683
794
|
{
|
|
795
|
+
parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
|
|
684
796
|
agent,
|
|
685
797
|
task: taskWithOutputInstruction,
|
|
686
798
|
cwd: runnerCwd,
|
|
@@ -691,6 +803,7 @@ export function executeAsyncSingle(
|
|
|
691
803
|
),
|
|
692
804
|
tools: agentConfig.tools,
|
|
693
805
|
extensions: agentConfig.extensions,
|
|
806
|
+
subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
|
|
694
807
|
mcpDirectTools: agentConfig.mcpDirectTools,
|
|
695
808
|
completionGuard: agentConfig.completionGuard,
|
|
696
809
|
systemPrompt,
|