@oh-my-pi/pi-coding-agent 16.1.14 → 16.1.16
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 +69 -0
- package/dist/cli.js +3705 -4015
- package/dist/types/advisor/runtime.d.ts +3 -0
- package/dist/types/cli/args.d.ts +2 -5
- package/dist/types/cli/flag-tables.d.ts +2 -2
- package/dist/types/cli/session-picker.d.ts +4 -2
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +32 -1
- package/dist/types/eval/agent-bridge.d.ts +19 -0
- package/dist/types/eval/js/shared/helpers.d.ts +1 -13
- package/dist/types/eval/js/shared/types.d.ts +1 -1
- package/dist/types/eval/js/worker-protocol.d.ts +1 -1
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/export/share.d.ts +8 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/transports/stdio.d.ts +12 -1
- package/dist/types/modes/components/plugin-settings.d.ts +5 -0
- package/dist/types/modes/components/session-selector.d.ts +25 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/secrets/obfuscator.d.ts +3 -3
- package/dist/types/task/isolation-runner.d.ts +128 -0
- package/dist/types/task/worktree.d.ts +14 -1
- package/dist/types/thinking.d.ts +15 -0
- package/dist/types/tools/eval-render.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +11 -17
- package/dist/types/tools/todo.d.ts +26 -28
- package/dist/types/tui/output-block.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/shell-snapshot.d.ts +10 -0
- package/dist/types/web/search/providers/exa.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +17 -3
- package/package.json +12 -12
- package/scripts/build-binary.ts +18 -4
- package/src/advisor/__tests__/advisor.test.ts +114 -0
- package/src/advisor/runtime.ts +129 -1
- package/src/cli/args.ts +4 -5
- package/src/cli/flag-tables.ts +3 -3
- package/src/cli/gallery-fixtures/interaction.ts +6 -9
- package/src/cli/gallery-fixtures/shell.ts +15 -23
- package/src/cli/session-picker.ts +17 -3
- package/src/commands/launch.ts +3 -3
- package/src/config/model-registry.ts +12 -4
- package/src/config/settings-schema.ts +37 -1
- package/src/edit/renderer.ts +34 -12
- package/src/eval/__tests__/agent-bridge.test.ts +462 -3
- package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
- package/src/eval/__tests__/julia-prelude.test.ts +1 -30
- package/src/eval/__tests__/prelude-agent.test.ts +42 -8
- package/src/eval/agent-bridge.ts +301 -71
- package/src/eval/jl/prelude.jl +32 -227
- package/src/eval/jl/runner.jl +38 -12
- package/src/eval/js/shared/helpers.ts +1 -114
- package/src/eval/js/shared/prelude.txt +13 -27
- package/src/eval/js/shared/runtime.ts +0 -6
- package/src/eval/js/shared/types.ts +1 -1
- package/src/eval/js/worker-protocol.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +13 -0
- package/src/eval/py/executor.ts +1 -1
- package/src/eval/py/prelude.py +47 -105
- package/src/eval/py/runner.py +0 -6
- package/src/eval/rb/prelude.rb +21 -189
- package/src/eval/rb/runner.rb +116 -9
- package/src/exec/bash-executor.ts +44 -0
- package/src/export/html/tool-views.generated.js +29 -29
- package/src/export/share.ts +51 -28
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/local-protocol.ts +100 -53
- package/src/main.ts +15 -4
- package/src/mcp/transports/stdio.ts +20 -4
- package/src/modes/acp/acp-event-mapper.ts +7 -2
- package/src/modes/components/custom-editor.test.ts +22 -0
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/footer.ts +4 -3
- package/src/modes/components/plugin-settings.ts +7 -1
- package/src/modes/components/session-selector.ts +143 -29
- package/src/modes/components/status-line/component.ts +5 -1
- package/src/modes/components/status-line/context-thresholds.ts +11 -3
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/controllers/command-controller.ts +6 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/utils/copy-targets.ts +7 -2
- package/src/prompts/system/system-prompt.md +6 -6
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/bash.md +18 -2
- package/src/prompts/tools/eval.md +19 -19
- package/src/prompts/tools/search.md +1 -0
- package/src/prompts/tools/todo.md +1 -1
- package/src/secrets/obfuscator.ts +3 -9
- package/src/session/agent-session.ts +264 -52
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/task/index.ts +61 -207
- package/src/task/isolation-runner.ts +354 -0
- package/src/task/worktree.ts +46 -9
- package/src/thinking.ts +20 -0
- package/src/tools/ask.ts +44 -38
- package/src/tools/bash.ts +9 -2
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval-render.ts +34 -27
- package/src/tools/eval.ts +100 -103
- package/src/tools/index.ts +8 -1
- package/src/tools/read.ts +136 -60
- package/src/tools/todo.ts +60 -64
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +11 -0
- package/src/utils/image-resize.ts +30 -0
- package/src/utils/shell-snapshot.ts +63 -1
- package/src/web/search/providers/exa.ts +85 -1
- package/src/web/search/providers/perplexity.ts +18 -6
package/src/eval/agent-bridge.ts
CHANGED
|
@@ -12,9 +12,19 @@ import { MCPManager } from "../mcp/manager";
|
|
|
12
12
|
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
|
|
13
13
|
import { MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
14
14
|
import * as taskDiscovery from "../task/discovery";
|
|
15
|
+
import type { ExecutorOptions } from "../task/executor";
|
|
15
16
|
import * as taskExecutor from "../task/executor";
|
|
17
|
+
import {
|
|
18
|
+
applyEligibleNestedPatches,
|
|
19
|
+
type IsolationContext,
|
|
20
|
+
makeIsolationCommitMessage,
|
|
21
|
+
mergeIsolatedChanges,
|
|
22
|
+
prepareIsolationContext,
|
|
23
|
+
runIsolatedSubprocess,
|
|
24
|
+
} from "../task/isolation-runner";
|
|
16
25
|
import { AgentOutputManager } from "../task/output-manager";
|
|
17
26
|
import type { AgentDefinition, AgentProgress, SingleResult } from "../task/types";
|
|
27
|
+
import { type NestedRepoPatch, parseIsolationMode } from "../task/worktree";
|
|
18
28
|
import type { ToolSession } from "../tools";
|
|
19
29
|
import { ToolError } from "../tools/tool-errors";
|
|
20
30
|
import { withBridgeTimeoutPause } from "./bridge-timeout";
|
|
@@ -33,18 +43,47 @@ const DEFAULT_AGENT_LABEL = "EvalAgent";
|
|
|
33
43
|
|
|
34
44
|
const agentArgsSchema = type({
|
|
35
45
|
prompt: "string>0",
|
|
36
|
-
"
|
|
46
|
+
"agent?": "string>0",
|
|
37
47
|
"model?": "string>0|string>0[]",
|
|
38
48
|
"label?": "string",
|
|
39
49
|
"schema?": "unknown",
|
|
50
|
+
"isolated?": "boolean",
|
|
51
|
+
"apply?": "boolean",
|
|
52
|
+
"merge?": "boolean",
|
|
53
|
+
"handle?": "boolean",
|
|
40
54
|
});
|
|
41
55
|
|
|
42
56
|
interface EvalAgentArgs {
|
|
43
57
|
prompt: string;
|
|
44
|
-
|
|
58
|
+
agent?: string;
|
|
45
59
|
model?: string | string[];
|
|
46
60
|
label?: string;
|
|
47
61
|
schema?: unknown;
|
|
62
|
+
/**
|
|
63
|
+
* Run this subagent inside an isolation worktree (copy-on-write of the
|
|
64
|
+
* parent repo). Strict opt-in: defaults to `false` regardless of the
|
|
65
|
+
* session's `task.isolation.mode`, mirroring the `task` tool. Passing
|
|
66
|
+
* `true` while `task.isolation.mode === "none"` errors out instead of
|
|
67
|
+
* silently downgrading.
|
|
68
|
+
*/
|
|
69
|
+
isolated?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* When isolated, apply the captured patch / merge the captured branch back
|
|
72
|
+
* to the parent repo (default `true`). Pass `false` to keep changes in the
|
|
73
|
+
* isolation worktree only — the patch artifact path / branch name lands in
|
|
74
|
+
* the result so the caller can inspect or apply manually.
|
|
75
|
+
*/
|
|
76
|
+
apply?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* When isolated, allow branch-merge mode (cherry-pick onto HEAD). Defaults
|
|
79
|
+
* to `true`, in which case the active `task.isolation.merge` setting picks
|
|
80
|
+
* patch vs branch. Pass `false` to force patch mode even when the setting
|
|
81
|
+
* is `"branch"` — useful when a fan-out cannot tolerate the per-call git
|
|
82
|
+
* lock + repo mutation that branch mode performs.
|
|
83
|
+
*/
|
|
84
|
+
merge?: boolean;
|
|
85
|
+
/** True when a runtime helper will return an `agent://` handle backed by the output artifacts. */
|
|
86
|
+
handle?: boolean;
|
|
48
87
|
}
|
|
49
88
|
|
|
50
89
|
export interface EvalAgentBridgeOptions {
|
|
@@ -60,6 +99,24 @@ export interface EvalAgentResult {
|
|
|
60
99
|
id: string;
|
|
61
100
|
model?: string | string[];
|
|
62
101
|
structured: boolean;
|
|
102
|
+
/** True iff this run executed inside an isolation worktree. */
|
|
103
|
+
isolated?: boolean;
|
|
104
|
+
/** Captured patch artifact (patch mode) — surfaced regardless of `apply`. */
|
|
105
|
+
patchPath?: string;
|
|
106
|
+
/** Captured branch (branch mode) — surfaced regardless of `apply`. */
|
|
107
|
+
branchName?: string;
|
|
108
|
+
/** Captured nested repository patches — surfaced for isolated `apply=false` manual application. */
|
|
109
|
+
nestedPatches?: NestedRepoPatch[];
|
|
110
|
+
/**
|
|
111
|
+
* Tri-state apply outcome for isolated runs:
|
|
112
|
+
* - `true` — apply ran (or had nothing to do) and left the repo clean.
|
|
113
|
+
* - `false` — apply attempted and failed; artifacts preserved.
|
|
114
|
+
* - `null` — caller opted out via `apply=false`.
|
|
115
|
+
* Omitted for non-isolated runs.
|
|
116
|
+
*/
|
|
117
|
+
changesApplied?: boolean | null;
|
|
118
|
+
/** Human-readable isolation apply/merge summary; kept out of schema-backed `text`. */
|
|
119
|
+
isolationSummary?: string;
|
|
63
120
|
};
|
|
64
121
|
}
|
|
65
122
|
|
|
@@ -129,15 +186,47 @@ function getOutputManager(session: ToolSession): AgentOutputManager {
|
|
|
129
186
|
return manager;
|
|
130
187
|
}
|
|
131
188
|
|
|
132
|
-
|
|
189
|
+
interface ArtifactPaths {
|
|
133
190
|
sessionFile: string | null;
|
|
134
191
|
artifactsDir: string;
|
|
135
|
-
|
|
192
|
+
/**
|
|
193
|
+
* True when `artifactsDir` was created off the session path (no session
|
|
194
|
+
* file). Caller is then free to `rm -rf` it once all isolated patch
|
|
195
|
+
* artifacts have been consumed or applied.
|
|
196
|
+
*/
|
|
197
|
+
tempArtifactsDir: boolean;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function getArtifacts(session: ToolSession): Promise<ArtifactPaths> {
|
|
136
201
|
const sessionFile = session.getSessionFile();
|
|
137
202
|
const sessionArtifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
|
|
203
|
+
const tempArtifactsDir = sessionArtifactsDir === null;
|
|
138
204
|
const artifactsDir = sessionArtifactsDir ?? path.join(os.tmpdir(), `omp-eval-agent-${Snowflake.next()}`);
|
|
139
205
|
await fs.mkdir(artifactsDir, { recursive: true });
|
|
140
|
-
return { sessionFile, artifactsDir };
|
|
206
|
+
return { sessionFile, artifactsDir, tempArtifactsDir };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Persist nested-repo patches to the per-call artifacts dir so an isolated
|
|
211
|
+
* apply failure can surface their paths in the thrown ToolError. The
|
|
212
|
+
* isolation worktree is already gone by the time we run, so without this the
|
|
213
|
+
* captured nested patches would be unrecoverable.
|
|
214
|
+
*/
|
|
215
|
+
async function persistNestedPatches(
|
|
216
|
+
artifactsDir: string,
|
|
217
|
+
agentId: string,
|
|
218
|
+
nestedPatches: NestedRepoPatch[],
|
|
219
|
+
): Promise<string[]> {
|
|
220
|
+
const written: string[] = [];
|
|
221
|
+
for (let index = 0; index < nestedPatches.length; index++) {
|
|
222
|
+
const patch = nestedPatches[index];
|
|
223
|
+
if (!patch) continue;
|
|
224
|
+
const slug = patch.relativePath.replace(/[^A-Za-z0-9._-]+/g, "_") || `nested-${index}`;
|
|
225
|
+
const out = path.join(artifactsDir, `${agentId}.nested-${index}-${slug}.patch`);
|
|
226
|
+
await Bun.write(out, patch.patch);
|
|
227
|
+
written.push(out);
|
|
228
|
+
}
|
|
229
|
+
return written;
|
|
141
230
|
}
|
|
142
231
|
|
|
143
232
|
function emitProgressStatus(emitStatus: ((event: JsStatusEvent) => void) | undefined, progress: AgentProgress): void {
|
|
@@ -187,7 +276,7 @@ function buildSubagentFailureMessage(agentName: string, result: SingleResult): s
|
|
|
187
276
|
*/
|
|
188
277
|
export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOptions): Promise<EvalAgentResult> {
|
|
189
278
|
const parsed = parseAgentArgs(args);
|
|
190
|
-
const agentName = parsed.
|
|
279
|
+
const agentName = parsed.agent ?? DEFAULT_AGENT_TYPE;
|
|
191
280
|
const structured = Object.hasOwn(parsed, "schema");
|
|
192
281
|
|
|
193
282
|
assertNotPlanMode(options.session);
|
|
@@ -235,85 +324,226 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
235
324
|
};
|
|
236
325
|
const parentArtifactManager = options.session.getArtifactManager?.() ?? undefined;
|
|
237
326
|
const mcpManager = options.session.mcpManager ?? MCPManager.instance();
|
|
238
|
-
const { sessionFile, artifactsDir } = await getArtifacts(options.session);
|
|
327
|
+
const { sessionFile, artifactsDir, tempArtifactsDir } = await getArtifacts(options.session);
|
|
239
328
|
const outputManager = getOutputManager(options.session);
|
|
240
329
|
const id = await outputManager.allocate(outputIdBase(parsed.label, agentName));
|
|
241
330
|
const assignment = parsed.prompt.trim();
|
|
242
|
-
// Suspend eval timeout accounting while the subagent owns control. The
|
|
243
|
-
// timeout clock restarts once the bridge returns to the cell runtime.
|
|
244
|
-
const result = await withBridgeTimeoutPause(options.emitStatus, () =>
|
|
245
|
-
taskExecutor.runSubprocess({
|
|
246
|
-
cwd: options.session.cwd,
|
|
247
|
-
agent: effectiveAgent,
|
|
248
|
-
task: renderSubagentPrompt(assignment),
|
|
249
|
-
assignment,
|
|
250
|
-
description: trimToUndefined(parsed.label),
|
|
251
|
-
index: 0,
|
|
252
|
-
id,
|
|
253
|
-
taskDepth: options.session.taskDepth ?? 0,
|
|
254
|
-
modelOverride,
|
|
255
|
-
parentActiveModelPattern,
|
|
256
|
-
thinkingLevel: effectiveAgent.thinkingLevel,
|
|
257
|
-
outputSchema: structured ? parsed.schema : undefined,
|
|
258
|
-
sessionFile,
|
|
259
|
-
persistArtifacts: Boolean(sessionFile),
|
|
260
|
-
artifactsDir,
|
|
261
|
-
// Eval `agent()` subagents are short-lived programmatic helpers (data
|
|
262
|
-
// collection, structured output, parallel() fan-out). LSP server
|
|
263
|
-
// cold-start costs tens of seconds and is pure overhead here, so it is
|
|
264
|
-
// forced off regardless of the `task.enableLsp` setting — that knob only
|
|
265
|
-
// governs LSP-aware delegation through the `task` tool.
|
|
266
|
-
enableLsp: false,
|
|
267
|
-
signal: options.signal,
|
|
268
|
-
eventBus: options.session.eventBus,
|
|
269
|
-
onProgress: progress => emitProgressStatus(options.emitStatus, progress),
|
|
270
|
-
authStorage: options.session.authStorage,
|
|
271
|
-
modelRegistry: options.session.modelRegistry,
|
|
272
|
-
settings: options.session.settings,
|
|
273
|
-
// Eval `agent()` subagents are never wall-clock capped: the parent
|
|
274
|
-
// cell's idle watchdog is suspended for the whole bridge call
|
|
275
|
-
// (withBridgeTimeoutPause), so a long-running phase/recovery workflow
|
|
276
|
-
// must not be killed by `task.maxRuntimeMs`. Force the limit off
|
|
277
|
-
// regardless of the inherited session setting.
|
|
278
|
-
maxRuntimeMs: 0,
|
|
279
|
-
mcpManager,
|
|
280
|
-
contextFiles,
|
|
281
|
-
skills: availableSkills,
|
|
282
|
-
autoloadSkills: resolvedAutoloadSkills,
|
|
283
|
-
workspaceTree: options.session.workspaceTree,
|
|
284
|
-
promptTemplates: options.session.promptTemplates,
|
|
285
|
-
localProtocolOptions,
|
|
286
|
-
parentArtifactManager,
|
|
287
|
-
parentHindsightSessionState: options.session.getHindsightSessionState?.(),
|
|
288
|
-
parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
|
|
289
|
-
parentTelemetry: options.session.getTelemetry?.(),
|
|
290
|
-
parentAgentId: options.session.getAgentId?.() ?? MAIN_AGENT_ID,
|
|
291
|
-
// Deliberately omit parentEvalSessionId: the parent's Python kernel is
|
|
292
|
-
// blocked on this bridge call, so sharing the eval session would deadlock
|
|
293
|
-
// (subagent queues behind the parent's in-flight execution, parent waits
|
|
294
|
-
// for subagent → circular). Each bridge-spawned subagent gets its own
|
|
295
|
-
// eval session with an independent kernel.
|
|
296
|
-
}),
|
|
297
|
-
);
|
|
298
331
|
|
|
299
|
-
|
|
300
|
-
|
|
332
|
+
// Isolation gating. Strict opt-in: only the explicit `isolated=true`
|
|
333
|
+
// argument turns it on; `task.isolation.mode` no longer drives the
|
|
334
|
+
// default. Mirrors the `task` tool so eval `agent()` and `task` callers
|
|
335
|
+
// see the same semantic. `isolated=true` while the mode is `"none"`
|
|
336
|
+
// surfaces a clear error instead of silently downgrading.
|
|
337
|
+
const isolationMode = options.session.settings.get("task.isolation.mode");
|
|
338
|
+
const isolationEnabledInSettings = isolationMode !== "none";
|
|
339
|
+
if (parsed.isolated === true && !isolationEnabledInSettings) {
|
|
340
|
+
throw new ToolError(`agent(isolated=True) requires task.isolation.mode to be set; current mode is "none".`);
|
|
301
341
|
}
|
|
342
|
+
const isIsolated = parsed.isolated === true;
|
|
343
|
+
const settingsMergeMode = options.session.settings.get("task.isolation.merge");
|
|
344
|
+
const mergeMode: "patch" | "branch" = parsed.merge === false ? "patch" : settingsMergeMode;
|
|
345
|
+
const applyChanges = parsed.apply !== false;
|
|
346
|
+
|
|
347
|
+
// Isolation context capture (prepareIsolationContext → captureBaseline)
|
|
348
|
+
// happens inside the timeout-pause closure below; on dirty/large repos the
|
|
349
|
+
// baseline walk can run long and must stay covered by the eval idle
|
|
350
|
+
// suspension.
|
|
351
|
+
|
|
352
|
+
const buildCommitMessage = makeIsolationCommitMessage(options.session);
|
|
302
353
|
|
|
303
|
-
|
|
354
|
+
const baseRunOptions: ExecutorOptions = {
|
|
355
|
+
cwd: options.session.cwd,
|
|
356
|
+
agent: effectiveAgent,
|
|
357
|
+
task: renderSubagentPrompt(assignment),
|
|
358
|
+
assignment,
|
|
359
|
+
description: trimToUndefined(parsed.label),
|
|
360
|
+
index: 0,
|
|
361
|
+
id,
|
|
362
|
+
taskDepth: options.session.taskDepth ?? 0,
|
|
363
|
+
modelOverride,
|
|
364
|
+
parentActiveModelPattern,
|
|
365
|
+
thinkingLevel: effectiveAgent.thinkingLevel,
|
|
366
|
+
outputSchema: structured ? parsed.schema : undefined,
|
|
367
|
+
sessionFile,
|
|
368
|
+
persistArtifacts: Boolean(sessionFile),
|
|
369
|
+
artifactsDir,
|
|
370
|
+
// Eval `agent()` subagents are short-lived programmatic helpers (data
|
|
371
|
+
// collection, structured output, parallel() fan-out). LSP server
|
|
372
|
+
// cold-start costs tens of seconds and is pure overhead here, so it is
|
|
373
|
+
// forced off regardless of the `task.enableLsp` setting — that knob only
|
|
374
|
+
// governs LSP-aware delegation through the `task` tool.
|
|
375
|
+
enableLsp: false,
|
|
376
|
+
signal: options.signal,
|
|
377
|
+
eventBus: options.session.eventBus,
|
|
378
|
+
onProgress: progress => emitProgressStatus(options.emitStatus, progress),
|
|
379
|
+
authStorage: options.session.authStorage,
|
|
380
|
+
modelRegistry: options.session.modelRegistry,
|
|
381
|
+
settings: options.session.settings,
|
|
382
|
+
// Eval `agent()` subagents are never wall-clock capped: the parent
|
|
383
|
+
// cell's idle watchdog is suspended for the whole bridge call
|
|
384
|
+
// (withBridgeTimeoutPause), so a long-running phase/recovery workflow
|
|
385
|
+
// must not be killed by `task.maxRuntimeMs`. Force the limit off
|
|
386
|
+
// regardless of the inherited session setting.
|
|
387
|
+
maxRuntimeMs: 0,
|
|
388
|
+
mcpManager,
|
|
389
|
+
contextFiles,
|
|
390
|
+
skills: availableSkills,
|
|
391
|
+
autoloadSkills: resolvedAutoloadSkills,
|
|
392
|
+
workspaceTree: options.session.workspaceTree,
|
|
393
|
+
promptTemplates: options.session.promptTemplates,
|
|
394
|
+
localProtocolOptions,
|
|
395
|
+
parentArtifactManager,
|
|
396
|
+
parentHindsightSessionState: options.session.getHindsightSessionState?.(),
|
|
397
|
+
parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
|
|
398
|
+
parentTelemetry: options.session.getTelemetry?.(),
|
|
399
|
+
parentAgentId: options.session.getAgentId?.() ?? MAIN_AGENT_ID,
|
|
400
|
+
// Deliberately omit parentEvalSessionId: the parent's Python kernel is
|
|
401
|
+
// blocked on this bridge call, so sharing the eval session would deadlock
|
|
402
|
+
// (subagent queues behind the parent's in-flight execution, parent waits
|
|
403
|
+
// for subagent → circular). Each bridge-spawned subagent gets its own
|
|
404
|
+
// eval session with an independent kernel.
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// Suspend eval timeout accounting through the WHOLE bridge call: the
|
|
408
|
+
// subagent subprocess plus any isolation post-processing (merge,
|
|
409
|
+
// nested-patch apply, cleanup). All of that is host-side work while the
|
|
410
|
+
// runtime is parked waiting for the result, and the cell timeout must
|
|
411
|
+
// not abort us mid-cherry-pick or mid-nested-commit. The clock restarts
|
|
412
|
+
// only after we hand control back to the runtime.
|
|
413
|
+
const { result, mergeSummary, changesApplied } = await withBridgeTimeoutPause(options.emitStatus, async () => {
|
|
414
|
+
let isolationContext: IsolationContext | null = null;
|
|
415
|
+
if (isIsolated) {
|
|
416
|
+
try {
|
|
417
|
+
isolationContext = await prepareIsolationContext(options.session.cwd);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
420
|
+
throw new ToolError(`Isolated agent() execution requires a git repository. ${message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const preferredBackend = isIsolated ? parseIsolationMode(isolationMode) : undefined;
|
|
424
|
+
|
|
425
|
+
const result = await (async () => {
|
|
426
|
+
if (!isolationContext) {
|
|
427
|
+
return taskExecutor.runSubprocess(baseRunOptions);
|
|
428
|
+
}
|
|
429
|
+
const taskStart = Date.now();
|
|
430
|
+
return runIsolatedSubprocess({
|
|
431
|
+
baseOptions: baseRunOptions,
|
|
432
|
+
context: isolationContext,
|
|
433
|
+
preferredBackend,
|
|
434
|
+
agentId: id,
|
|
435
|
+
mergeMode,
|
|
436
|
+
artifactsDir,
|
|
437
|
+
description: trimToUndefined(parsed.label),
|
|
438
|
+
buildCommitMessage,
|
|
439
|
+
buildFailureResult: err => {
|
|
440
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
441
|
+
return {
|
|
442
|
+
index: 0,
|
|
443
|
+
id,
|
|
444
|
+
agent: effectiveAgent.name,
|
|
445
|
+
agentSource: effectiveAgent.source,
|
|
446
|
+
task: renderSubagentPrompt(assignment),
|
|
447
|
+
assignment,
|
|
448
|
+
description: trimToUndefined(parsed.label),
|
|
449
|
+
exitCode: 1,
|
|
450
|
+
output: "",
|
|
451
|
+
stderr: message,
|
|
452
|
+
truncated: false,
|
|
453
|
+
durationMs: Date.now() - taskStart,
|
|
454
|
+
tokens: 0,
|
|
455
|
+
requests: 0,
|
|
456
|
+
modelOverride,
|
|
457
|
+
error: message,
|
|
458
|
+
};
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
})();
|
|
462
|
+
|
|
463
|
+
if (result.exitCode !== 0 || result.error || result.aborted) {
|
|
464
|
+
throw new ToolError(buildSubagentFailureMessage(agentName, result));
|
|
465
|
+
}
|
|
304
466
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
467
|
+
let mergeSummary = "";
|
|
468
|
+
let changesApplied: boolean | null = null;
|
|
469
|
+
if (isIsolated && isolationContext) {
|
|
470
|
+
if (applyChanges) {
|
|
471
|
+
const outcome = await mergeIsolatedChanges({
|
|
472
|
+
result,
|
|
473
|
+
repoRoot: isolationContext.repoRoot,
|
|
474
|
+
mergeMode,
|
|
475
|
+
});
|
|
476
|
+
mergeSummary = outcome.summary;
|
|
477
|
+
changesApplied = outcome.changesApplied;
|
|
478
|
+
if (outcome.changesApplied === false) {
|
|
479
|
+
const summaryText = outcome.summary.trim();
|
|
480
|
+
const recoveryParts: string[] = [];
|
|
481
|
+
if (result.patchPath) recoveryParts.push(`Captured patch preserved at ${result.patchPath}.`);
|
|
482
|
+
if (result.branchName) recoveryParts.push(`Captured branch preserved as ${result.branchName}.`);
|
|
483
|
+
if (result.nestedPatches?.length) {
|
|
484
|
+
const nestedPaths = await persistNestedPatches(artifactsDir, result.id, result.nestedPatches);
|
|
485
|
+
recoveryParts.push(
|
|
486
|
+
`Captured nested repository patches (${result.nestedPatches.length}) preserved at: ${nestedPaths.join(", ")}.`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const recoveryHint = recoveryParts.length > 0 ? ` ${recoveryParts.join(" ")}` : "";
|
|
490
|
+
throw new ToolError(
|
|
491
|
+
`agent() isolated apply failed for ${result.id}${summaryText ? `: ${summaryText}` : ""}${recoveryHint}`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
mergeSummary += await applyEligibleNestedPatches({
|
|
496
|
+
result,
|
|
497
|
+
repoRoot: isolationContext.repoRoot,
|
|
498
|
+
mergeMode,
|
|
499
|
+
changesApplied: outcome.changesApplied,
|
|
500
|
+
mergedBranchForNestedPatches: outcome.mergedBranchForNestedPatches,
|
|
501
|
+
commitMessage: buildCommitMessage(),
|
|
502
|
+
});
|
|
503
|
+
} else if (result.branchName) {
|
|
504
|
+
mergeSummary = `\n\nIsolation: changes captured on branch \`${result.branchName}\` (apply=false). Not merged.`;
|
|
505
|
+
} else if (result.patchPath) {
|
|
506
|
+
mergeSummary = `\n\nIsolation: changes captured at \`${result.patchPath}\` (apply=false). Not applied.`;
|
|
507
|
+
} else {
|
|
508
|
+
const nestedPatches = result.nestedPatches ?? [];
|
|
509
|
+
if (nestedPatches.length > 0) {
|
|
510
|
+
mergeSummary = `\n\nIsolation: changes captured for ${nestedPatches.length} nested repositor${nestedPatches.length === 1 ? "y" : "ies"} (apply=false). Not applied.`;
|
|
511
|
+
} else {
|
|
512
|
+
mergeSummary = "\n\nIsolation: no changes captured.";
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Clean up the temp artifacts dir we created for this call only when the
|
|
518
|
+
// caller will not need files from it later. Keep it when the runtime helper
|
|
519
|
+
// will return an `agent://` handle (the `.md`/`.jsonl` backing files live
|
|
520
|
+
// here) and on `apply=false` (`changesApplied === null`) where the caller
|
|
521
|
+
// consumes `details.patchPath` / `details.branchName` /
|
|
522
|
+
// `details.nestedPatches` out of band. Failed isolated applies throw
|
|
523
|
+
// earlier with a recovery hint, so they never reach this gate.
|
|
524
|
+
const shouldCleanupTempArtifacts = tempArtifactsDir && !parsed.handle && (!isIsolated || changesApplied === true);
|
|
525
|
+
if (shouldCleanupTempArtifacts) {
|
|
526
|
+
await fs.rm(artifactsDir, { recursive: true, force: true });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
options.session.recordEvalSubagentUsage?.(result.usage?.output ?? 0);
|
|
530
|
+
|
|
531
|
+
return { result, mergeSummary, changesApplied };
|
|
532
|
+
});
|
|
309
533
|
|
|
310
534
|
return {
|
|
311
|
-
text: result.output,
|
|
535
|
+
text: structured ? result.output : result.output + mergeSummary,
|
|
312
536
|
details: {
|
|
313
537
|
agent: result.agent,
|
|
314
538
|
id: result.id,
|
|
315
539
|
model: result.resolvedModel ?? modelOverride,
|
|
316
540
|
structured,
|
|
541
|
+
isolated: isIsolated || undefined,
|
|
542
|
+
patchPath: result.patchPath,
|
|
543
|
+
branchName: result.branchName,
|
|
544
|
+
nestedPatches: result.nestedPatches?.length ? result.nestedPatches : undefined,
|
|
545
|
+
changesApplied: isIsolated ? changesApplied : undefined,
|
|
546
|
+
isolationSummary: mergeSummary ? mergeSummary.trim() : undefined,
|
|
317
547
|
},
|
|
318
548
|
};
|
|
319
549
|
}
|