@oh-my-pi/pi-coding-agent 17.0.2 → 17.0.3
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 +70 -20
- package/dist/cli.js +4087 -4108
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +29 -0
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +14 -3
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-manager.d.ts +5 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/package.json +12 -12
- package/src/cli/file-processor.ts +1 -2
- package/src/config/settings-schema.ts +4 -3
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
- package/src/lsp/client.ts +57 -0
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +152 -1
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/interactive-mode.ts +30 -8
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +129 -87
- package/src/session/agent-session.ts +75 -21
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-manager.ts +9 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +99 -21
- package/src/task/index.ts +258 -429
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +58 -0
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/fetch.ts +28 -105
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/index.ts +47 -14
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +14 -26
- package/src/tools/todo.ts +126 -13
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/zip.ts +16 -1
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared policy resolution and execution for task and eval subagents.
|
|
3
|
+
*
|
|
4
|
+
* The two public frontends deliberately retain their presentation concerns, but
|
|
5
|
+
* every decision that affects what a child may run lives here.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
12
|
+
import type { LocalProtocolOptions } from "../internal-urls";
|
|
13
|
+
import { registerArtifactsDir } from "../internal-urls/registry-helpers";
|
|
14
|
+
import { MCPManager } from "../mcp/manager";
|
|
15
|
+
import { loadOverallPlanReference } from "../plan-mode/plan-handoff";
|
|
16
|
+
import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
|
|
17
|
+
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
|
|
18
|
+
import { MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
19
|
+
import type { ToolSession } from "../tools";
|
|
20
|
+
import { isIrcEnabled } from "../tools/hub";
|
|
21
|
+
import { buildOutputValidator } from "../tools/output-schema-validator";
|
|
22
|
+
import { type DiscoveryResult, discoverAgents, getAgent } from "./discovery";
|
|
23
|
+
import { type ExecutorOptions, runSubprocess } from "./executor";
|
|
24
|
+
import {
|
|
25
|
+
applyEligibleNestedPatches,
|
|
26
|
+
type IsolationContext,
|
|
27
|
+
makeIsolationCommitMessage,
|
|
28
|
+
mergeIsolatedChanges,
|
|
29
|
+
prepareIsolationContext,
|
|
30
|
+
runIsolatedSubprocess,
|
|
31
|
+
} from "./isolation-runner";
|
|
32
|
+
import { generateTaskName } from "./name-generator";
|
|
33
|
+
import { AgentOutputManager } from "./output-manager";
|
|
34
|
+
import { resolveSpawnPolicy } from "./spawn-policy";
|
|
35
|
+
import {
|
|
36
|
+
type AgentDefinition,
|
|
37
|
+
type AgentProgress,
|
|
38
|
+
canSpawnAtDepth,
|
|
39
|
+
type SingleResult,
|
|
40
|
+
type StructuredSubagentOutput,
|
|
41
|
+
} from "./types";
|
|
42
|
+
import { type NestedRepoPatch, parseIsolationMode } from "./worktree";
|
|
43
|
+
|
|
44
|
+
/** Validation behavior requested for an effective output schema. */
|
|
45
|
+
export type StructuredSubagentSchemaMode = "permissive" | "strict";
|
|
46
|
+
|
|
47
|
+
/** Where an effective output schema came from. */
|
|
48
|
+
export type StructuredSubagentSchemaSource = "caller" | "agent" | "session" | "none";
|
|
49
|
+
|
|
50
|
+
/** Final structured completion metadata returned for a schema-bearing run. */
|
|
51
|
+
export type StructuredSubagentSchemaResult = StructuredSubagentOutput;
|
|
52
|
+
|
|
53
|
+
/** A schema validation or extraction error attached to structured completion metadata. */
|
|
54
|
+
export type StructuredSubagentSchemaError = NonNullable<StructuredSubagentOutput["error"]>;
|
|
55
|
+
|
|
56
|
+
/** A selected schema paired with its source and enforcement mode. */
|
|
57
|
+
export interface StructuredSubagentSchemaResolution {
|
|
58
|
+
schema: unknown;
|
|
59
|
+
source: StructuredSubagentSchemaSource;
|
|
60
|
+
mode: StructuredSubagentSchemaMode;
|
|
61
|
+
outputSchemaOverridesAgent: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Isolation controls shared by the task and eval surfaces. */
|
|
65
|
+
export interface StructuredSubagentIsolationControls {
|
|
66
|
+
requested?: boolean;
|
|
67
|
+
merge?: "patch" | "branch";
|
|
68
|
+
apply?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Identity and presentation metadata supplied by the calling surface. */
|
|
72
|
+
export interface StructuredSubagentIdentity {
|
|
73
|
+
/** A previously reserved output/registry id. */
|
|
74
|
+
id?: string;
|
|
75
|
+
/** Stable user-facing label used when allocating a new id. */
|
|
76
|
+
label?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** One normalized child invocation. */
|
|
80
|
+
export interface StructuredSubagentRequest {
|
|
81
|
+
session: ToolSession;
|
|
82
|
+
invocationKind: "task" | "eval";
|
|
83
|
+
assignment: string;
|
|
84
|
+
context?: string;
|
|
85
|
+
agent?: string;
|
|
86
|
+
model?: string | string[];
|
|
87
|
+
/** Presence, rather than truthiness, makes this the highest-priority schema. */
|
|
88
|
+
outputSchema?: unknown;
|
|
89
|
+
schemaMode?: StructuredSubagentSchemaMode;
|
|
90
|
+
identity?: StructuredSubagentIdentity;
|
|
91
|
+
index?: number;
|
|
92
|
+
parentToolCallId?: string;
|
|
93
|
+
detached?: boolean;
|
|
94
|
+
invokedAt?: number;
|
|
95
|
+
acquiredAt?: number;
|
|
96
|
+
isolation?: StructuredSubagentIsolationControls;
|
|
97
|
+
/** The parent agent name forbidden from recursively spawning itself. */
|
|
98
|
+
blockedAgent?: string;
|
|
99
|
+
/** Preserve a completed temporary artifacts directory for an agent:// handle. */
|
|
100
|
+
retainArtifacts?: boolean;
|
|
101
|
+
/** Task UI agents keep live registry references; eval one-shots normally do not. */
|
|
102
|
+
keepAlive?: boolean;
|
|
103
|
+
/** Task subagents share their parent's eval kernel; eval bridge children must not. */
|
|
104
|
+
shareEvalSession?: boolean;
|
|
105
|
+
/** Task frontends may inherit LSP; eval frontends normally set this false. */
|
|
106
|
+
enableLsp?: boolean;
|
|
107
|
+
/** Explicitly pass false for plan mode or invocation kinds that must not use IRC. */
|
|
108
|
+
enableIrc?: boolean;
|
|
109
|
+
/** `0` disables executor wall-clock timeout. Undefined inherits settings. */
|
|
110
|
+
maxRuntimeMs?: number;
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
onProgress?: (progress: AgentProgress) => void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A normalized preflight result, reusable by tests and adapters. */
|
|
116
|
+
export interface EffectiveSubagentPolicy {
|
|
117
|
+
discovery: DiscoveryResult;
|
|
118
|
+
agentName: string;
|
|
119
|
+
agent: AgentDefinition;
|
|
120
|
+
effectiveAgent: AgentDefinition;
|
|
121
|
+
modelOverride?: string | string[];
|
|
122
|
+
parentActiveModelPattern?: string;
|
|
123
|
+
schema: StructuredSubagentSchemaResolution;
|
|
124
|
+
planMode: boolean;
|
|
125
|
+
isIsolated: boolean;
|
|
126
|
+
mergeMode: "patch" | "branch";
|
|
127
|
+
applyChanges: boolean;
|
|
128
|
+
enableLsp: boolean;
|
|
129
|
+
enableIrc: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Settled child execution plus data needed by the frontends' own rendering. */
|
|
133
|
+
export interface StructuredSubagentResult {
|
|
134
|
+
result: SingleResult;
|
|
135
|
+
policy: EffectiveSubagentPolicy;
|
|
136
|
+
mergeSummary: string;
|
|
137
|
+
changesApplied: boolean | null;
|
|
138
|
+
artifactsDir: string;
|
|
139
|
+
temporaryArtifacts: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Machine-readable failure category so adapters can retain their native errors. */
|
|
143
|
+
export class StructuredSubagentError extends Error {
|
|
144
|
+
readonly kind: "preflight" | "isolation" | "execution";
|
|
145
|
+
|
|
146
|
+
constructor(kind: "preflight" | "isolation" | "execution", message: string, options?: ErrorOptions) {
|
|
147
|
+
super(message, options);
|
|
148
|
+
this.name = "StructuredSubagentError";
|
|
149
|
+
this.kind = kind;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const PLAN_MODE_TOOLS = ["read", "grep", "glob", "web_search"] as const;
|
|
154
|
+
|
|
155
|
+
function renderSubagentPrompt(assignment: string): string {
|
|
156
|
+
return prompt.render(subagentUserPromptTemplate, { assignment: assignment.trim() });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function trimToUndefined(value: string | undefined): string | undefined {
|
|
160
|
+
const trimmed = value?.trim();
|
|
161
|
+
return trimmed || undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sanitizeAgentId(value: string | undefined): string | undefined {
|
|
165
|
+
const trimmed = trimToUndefined(value);
|
|
166
|
+
const sanitized = trimmed?.replace(/[^A-Za-z0-9_-]+/g, "").slice(0, 48);
|
|
167
|
+
return sanitized || undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveSchema(request: StructuredSubagentRequest, agent: AgentDefinition): StructuredSubagentSchemaResolution {
|
|
171
|
+
const mode = request.schemaMode ?? request.session.outputSchemaMode ?? "permissive";
|
|
172
|
+
if (Object.hasOwn(request, "outputSchema")) {
|
|
173
|
+
return { schema: request.outputSchema, source: "caller", mode, outputSchemaOverridesAgent: true };
|
|
174
|
+
}
|
|
175
|
+
if (agent.output !== undefined) {
|
|
176
|
+
return { schema: agent.output, source: "agent", mode, outputSchemaOverridesAgent: false };
|
|
177
|
+
}
|
|
178
|
+
if (request.session.outputSchema !== undefined) {
|
|
179
|
+
return { schema: request.session.outputSchema, source: "session", mode, outputSchemaOverridesAgent: false };
|
|
180
|
+
}
|
|
181
|
+
return { schema: undefined, source: "none", mode, outputSchemaOverridesAgent: false };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createPlanModeAgent(agent: AgentDefinition): AgentDefinition {
|
|
185
|
+
const tools = [...PLAN_MODE_TOOLS, ...(agent.tools ?? []).filter(tool => tool === "ast_grep")];
|
|
186
|
+
return {
|
|
187
|
+
...agent,
|
|
188
|
+
systemPrompt: `${planModeSubagentPrompt}\n\n${agent.systemPrompt}`,
|
|
189
|
+
tools,
|
|
190
|
+
spawns: undefined,
|
|
191
|
+
prewalk: undefined,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function assertPlanControlsAllowed(request: StructuredSubagentRequest, planMode: boolean): void {
|
|
196
|
+
if (!planMode) return;
|
|
197
|
+
const isolation = request.isolation;
|
|
198
|
+
if (
|
|
199
|
+
isolation &&
|
|
200
|
+
(Object.hasOwn(isolation, "requested") || Object.hasOwn(isolation, "apply") || Object.hasOwn(isolation, "merge"))
|
|
201
|
+
) {
|
|
202
|
+
throw new StructuredSubagentError(
|
|
203
|
+
"preflight",
|
|
204
|
+
"Subagent isolation, apply, and merge controls are unavailable in plan mode.",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function assertDepthAndSpawnAllowed(request: StructuredSubagentRequest, agentName: string): void {
|
|
210
|
+
const taskDepth = request.session.taskDepth ?? 0;
|
|
211
|
+
const maxDepth = request.session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
212
|
+
if (!canSpawnAtDepth(maxDepth, taskDepth)) {
|
|
213
|
+
throw new StructuredSubagentError(
|
|
214
|
+
"preflight",
|
|
215
|
+
`Cannot spawn another agent at task depth ${taskDepth}; maximum depth is ${maxDepth}.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const blockedAgent = request.blockedAgent ?? $env.PI_BLOCKED_AGENT;
|
|
219
|
+
if (blockedAgent && blockedAgent === agentName) {
|
|
220
|
+
throw new StructuredSubagentError(
|
|
221
|
+
"preflight",
|
|
222
|
+
`Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const spawnPolicy = resolveSpawnPolicy(request.session.getSessionSpawns());
|
|
226
|
+
if (!spawnPolicy.enabled || (spawnPolicy.allowedAgents !== null && !spawnPolicy.allowedAgents.includes(agentName))) {
|
|
227
|
+
throw new StructuredSubagentError(
|
|
228
|
+
"preflight",
|
|
229
|
+
`Cannot spawn '${agentName}'. Allowed: ${spawnPolicy.allowedErrorText}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolve every policy shared by task and eval before allocating artifacts or
|
|
236
|
+
* dispatching work. Callers translate {@link StructuredSubagentError} into
|
|
237
|
+
* their own wire-level error surface.
|
|
238
|
+
*/
|
|
239
|
+
export async function resolveEffectiveSubagentPolicy(
|
|
240
|
+
request: StructuredSubagentRequest,
|
|
241
|
+
): Promise<EffectiveSubagentPolicy> {
|
|
242
|
+
const spawnPolicy = resolveSpawnPolicy(request.session.getSessionSpawns());
|
|
243
|
+
const agentName = request.agent?.trim() || spawnPolicy.defaultAgent;
|
|
244
|
+
const planMode = request.session.getPlanModeState?.()?.enabled === true;
|
|
245
|
+
assertPlanControlsAllowed(request, planMode);
|
|
246
|
+
assertDepthAndSpawnAllowed(request, agentName);
|
|
247
|
+
|
|
248
|
+
const discovery = await discoverAgents(request.session.cwd);
|
|
249
|
+
const agent = getAgent(discovery.agents, agentName);
|
|
250
|
+
if (!agent) {
|
|
251
|
+
const available = discovery.agents.map(candidate => candidate.name).join(", ") || "none";
|
|
252
|
+
throw new StructuredSubagentError("preflight", `Unknown agent "${agentName}". Available: ${available}`);
|
|
253
|
+
}
|
|
254
|
+
const disabledAgents = request.session.settings.get("task.disabledAgents") as string[];
|
|
255
|
+
if (disabledAgents.includes(agentName)) {
|
|
256
|
+
const enabled = discovery.agents
|
|
257
|
+
.filter(candidate => !disabledAgents.includes(candidate.name))
|
|
258
|
+
.map(candidate => candidate.name);
|
|
259
|
+
throw new StructuredSubagentError(
|
|
260
|
+
"preflight",
|
|
261
|
+
`Agent "${agentName}" is disabled in settings. Enable it via /agents, or use a different agent type.${enabled.length > 0 ? ` Available: ${enabled.join(", ")}` : ""}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const effectiveAgent = planMode ? createPlanModeAgent(agent) : agent;
|
|
266
|
+
const schema = resolveSchema(request, effectiveAgent);
|
|
267
|
+
if (schema.source === "caller" || (schema.source !== "none" && schema.mode === "strict")) {
|
|
268
|
+
const { error } = buildOutputValidator(schema.schema);
|
|
269
|
+
if (error) {
|
|
270
|
+
const scope =
|
|
271
|
+
schema.source === "caller" ? (schema.mode === "strict" ? "strict caller" : "caller") : "strict effective";
|
|
272
|
+
throw new StructuredSubagentError("preflight", `Invalid ${scope} output schema: ${error}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const agentModelOverrides = request.session.settings.get("task.agentModelOverrides");
|
|
276
|
+
const parentActiveModelPattern = request.session.getActiveModelString?.();
|
|
277
|
+
const modelOverride = resolveAgentModelPatterns({
|
|
278
|
+
settingsOverride: request.model ?? agentModelOverrides[agentName],
|
|
279
|
+
agentModel: effectiveAgent.model,
|
|
280
|
+
settings: request.session.settings,
|
|
281
|
+
activeModelPattern: parentActiveModelPattern,
|
|
282
|
+
fallbackModelPattern: request.session.getModelString?.(),
|
|
283
|
+
});
|
|
284
|
+
const isolationMode = request.session.settings.get("task.isolation.mode");
|
|
285
|
+
const isIsolated = request.isolation?.requested === true;
|
|
286
|
+
if (isIsolated && isolationMode === "none") {
|
|
287
|
+
throw new StructuredSubagentError(
|
|
288
|
+
"preflight",
|
|
289
|
+
`Subagent isolated execution requires task.isolation.mode to be set; current mode is "none".`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
discovery,
|
|
294
|
+
agentName,
|
|
295
|
+
agent,
|
|
296
|
+
effectiveAgent,
|
|
297
|
+
modelOverride,
|
|
298
|
+
parentActiveModelPattern,
|
|
299
|
+
schema,
|
|
300
|
+
planMode,
|
|
301
|
+
isIsolated,
|
|
302
|
+
mergeMode: request.isolation?.merge ?? request.session.settings.get("task.isolation.merge"),
|
|
303
|
+
applyChanges: request.isolation?.apply !== false,
|
|
304
|
+
enableLsp:
|
|
305
|
+
!planMode &&
|
|
306
|
+
(request.enableLsp ?? ((request.session.enableLsp ?? true) && request.session.settings.get("task.enableLsp"))),
|
|
307
|
+
enableIrc:
|
|
308
|
+
!planMode &&
|
|
309
|
+
(request.enableIrc ??
|
|
310
|
+
(request.session.enableIrc !== false &&
|
|
311
|
+
isIrcEnabled(request.session.settings, request.session.taskDepth ?? 0))),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Reserve a session-global agent id only after preflight has succeeded. */
|
|
316
|
+
export async function reserveStructuredSubagentId(
|
|
317
|
+
session: ToolSession,
|
|
318
|
+
identity: StructuredSubagentIdentity | undefined,
|
|
319
|
+
): Promise<string> {
|
|
320
|
+
if (identity?.id) return identity.id;
|
|
321
|
+
const manager = session.agentOutputManager ?? new AgentOutputManager(session.getArtifactsDir ?? (() => null));
|
|
322
|
+
session.agentOutputManager ??= manager;
|
|
323
|
+
return manager.allocate(sanitizeAgentId(identity?.label) ?? generateTaskName());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
interface ArtifactLease {
|
|
327
|
+
sessionFile: string | null;
|
|
328
|
+
artifactsDir: string;
|
|
329
|
+
temporary: boolean;
|
|
330
|
+
unregister: (() => void) | undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function leaseArtifacts(
|
|
334
|
+
session: ToolSession,
|
|
335
|
+
invocationKind: StructuredSubagentRequest["invocationKind"],
|
|
336
|
+
): Promise<ArtifactLease> {
|
|
337
|
+
const sessionFile = session.getSessionFile();
|
|
338
|
+
if (sessionFile) {
|
|
339
|
+
const artifactsDir = sessionFile.slice(0, -6);
|
|
340
|
+
await fs.mkdir(artifactsDir, { recursive: true });
|
|
341
|
+
return { sessionFile, artifactsDir, temporary: false, unregister: undefined };
|
|
342
|
+
}
|
|
343
|
+
const artifactsDir = path.join(
|
|
344
|
+
os.tmpdir(),
|
|
345
|
+
`${invocationKind === "eval" ? "omp-eval-agent" : "omp-task"}-${Snowflake.next()}`,
|
|
346
|
+
);
|
|
347
|
+
await fs.mkdir(artifactsDir, { recursive: true });
|
|
348
|
+
return { sessionFile: null, artifactsDir, temporary: true, unregister: registerArtifactsDir(artifactsDir) };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function resolveAutoloadSkills(session: ToolSession, agent: AgentDefinition) {
|
|
352
|
+
const skills = [...(session.skills ?? [])];
|
|
353
|
+
const autoloadSkills = agent.autoloadSkills?.length
|
|
354
|
+
? agent.autoloadSkills.map(name => skills.find(skill => skill.name === name)).filter(skill => skill !== undefined)
|
|
355
|
+
: [];
|
|
356
|
+
return { skills, autoloadSkills };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function buildExecutorOptions(
|
|
360
|
+
request: StructuredSubagentRequest,
|
|
361
|
+
policy: EffectiveSubagentPolicy,
|
|
362
|
+
lease: ArtifactLease,
|
|
363
|
+
id: string,
|
|
364
|
+
): ExecutorOptions {
|
|
365
|
+
const { session } = request;
|
|
366
|
+
const { skills, autoloadSkills } = resolveAutoloadSkills(session, policy.agent);
|
|
367
|
+
const localProtocolOptions: LocalProtocolOptions = session.localProtocolOptions ?? {
|
|
368
|
+
getArtifactsDir: session.getArtifactsDir ?? (() => null),
|
|
369
|
+
getSessionId: session.getSessionId ?? (() => null),
|
|
370
|
+
};
|
|
371
|
+
const enableMCP = !policy.planMode && (session.enableMCP ?? true);
|
|
372
|
+
return {
|
|
373
|
+
cwd: session.cwd,
|
|
374
|
+
agent: policy.effectiveAgent,
|
|
375
|
+
task: renderSubagentPrompt(request.assignment),
|
|
376
|
+
assignment: request.assignment.trim(),
|
|
377
|
+
context: request.context?.trim() || undefined,
|
|
378
|
+
planReference: undefined,
|
|
379
|
+
description: trimToUndefined(request.identity?.label),
|
|
380
|
+
index: request.index ?? 0,
|
|
381
|
+
parentToolCallId: request.parentToolCallId,
|
|
382
|
+
detached: request.detached,
|
|
383
|
+
id,
|
|
384
|
+
taskDepth: session.taskDepth ?? 0,
|
|
385
|
+
invokedAt: request.invokedAt,
|
|
386
|
+
acquiredAt: request.acquiredAt,
|
|
387
|
+
modelOverride: policy.modelOverride,
|
|
388
|
+
parentActiveModelPattern: policy.parentActiveModelPattern,
|
|
389
|
+
thinkingLevel: policy.effectiveAgent.thinkingLevel,
|
|
390
|
+
...(policy.schema.source === "none"
|
|
391
|
+
? {}
|
|
392
|
+
: {
|
|
393
|
+
outputSchemaSource: policy.schema.source,
|
|
394
|
+
outputSchema: policy.schema.schema,
|
|
395
|
+
outputSchemaOverridesAgent: policy.schema.outputSchemaOverridesAgent,
|
|
396
|
+
outputSchemaMode: policy.schema.mode,
|
|
397
|
+
}),
|
|
398
|
+
sessionFile: lease.sessionFile,
|
|
399
|
+
persistArtifacts: !lease.temporary,
|
|
400
|
+
artifactsDir: lease.artifactsDir,
|
|
401
|
+
enableLsp: policy.enableLsp,
|
|
402
|
+
enableIrc: policy.enableIrc,
|
|
403
|
+
maxRuntimeMs: request.maxRuntimeMs,
|
|
404
|
+
restrictToolNames: policy.planMode,
|
|
405
|
+
keepAlive: request.keepAlive,
|
|
406
|
+
signal: request.signal,
|
|
407
|
+
eventBus: session.eventBus,
|
|
408
|
+
onProgress: request.onProgress,
|
|
409
|
+
authStorage: session.authStorage,
|
|
410
|
+
modelRegistry: session.modelRegistry,
|
|
411
|
+
settings: session.settings,
|
|
412
|
+
mcpManager: enableMCP ? (session.mcpManager ?? MCPManager.instance()) : undefined,
|
|
413
|
+
enableMCP,
|
|
414
|
+
contextFiles: session.contextFiles?.filter(file => path.basename(file.path).toLowerCase() !== "agents.md"),
|
|
415
|
+
skills,
|
|
416
|
+
autoloadSkills,
|
|
417
|
+
workspaceTree: session.workspaceTree,
|
|
418
|
+
promptTemplates: session.promptTemplates,
|
|
419
|
+
rules: session.rules,
|
|
420
|
+
preloadedExtensionPaths: policy.planMode ? [] : session.extensionPaths,
|
|
421
|
+
preloadedCustomToolPaths: policy.planMode ? [] : session.customToolPaths,
|
|
422
|
+
localProtocolOptions,
|
|
423
|
+
parentArtifactManager: session.getArtifactManager?.() ?? undefined,
|
|
424
|
+
parentHindsightSessionState: session.getHindsightSessionState?.(),
|
|
425
|
+
parentMnemopiSessionState: session.getMnemopiSessionState?.(),
|
|
426
|
+
parentTelemetry: session.getTelemetry?.(),
|
|
427
|
+
parentEvalSessionId: request.shareEvalSession === false ? undefined : (session.getEvalSessionId?.() ?? undefined),
|
|
428
|
+
parentAgentId: session.getAgentId?.() ?? MAIN_AGENT_ID,
|
|
429
|
+
parentServiceTier: session.getServiceTierByFamily ? (session.getServiceTierByFamily() ?? null) : undefined,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function loadPlanReference(
|
|
434
|
+
request: StructuredSubagentRequest,
|
|
435
|
+
policy: EffectiveSubagentPolicy,
|
|
436
|
+
): Promise<{ path: string; content: string } | undefined> {
|
|
437
|
+
if (policy.planMode) return undefined;
|
|
438
|
+
const localProtocolOptions: LocalProtocolOptions = request.session.localProtocolOptions ?? {
|
|
439
|
+
getArtifactsDir: request.session.getArtifactsDir ?? (() => null),
|
|
440
|
+
getSessionId: request.session.getSessionId ?? (() => null),
|
|
441
|
+
};
|
|
442
|
+
return loadOverallPlanReference(request.session.getPlanReferencePath?.() ?? "local://PLAN.md", localProtocolOptions);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function buildFailureResult(
|
|
446
|
+
request: StructuredSubagentRequest,
|
|
447
|
+
policy: EffectiveSubagentPolicy,
|
|
448
|
+
id: string,
|
|
449
|
+
startedAt: number,
|
|
450
|
+
) {
|
|
451
|
+
return (error: unknown): SingleResult => {
|
|
452
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
453
|
+
return {
|
|
454
|
+
index: request.index ?? 0,
|
|
455
|
+
id,
|
|
456
|
+
agent: policy.agent.name,
|
|
457
|
+
agentSource: policy.agent.source,
|
|
458
|
+
task: renderSubagentPrompt(request.assignment),
|
|
459
|
+
assignment: request.assignment.trim(),
|
|
460
|
+
description: trimToUndefined(request.identity?.label),
|
|
461
|
+
exitCode: 1,
|
|
462
|
+
output: "",
|
|
463
|
+
stderr: message,
|
|
464
|
+
truncated: false,
|
|
465
|
+
durationMs: Date.now() - startedAt,
|
|
466
|
+
tokens: 0,
|
|
467
|
+
requests: 0,
|
|
468
|
+
modelOverride: policy.modelOverride,
|
|
469
|
+
error: message,
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function persistNestedPatches(
|
|
475
|
+
artifactsDir: string,
|
|
476
|
+
agentId: string,
|
|
477
|
+
nestedPatches: NestedRepoPatch[],
|
|
478
|
+
): Promise<string[]> {
|
|
479
|
+
const saved: string[] = [];
|
|
480
|
+
for (const [index, nestedPatch] of nestedPatches.entries()) {
|
|
481
|
+
const destination = path.join(
|
|
482
|
+
artifactsDir,
|
|
483
|
+
`${agentId}.nested-${index}-${nestedPatch.relativePath.replace(/[^a-zA-Z0-9._-]/g, "_") || "root"}.patch`,
|
|
484
|
+
);
|
|
485
|
+
try {
|
|
486
|
+
await fs.writeFile(destination, nestedPatch.patch);
|
|
487
|
+
saved.push(destination);
|
|
488
|
+
} catch {}
|
|
489
|
+
}
|
|
490
|
+
return saved;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function isolationRecoveryHint(result: SingleResult, artifactsDir: string): Promise<string> {
|
|
494
|
+
const hints: string[] = [];
|
|
495
|
+
if (result.patchPath) hints.push(`Captured patch preserved at ${result.patchPath}.`);
|
|
496
|
+
for (const nestedPath of await persistNestedPatches(artifactsDir, result.id, result.nestedPatches ?? [])) {
|
|
497
|
+
hints.push(`Captured nested patch preserved at ${nestedPath}.`);
|
|
498
|
+
}
|
|
499
|
+
if (result.branchName) hints.push(`Captured branch preserved as ${result.branchName}.`);
|
|
500
|
+
return hints.length > 0 ? ` ${hints.join(" ")}` : "";
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function attachStructuredOutputMetadata(result: SingleResult, schema: StructuredSubagentSchemaResolution): void {
|
|
504
|
+
if (schema.source === "none") {
|
|
505
|
+
delete result.structuredOutput;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (result.structuredOutput) return;
|
|
509
|
+
let fallbackData: unknown = result.output;
|
|
510
|
+
try {
|
|
511
|
+
fallbackData = JSON.parse(result.output);
|
|
512
|
+
} catch {}
|
|
513
|
+
const output: StructuredSubagentOutput = {
|
|
514
|
+
source: schema.source,
|
|
515
|
+
mode: schema.mode,
|
|
516
|
+
status: result.exitCode === 0 ? "valid" : "invalid",
|
|
517
|
+
data: fallbackData,
|
|
518
|
+
...(result.error ? { error: result.error } : {}),
|
|
519
|
+
};
|
|
520
|
+
result.structuredOutput = output;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Execute a validated subagent. Preflight errors occur before any artifact
|
|
525
|
+
* lease or child dispatch; callers keep responsibility for their result text.
|
|
526
|
+
*/
|
|
527
|
+
export async function runStructuredSubagent(request: StructuredSubagentRequest): Promise<StructuredSubagentResult> {
|
|
528
|
+
const policy = await resolveEffectiveSubagentPolicy(request);
|
|
529
|
+
const lease = await leaseArtifacts(request.session, request.invocationKind);
|
|
530
|
+
let changesApplied: boolean | null = null;
|
|
531
|
+
let mergeSummary = "";
|
|
532
|
+
let requiresRecoveryArtifacts = false;
|
|
533
|
+
let completedSuccessfully = false;
|
|
534
|
+
try {
|
|
535
|
+
const id = await reserveStructuredSubagentId(request.session, {
|
|
536
|
+
...request.identity,
|
|
537
|
+
label: request.identity?.label ?? (request.invocationKind === "eval" ? "EvalAgent" : undefined),
|
|
538
|
+
});
|
|
539
|
+
const baseOptions = buildExecutorOptions(request, policy, lease, id);
|
|
540
|
+
baseOptions.planReference = await loadPlanReference(request, policy);
|
|
541
|
+
let isolationContext: IsolationContext | null = null;
|
|
542
|
+
if (policy.isIsolated) {
|
|
543
|
+
try {
|
|
544
|
+
isolationContext = await prepareIsolationContext(request.session.cwd);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
547
|
+
throw new StructuredSubagentError(
|
|
548
|
+
"isolation",
|
|
549
|
+
`Isolated subagent execution requires a git repository. ${message}`,
|
|
550
|
+
{ cause: error },
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const result = !isolationContext
|
|
555
|
+
? await runSubprocess(baseOptions)
|
|
556
|
+
: await runIsolatedSubprocess({
|
|
557
|
+
baseOptions,
|
|
558
|
+
context: isolationContext,
|
|
559
|
+
preferredBackend: parseIsolationMode(request.session.settings.get("task.isolation.mode")),
|
|
560
|
+
agentId: id,
|
|
561
|
+
mergeMode: policy.mergeMode,
|
|
562
|
+
artifactsDir: lease.artifactsDir,
|
|
563
|
+
description: trimToUndefined(request.identity?.label),
|
|
564
|
+
buildCommitMessage: makeIsolationCommitMessage(request.session),
|
|
565
|
+
buildFailureResult: buildFailureResult(request, policy, id, Date.now()),
|
|
566
|
+
});
|
|
567
|
+
attachStructuredOutputMetadata(result, policy.schema);
|
|
568
|
+
requiresRecoveryArtifacts =
|
|
569
|
+
policy.isIsolated &&
|
|
570
|
+
(result.exitCode !== 0 || result.error !== undefined || result.aborted === true) &&
|
|
571
|
+
(result.patchPath !== undefined || result.branchName !== undefined || (result.nestedPatches?.length ?? 0) > 0);
|
|
572
|
+
|
|
573
|
+
if (
|
|
574
|
+
policy.isIsolated &&
|
|
575
|
+
isolationContext &&
|
|
576
|
+
policy.applyChanges &&
|
|
577
|
+
result.exitCode === 0 &&
|
|
578
|
+
!result.error &&
|
|
579
|
+
!result.aborted
|
|
580
|
+
) {
|
|
581
|
+
const outcome = await mergeIsolatedChanges({
|
|
582
|
+
result,
|
|
583
|
+
repoRoot: isolationContext.repoRoot,
|
|
584
|
+
mergeMode: policy.mergeMode,
|
|
585
|
+
});
|
|
586
|
+
mergeSummary = outcome.summary;
|
|
587
|
+
changesApplied = outcome.changesApplied;
|
|
588
|
+
if (outcome.changesApplied !== false) {
|
|
589
|
+
const nestedPatchSummary = await applyEligibleNestedPatches({
|
|
590
|
+
result,
|
|
591
|
+
repoRoot: isolationContext.repoRoot,
|
|
592
|
+
mergeMode: policy.mergeMode,
|
|
593
|
+
changesApplied: outcome.changesApplied,
|
|
594
|
+
mergedBranchForNestedPatches: outcome.mergedBranchForNestedPatches,
|
|
595
|
+
commitMessage: makeIsolationCommitMessage(request.session)(),
|
|
596
|
+
});
|
|
597
|
+
mergeSummary += nestedPatchSummary;
|
|
598
|
+
requiresRecoveryArtifacts ||=
|
|
599
|
+
nestedPatchSummary.includes("<system-notification>") && (result.nestedPatches?.length ?? 0) > 0;
|
|
600
|
+
}
|
|
601
|
+
} else if (policy.isIsolated && isolationContext && !policy.applyChanges) {
|
|
602
|
+
if (result.branchName)
|
|
603
|
+
mergeSummary = `\n\nIsolation: changes captured on branch \`${result.branchName}\` (apply=false). Not merged.`;
|
|
604
|
+
else if (result.patchPath)
|
|
605
|
+
mergeSummary = `\n\nIsolation: changes captured at \`${result.patchPath}\` (apply=false). Not applied.`;
|
|
606
|
+
else if ((result.nestedPatches?.length ?? 0) > 0)
|
|
607
|
+
mergeSummary = `\n\nIsolation: changes captured for ${result.nestedPatches?.length} nested ${(result.nestedPatches?.length ?? 0) === 1 ? "repository" : "repositories"} (apply=false). Not applied.`;
|
|
608
|
+
else mergeSummary = "\n\nIsolation: no changes captured.";
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
completedSuccessfully = result.exitCode === 0 && !result.error && !result.aborted;
|
|
612
|
+
return {
|
|
613
|
+
result,
|
|
614
|
+
policy,
|
|
615
|
+
mergeSummary,
|
|
616
|
+
changesApplied,
|
|
617
|
+
artifactsDir: lease.artifactsDir,
|
|
618
|
+
temporaryArtifacts: lease.temporary,
|
|
619
|
+
};
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (error instanceof StructuredSubagentError) throw error;
|
|
622
|
+
throw new StructuredSubagentError(
|
|
623
|
+
"execution",
|
|
624
|
+
`Subagent execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
625
|
+
{ cause: error },
|
|
626
|
+
);
|
|
627
|
+
} finally {
|
|
628
|
+
const shouldRetainArtifacts =
|
|
629
|
+
(request.retainArtifacts && completedSuccessfully) ||
|
|
630
|
+
(policy.isIsolated && (!policy.applyChanges || changesApplied === false || requiresRecoveryArtifacts));
|
|
631
|
+
const shouldCleanup = lease.temporary && !shouldRetainArtifacts;
|
|
632
|
+
if (shouldCleanup) {
|
|
633
|
+
await fs.rm(lease.artifactsDir, { recursive: true, force: true });
|
|
634
|
+
lease.unregister?.();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Build the recovery suffix used by adapters after an isolated failure. */
|
|
640
|
+
export async function buildStructuredSubagentRecoveryHint(result: SingleResult, artifactsDir: string): Promise<string> {
|
|
641
|
+
return isolationRecoveryHint(result, artifactsDir);
|
|
642
|
+
}
|