@oh-my-pi/pi-coding-agent 16.2.12 → 16.3.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 +115 -7
- package/dist/cli.js +5996 -5945
- package/dist/types/advisor/config.d.ts +4 -2
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +9 -3
- package/dist/types/commit/model-selection.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +12 -10
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/models-config.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +28 -5
- package/dist/types/edit/modes/patch.d.ts +11 -0
- package/dist/types/eval/agent-bridge.d.ts +5 -1
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
- package/dist/types/extensibility/skills.d.ts +2 -1
- package/dist/types/mnemopi/state.d.ts +12 -0
- package/dist/types/modes/components/agent-hub.d.ts +9 -5
- package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
- package/dist/types/modes/interactive-mode.d.ts +12 -7
- package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
- package/dist/types/modes/session-teardown.d.ts +63 -0
- package/dist/types/modes/session-teardown.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +9 -4
- package/dist/types/modes/types.d.ts +2 -3
- package/dist/types/modes/utils/copy-targets.d.ts +2 -0
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
- package/dist/types/sdk.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +46 -13
- package/dist/types/session/exit-diagnostics.d.ts +48 -0
- package/dist/types/session/session-loader.d.ts +5 -0
- package/dist/types/task/executor.d.ts +12 -5
- package/dist/types/task/parallel.d.ts +1 -0
- package/dist/types/task/render.test.d.ts +1 -0
- package/dist/types/thinking.d.ts +2 -0
- package/dist/types/tools/checkpoint.d.ts +8 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +11 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/output-meta.d.ts +7 -0
- package/dist/types/tools/output-schema-validator.d.ts +7 -4
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/render-utils.d.ts +4 -1
- package/dist/types/utils/git.d.ts +47 -2
- package/dist/types/web/search/provider.d.ts +7 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/config.ts +4 -2
- package/src/cli/bench-cli.ts +7 -2
- package/src/collab/guest.ts +94 -5
- package/src/collab/host.ts +80 -3
- package/src/collab/protocol.ts +9 -2
- package/src/commit/model-selection.ts +8 -2
- package/src/config/keybindings.ts +3 -1
- package/src/config/model-discovery.ts +66 -2
- package/src/config/model-registry.ts +7 -3
- package/src/config/model-resolver.ts +51 -23
- package/src/config/models-config-schema.ts +13 -0
- package/src/config/settings-schema.ts +44 -14
- package/src/edit/hashline/diff.ts +13 -2
- package/src/edit/index.ts +58 -8
- package/src/edit/modes/patch.ts +53 -18
- package/src/edit/streaming.ts +7 -6
- package/src/eval/__tests__/agent-bridge.test.ts +57 -1
- package/src/eval/agent-bridge.ts +15 -5
- package/src/exec/bash-executor.ts +7 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
- package/src/extensibility/skills.ts +29 -6
- package/src/goals/guided-setup.ts +4 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +11 -14
- package/src/main.ts +38 -10
- package/src/mnemopi/state.ts +43 -7
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/agent-hub.ts +10 -2
- package/src/modes/components/agent-transcript-viewer.ts +39 -7
- package/src/modes/components/assistant-message.ts +16 -10
- package/src/modes/components/chat-transcript-builder.ts +11 -1
- package/src/modes/components/custom-editor.test.ts +20 -0
- package/src/modes/components/hook-selector.ts +44 -25
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/status-line/component.test.ts +44 -0
- package/src/modes/components/status-line/component.ts +9 -1
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/components/usage-row.ts +16 -2
- package/src/modes/controllers/command-controller.ts +6 -21
- package/src/modes/controllers/event-controller.ts +11 -9
- package/src/modes/controllers/extension-ui-controller.ts +84 -3
- package/src/modes/controllers/input-controller.ts +16 -13
- package/src/modes/controllers/selector-controller.ts +3 -8
- package/src/modes/controllers/streaming-reveal.ts +75 -53
- package/src/modes/controllers/tool-args-reveal.ts +340 -10
- package/src/modes/interactive-mode.ts +122 -79
- package/src/modes/rpc/rpc-client.ts +21 -0
- package/src/modes/rpc/rpc-mode.ts +197 -46
- package/src/modes/session-teardown.test.ts +219 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-wizard/scenes/theme.ts +2 -2
- package/src/modes/skill-command.ts +7 -20
- package/src/modes/theme/theme.ts +29 -15
- package/src/modes/types.ts +2 -3
- package/src/modes/utils/copy-targets.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +19 -2
- package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/agents/tester.md +6 -2
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/system/irc-incoming.md +2 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/plan-mode-approved.md +7 -10
- package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
- package/src/prompts/system/plan-mode-reference.md +3 -4
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +3 -0
- package/src/prompts/tools/irc.md +2 -1
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/rewind.md +3 -2
- package/src/prompts/tools/ssh.md +1 -0
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +18 -4
- package/src/session/agent-session.ts +660 -114
- package/src/session/exit-diagnostics.ts +202 -0
- package/src/session/session-context.ts +25 -13
- package/src/session/session-loader.ts +58 -25
- package/src/session/session-manager.ts +0 -1
- package/src/session/session-persistence.ts +30 -4
- package/src/session/settings-stream-fn.ts +14 -0
- package/src/slash-commands/builtin-registry.ts +35 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +31 -9
- package/src/task/isolation-runner.ts +18 -2
- package/src/task/parallel.ts +7 -2
- package/src/task/render.test.ts +121 -0
- package/src/task/render.ts +48 -2
- package/src/task/worktree.ts +12 -13
- package/src/task/yield-assembly.ts +8 -5
- package/src/thinking.ts +5 -0
- package/src/tools/ask.ts +188 -9
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/ast-grep.ts +11 -0
- package/src/tools/bash.ts +6 -30
- package/src/tools/checkpoint.ts +15 -1
- package/src/tools/eval-render.ts +15 -0
- package/src/tools/fetch.ts +82 -18
- package/src/tools/gh.ts +1 -1
- package/src/tools/grep.ts +45 -27
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +24 -9
- package/src/tools/output-meta.ts +50 -0
- package/src/tools/output-schema-validator.ts +152 -15
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +5 -3
- package/src/tools/ssh.ts +8 -1
- package/src/tools/yield.ts +41 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/git.ts +271 -29
- package/src/utils/thinking-display.ts +13 -0
- package/src/web/search/index.ts +9 -28
- package/src/web/search/provider.ts +26 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/types.ts +5 -1
|
@@ -233,7 +233,7 @@ interface StringDef {
|
|
|
233
233
|
|
|
234
234
|
interface NumberDef {
|
|
235
235
|
type: "number";
|
|
236
|
-
default: number;
|
|
236
|
+
default: number | undefined;
|
|
237
237
|
ui?: UiNumber;
|
|
238
238
|
}
|
|
239
239
|
|
|
@@ -1388,6 +1388,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
1388
1388
|
},
|
|
1389
1389
|
},
|
|
1390
1390
|
|
|
1391
|
+
"providers.anthropic.serverSideFallback": {
|
|
1392
|
+
type: "boolean",
|
|
1393
|
+
default: false,
|
|
1394
|
+
ui: {
|
|
1395
|
+
tab: "model",
|
|
1396
|
+
group: "Retry & Fallback",
|
|
1397
|
+
label: "Anthropic Server-Side Fallback (Fable 5)",
|
|
1398
|
+
description:
|
|
1399
|
+
"When a Claude Fable 5 / Mythos 5 request is blocked by Anthropic's safety classifier, retry it on Claude Opus 4.8 server-side (Anthropic `server-side-fallback-2026-06-01` beta). Opt-in — leaving this off preserves the pre-fallback behavior for every request.",
|
|
1400
|
+
},
|
|
1401
|
+
},
|
|
1402
|
+
|
|
1391
1403
|
// ────────────────────────────────────────────────────────────────────────
|
|
1392
1404
|
// Interaction
|
|
1393
1405
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -1997,7 +2009,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
1997
2009
|
},
|
|
1998
2010
|
},
|
|
1999
2011
|
|
|
2000
|
-
|
|
2012
|
+
// No default: an unset reserve tells the compaction layer the user never
|
|
2013
|
+
// chose one, so small-window recovery may swap in the proportional reserve
|
|
2014
|
+
// (see resolveBudgetReserveTokens). A materialized 16384 here would make
|
|
2015
|
+
// every session look explicitly configured.
|
|
2016
|
+
"compaction.reserveTokens": { type: "number", default: undefined },
|
|
2001
2017
|
|
|
2002
2018
|
"compaction.keepRecentTokens": { type: "number", default: 20000 },
|
|
2003
2019
|
|
|
@@ -4129,7 +4145,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
4129
4145
|
group: "Subagents",
|
|
4130
4146
|
label: "Soft Subagent Request Budget",
|
|
4131
4147
|
description:
|
|
4132
|
-
"Soft per-subagent request budget (assistant requests per run). Crossing it
|
|
4148
|
+
"Soft per-subagent request budget (assistant requests per run). Crossing it can inject a steering notice when task.softRequestBudgetNotice is enabled; at 1.5x the budget the run is aborted gracefully, salvaging partial output. 0 disables the guard. Bundled explore/sonic agents use a lower built-in budget.",
|
|
4133
4149
|
options: [
|
|
4134
4150
|
{ value: "0", label: "Disabled" },
|
|
4135
4151
|
{ value: "40", label: "40 requests" },
|
|
@@ -4139,6 +4155,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
4139
4155
|
},
|
|
4140
4156
|
},
|
|
4141
4157
|
|
|
4158
|
+
"task.softRequestBudgetNotice": {
|
|
4159
|
+
type: "boolean",
|
|
4160
|
+
default: false,
|
|
4161
|
+
ui: {
|
|
4162
|
+
tab: "tasks",
|
|
4163
|
+
group: "Subagents",
|
|
4164
|
+
label: "Soft Request Budget Notice",
|
|
4165
|
+
description:
|
|
4166
|
+
"Inject one steering notice when a subagent crosses its soft request budget. Off by default; enabling it asks the child to wrap up before the 1.5x graceful abort guard.",
|
|
4167
|
+
},
|
|
4168
|
+
},
|
|
4169
|
+
|
|
4142
4170
|
"task.disabledAgents": {
|
|
4143
4171
|
type: "array",
|
|
4144
4172
|
default: [] as string[],
|
|
@@ -4930,17 +4958,19 @@ export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boo
|
|
|
4930
4958
|
? boolean
|
|
4931
4959
|
: Schema[P] extends { type: "string" }
|
|
4932
4960
|
? string | undefined
|
|
4933
|
-
: Schema[P] extends { type: "number" }
|
|
4934
|
-
? number
|
|
4935
|
-
: Schema[P] extends { type: "
|
|
4936
|
-
?
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
: Schema[P] extends { type: "
|
|
4961
|
+
: Schema[P] extends { type: "number"; default: undefined }
|
|
4962
|
+
? number | undefined
|
|
4963
|
+
: Schema[P] extends { type: "number" }
|
|
4964
|
+
? number
|
|
4965
|
+
: Schema[P] extends { type: "enum"; values: infer V }
|
|
4966
|
+
? V extends readonly string[]
|
|
4967
|
+
? V[number]
|
|
4968
|
+
: never
|
|
4969
|
+
: Schema[P] extends { type: "array"; default: infer D }
|
|
4942
4970
|
? D
|
|
4943
|
-
:
|
|
4971
|
+
: Schema[P] extends { type: "record"; default: infer D }
|
|
4972
|
+
? D
|
|
4973
|
+
: never;
|
|
4944
4974
|
|
|
4945
4975
|
/** Get the default value for a setting path */
|
|
4946
4976
|
export function getDefault<P extends SettingPath>(path: P): SettingValue<P> {
|
|
@@ -5002,7 +5032,7 @@ export interface CompactionSettings {
|
|
|
5002
5032
|
strategy: "context-full" | "handoff" | "shake" | "snapcompact" | "off";
|
|
5003
5033
|
thresholdPercent: number;
|
|
5004
5034
|
thresholdTokens: number;
|
|
5005
|
-
reserveTokens: number;
|
|
5035
|
+
reserveTokens: number | undefined;
|
|
5006
5036
|
keepRecentTokens: number;
|
|
5007
5037
|
midTurnEnabled: boolean;
|
|
5008
5038
|
handoffSaveToDisk: boolean;
|
|
@@ -180,7 +180,8 @@ function resolvePreviewEdits(args: {
|
|
|
180
180
|
}): readonly Edit[] {
|
|
181
181
|
const { section, absolutePath, normalized, snapshots, expected, liveMatches, edits } = args;
|
|
182
182
|
if (!hasBlockEdit(edits)) return edits;
|
|
183
|
-
const baseText =
|
|
183
|
+
const baseText =
|
|
184
|
+
expected === undefined || liveMatches ? normalized : snapshots.byHashExact(absolutePath, expected)?.text;
|
|
184
185
|
if (baseText === undefined) {
|
|
185
186
|
throw createMismatchError(section, absolutePath, normalized, snapshots, expected ?? "");
|
|
186
187
|
}
|
|
@@ -199,7 +200,17 @@ function applyPreviewEdits(args: {
|
|
|
199
200
|
if (!options.skipHashValidation && expected === undefined) {
|
|
200
201
|
throw new Error(missingSnapshotTagMessage(section.path));
|
|
201
202
|
}
|
|
202
|
-
|
|
203
|
+
// A 16-bit tag can collide across two different file states, so hash
|
|
204
|
+
// equality alone does not prove the live text IS the snapshot the model's
|
|
205
|
+
// anchors were minted against (mirrors Patcher's apply-time guard). When
|
|
206
|
+
// the store retains text for the tag, require it to be unambiguous and
|
|
207
|
+
// byte-identical to live; otherwise fall through to recovery/reject below
|
|
208
|
+
// exactly as if the hash had not matched.
|
|
209
|
+
const liveMatches =
|
|
210
|
+
expected !== undefined &&
|
|
211
|
+
computeFileHash(normalized) === expected &&
|
|
212
|
+
(snapshots.byHash(absolutePath, expected) === null ||
|
|
213
|
+
snapshots.byHashExact(absolutePath, expected)?.text === normalized);
|
|
203
214
|
const edits = parsePreviewEdits(section, options.streaming);
|
|
204
215
|
const resolved = resolvePreviewEdits({ section, absolutePath, normalized, snapshots, expected, liveMatches, edits });
|
|
205
216
|
if (options.skipHashValidation || expected === undefined || liveMatches) return applyEdits(normalized, resolved);
|
package/src/edit/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { prompt } from "@oh-my-pi/pi-utils";
|
|
|
7
7
|
import {
|
|
8
8
|
createLspWritethrough,
|
|
9
9
|
type FileDiagnosticsResult,
|
|
10
|
+
flushLspWritethroughBatch,
|
|
10
11
|
type WritethroughCallback,
|
|
11
12
|
type WritethroughDeferredHandle,
|
|
12
13
|
writethroughNoop,
|
|
@@ -129,6 +130,8 @@ async function executeApplyPatchPerFile(
|
|
|
129
130
|
run: (batchRequest: LspBatchRequest | undefined) => Promise<AgentToolResult<EditToolDetails>>;
|
|
130
131
|
}[],
|
|
131
132
|
outerBatchRequest: LspBatchRequest | undefined,
|
|
133
|
+
cwd: string,
|
|
134
|
+
signal: AbortSignal | undefined,
|
|
132
135
|
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
133
136
|
): Promise<AgentToolResult<EditToolDetails, TInput>> {
|
|
134
137
|
if (fileEntries.length === 1) {
|
|
@@ -138,10 +141,15 @@ async function executeApplyPatchPerFile(
|
|
|
138
141
|
|
|
139
142
|
const perFileResults: EditToolPerFileResult[] = [];
|
|
140
143
|
const contentTexts: string[] = [];
|
|
144
|
+
let hasError = false;
|
|
141
145
|
|
|
142
146
|
for (let i = 0; i < fileEntries.length; i++) {
|
|
143
147
|
const { path, run } = fileEntries[i];
|
|
144
148
|
const isLast = i === fileEntries.length - 1;
|
|
149
|
+
// Per-file writes join the outer LSP write batch; only the last entry
|
|
150
|
+
// flushes it, so cross-file writes coalesce into a single
|
|
151
|
+
// format+diagnostics pass. The failure path below flushes explicitly
|
|
152
|
+
// when the loop stops early.
|
|
145
153
|
const batchRequest: LspBatchRequest | undefined = outerBatchRequest
|
|
146
154
|
? { id: outerBatchRequest.id, flush: isLast && outerBatchRequest.flush }
|
|
147
155
|
: undefined;
|
|
@@ -169,6 +177,36 @@ async function executeApplyPatchPerFile(
|
|
|
169
177
|
const displayErrorText = err instanceof HashlineMismatchError ? err.displayMessage : undefined;
|
|
170
178
|
perFileResults.push({ path, diff: "", isError: true, errorText, displayErrorText });
|
|
171
179
|
contentTexts.push(`Error editing ${path}: ${errorText}`);
|
|
180
|
+
hasError = true;
|
|
181
|
+
// Later entries were authored assuming this file's post-state; a
|
|
182
|
+
// partial cascade after failure typically compounds damage. Stop
|
|
183
|
+
// here, report applied vs. skipped, and let the caller re-issue
|
|
184
|
+
// only the failed and unapplied files. Matches
|
|
185
|
+
// `executeSinglePathEntries` semantics.
|
|
186
|
+
if (i > 0) {
|
|
187
|
+
const appliedPaths = fileEntries
|
|
188
|
+
.slice(0, i)
|
|
189
|
+
.map(e => e.path)
|
|
190
|
+
.join(", ");
|
|
191
|
+
contentTexts.push(`Files already applied: ${appliedPaths}.`);
|
|
192
|
+
}
|
|
193
|
+
if (i + 1 < fileEntries.length) {
|
|
194
|
+
const skippedPaths = fileEntries
|
|
195
|
+
.slice(i + 1)
|
|
196
|
+
.map(e => e.path)
|
|
197
|
+
.join(", ");
|
|
198
|
+
contentTexts.push(
|
|
199
|
+
`Files NOT applied: ${skippedPaths}; re-read the affected files and re-issue only the failed and unapplied files.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
// Stopping early skips the last-entry flush above; finalize the
|
|
203
|
+
// already-written files so an intervening failure cannot leave them
|
|
204
|
+
// sitting in an unfinalized LSP write batch (mirrors the delete-path
|
|
205
|
+
// flush in executePatchSingle).
|
|
206
|
+
if (outerBatchRequest?.flush) {
|
|
207
|
+
await flushLspWritethroughBatch(outerBatchRequest.id, cwd, signal);
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
172
210
|
}
|
|
173
211
|
|
|
174
212
|
// Emit partial result after each file so UI shows progressive completion
|
|
@@ -197,6 +235,10 @@ async function executeApplyPatchPerFile(
|
|
|
197
235
|
firstChangedLine: perFileResults.find(r => r.firstChangedLine)?.firstChangedLine,
|
|
198
236
|
perFileResults,
|
|
199
237
|
}),
|
|
238
|
+
// Any per-file failure marks the aggregate result as an error so the
|
|
239
|
+
// agent loop and renderer take the error branch instead of treating
|
|
240
|
+
// a mixed partial application as a successful edit.
|
|
241
|
+
...(hasError ? { isError: true } : {}),
|
|
200
242
|
};
|
|
201
243
|
}
|
|
202
244
|
|
|
@@ -204,7 +246,9 @@ async function executeSinglePathEntries(
|
|
|
204
246
|
path: string,
|
|
205
247
|
runs: ((batchRequest: LspBatchRequest | undefined) => Promise<AgentToolResult<EditToolDetails>>)[],
|
|
206
248
|
outerBatchRequest: LspBatchRequest | undefined,
|
|
207
|
-
onUpdate
|
|
249
|
+
onUpdate: ((partialResult: AgentToolResult<EditToolDetails, TInput>) => void) | undefined,
|
|
250
|
+
cwd: string,
|
|
251
|
+
signal: AbortSignal | undefined,
|
|
208
252
|
): Promise<AgentToolResult<EditToolDetails, TInput>> {
|
|
209
253
|
if (runs.length === 1) {
|
|
210
254
|
return runs[0](outerBatchRequest);
|
|
@@ -213,7 +257,7 @@ async function executeSinglePathEntries(
|
|
|
213
257
|
const contentTexts: string[] = [];
|
|
214
258
|
const diffTexts: string[] = [];
|
|
215
259
|
let firstChangedLine: number | undefined;
|
|
216
|
-
let
|
|
260
|
+
let hasError = false;
|
|
217
261
|
let metadataPath: string | undefined;
|
|
218
262
|
let hasFirstOldText = false;
|
|
219
263
|
let firstOldText: string | undefined;
|
|
@@ -264,10 +308,13 @@ async function executeSinglePathEntries(
|
|
|
264
308
|
`; re-read the file and re-issue only the failed and unapplied entries.`,
|
|
265
309
|
);
|
|
266
310
|
}
|
|
267
|
-
|
|
311
|
+
hasError = true;
|
|
268
312
|
// Stop at the first failure: later entries were authored against
|
|
269
313
|
// line numbers/content that assumed this entry succeeded, and
|
|
270
314
|
// applying them after a failure compounds the damage.
|
|
315
|
+
if (outerBatchRequest?.flush) {
|
|
316
|
+
await flushLspWritethroughBatch(outerBatchRequest.id, cwd, signal);
|
|
317
|
+
}
|
|
271
318
|
break;
|
|
272
319
|
}
|
|
273
320
|
|
|
@@ -278,7 +325,7 @@ async function executeSinglePathEntries(
|
|
|
278
325
|
diff: diffTexts.join("\n"),
|
|
279
326
|
firstChangedLine,
|
|
280
327
|
},
|
|
281
|
-
...(
|
|
328
|
+
...(hasError ? { isError: true } : {}),
|
|
282
329
|
});
|
|
283
330
|
}
|
|
284
331
|
}
|
|
@@ -300,7 +347,7 @@ async function executeSinglePathEntries(
|
|
|
300
347
|
// renderer takes the error branch instead of falling through to the
|
|
301
348
|
// streaming-edit preview (which displays the *proposed* diff and looks
|
|
302
349
|
// indistinguishable from success).
|
|
303
|
-
...(
|
|
350
|
+
...(hasError ? { isError: true } : {}),
|
|
304
351
|
};
|
|
305
352
|
}
|
|
306
353
|
|
|
@@ -498,11 +545,14 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
498
545
|
batchRequest: br,
|
|
499
546
|
allowFuzzy: tool.#allowFuzzy,
|
|
500
547
|
fuzzyThreshold: tool.#fuzzyThreshold,
|
|
548
|
+
// The JSON grammar has no `*** Update File`; its `op: "create"`
|
|
549
|
+
// doubles as the documented full-file overwrite (patch.md <avoid>).
|
|
550
|
+
allowCreateOverwrite: true,
|
|
501
551
|
writethrough: tool.#writethrough,
|
|
502
552
|
beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
|
|
503
553
|
}),
|
|
504
554
|
);
|
|
505
|
-
return executeSinglePathEntries(path, runs, batchRequest, onUpdate);
|
|
555
|
+
return executeSinglePathEntries(path, runs, batchRequest, onUpdate, tool.session.cwd, signal);
|
|
506
556
|
},
|
|
507
557
|
},
|
|
508
558
|
apply_patch: {
|
|
@@ -542,7 +592,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
542
592
|
}),
|
|
543
593
|
};
|
|
544
594
|
});
|
|
545
|
-
return executeApplyPatchPerFile(perFile, batchRequest, onUpdate);
|
|
595
|
+
return executeApplyPatchPerFile(perFile, batchRequest, tool.session.cwd, signal, onUpdate);
|
|
546
596
|
},
|
|
547
597
|
},
|
|
548
598
|
hashline: {
|
|
@@ -591,7 +641,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
591
641
|
beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
|
|
592
642
|
}),
|
|
593
643
|
);
|
|
594
|
-
return executeSinglePathEntries(path, runs, batchRequest, onUpdate);
|
|
644
|
+
return executeSinglePathEntries(path, runs, batchRequest, onUpdate, tool.session.cwd, signal);
|
|
595
645
|
},
|
|
596
646
|
},
|
|
597
647
|
}[this.mode];
|
package/src/edit/modes/patch.ts
CHANGED
|
@@ -96,6 +96,14 @@ export interface ApplyPatchOptions {
|
|
|
96
96
|
fuzzyThreshold?: number;
|
|
97
97
|
allowFuzzy?: boolean;
|
|
98
98
|
fs?: FileSystem;
|
|
99
|
+
/**
|
|
100
|
+
* Permit `op: "create"` to replace an existing file (full-file overwrite).
|
|
101
|
+
* The JSON `patch` edit mode sanctions create-as-overwrite for major
|
|
102
|
+
* restructures (see prompts/tools/patch.md); the Codex `apply_patch`
|
|
103
|
+
* envelope documents `*** Add File` as strictly non-overwriting and must
|
|
104
|
+
* leave this unset.
|
|
105
|
+
*/
|
|
106
|
+
allowCreateOverwrite?: boolean;
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -455,7 +463,8 @@ function trimCommonContext(oldLines: string[], newLines: string[]): HunkVariant
|
|
|
455
463
|
}
|
|
456
464
|
|
|
457
465
|
function collapseConsecutiveSharedLines(oldLines: string[], newLines: string[]): HunkVariant | undefined {
|
|
458
|
-
const
|
|
466
|
+
const newSet = new Set(newLines);
|
|
467
|
+
const shared = new Set(oldLines.filter(line => newSet.has(line)));
|
|
459
468
|
const collapse = (lines: string[]): string[] => {
|
|
460
469
|
const out: string[] = [];
|
|
461
470
|
let i = 0;
|
|
@@ -480,31 +489,32 @@ function collapseConsecutiveSharedLines(oldLines: string[], newLines: string[]):
|
|
|
480
489
|
}
|
|
481
490
|
|
|
482
491
|
function collapseRepeatedBlocks(oldLines: string[], newLines: string[]): HunkVariant | undefined {
|
|
483
|
-
const
|
|
492
|
+
const newSet = new Set(newLines);
|
|
493
|
+
const shared = new Set(oldLines.filter(line => newSet.has(line)));
|
|
484
494
|
const collapse = (lines: string[]): string[] => {
|
|
485
495
|
const output = [...lines];
|
|
486
496
|
let changed = false;
|
|
487
497
|
let i = 0;
|
|
488
498
|
while (i < output.length) {
|
|
489
499
|
let collapsed = false;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
500
|
+
// Only blocks whose lines are all shared are collapsible; if the first line
|
|
501
|
+
// is not shared no size can match, so skip the size search entirely.
|
|
502
|
+
if (shared.has(output[i])) {
|
|
503
|
+
for (let size = Math.floor((output.length - i) / 2); size >= 2; size--) {
|
|
504
|
+
let same = true;
|
|
505
|
+
for (let idx = 0; idx < size; idx++) {
|
|
506
|
+
if (output[i + idx] !== output[i + size + idx] || !shared.has(output[i + idx])) {
|
|
507
|
+
same = false;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (same) {
|
|
512
|
+
output.splice(i + size, size);
|
|
513
|
+
changed = true;
|
|
514
|
+
collapsed = true;
|
|
499
515
|
break;
|
|
500
516
|
}
|
|
501
517
|
}
|
|
502
|
-
if (same) {
|
|
503
|
-
output.splice(i + size, size);
|
|
504
|
-
changed = true;
|
|
505
|
-
collapsed = true;
|
|
506
|
-
break;
|
|
507
|
-
}
|
|
508
518
|
}
|
|
509
519
|
if (!collapsed) {
|
|
510
520
|
i++;
|
|
@@ -1479,6 +1489,7 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
|
|
|
1479
1489
|
fs = defaultFileSystem,
|
|
1480
1490
|
fuzzyThreshold = DEFAULT_FUZZY_THRESHOLD,
|
|
1481
1491
|
allowFuzzy = true,
|
|
1492
|
+
allowCreateOverwrite = false,
|
|
1482
1493
|
} = options;
|
|
1483
1494
|
|
|
1484
1495
|
const resolvePath = (p: string): string => resolveToCwd(p, cwd);
|
|
@@ -1490,6 +1501,14 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
|
|
|
1490
1501
|
if (destPath === absolutePath) {
|
|
1491
1502
|
throw new ApplyPatchError("rename path is the same as source path");
|
|
1492
1503
|
}
|
|
1504
|
+
// The `*** Move to` / rename contract is strictly non-overwriting:
|
|
1505
|
+
// reject before the update path reads or writes anything, so both
|
|
1506
|
+
// source and pre-existing destination remain untouched. Callers who
|
|
1507
|
+
// really need to replace the destination must delete it in an
|
|
1508
|
+
// earlier hunk.
|
|
1509
|
+
if (await fs.exists(destPath)) {
|
|
1510
|
+
throw new ApplyPatchError(`Cannot rename ${input.path} to ${input.rename}: destination already exists.`);
|
|
1511
|
+
}
|
|
1493
1512
|
}
|
|
1494
1513
|
|
|
1495
1514
|
// Handle CREATE operation
|
|
@@ -1497,6 +1516,17 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
|
|
|
1497
1516
|
if (!input.diff) {
|
|
1498
1517
|
throw new ApplyPatchError("Create operation requires diff (file content)");
|
|
1499
1518
|
}
|
|
1519
|
+
// The `*** Add File` contract of the apply_patch envelope is strictly
|
|
1520
|
+
// non-overwriting: reject before mkdir/write so pre-existing content
|
|
1521
|
+
// stays intact and the caller can re-issue as an explicit
|
|
1522
|
+
// `*** Update File` (or a delete+add pair) if overwrite is genuinely
|
|
1523
|
+
// intended. The JSON `patch` mode opts out via `allowCreateOverwrite`,
|
|
1524
|
+
// where `op: "create"` doubles as a sanctioned full-file overwrite.
|
|
1525
|
+
if (!allowCreateOverwrite && (await fs.exists(absolutePath))) {
|
|
1526
|
+
throw new ApplyPatchError(
|
|
1527
|
+
`Cannot create ${input.path}: file already exists. Use *** Update File to modify it in place.`,
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1500
1530
|
// Strip + prefixes if present (handles diffs formatted as additions)
|
|
1501
1531
|
const normalizedContent = normalizeCreateContent(input.diff);
|
|
1502
1532
|
const content = normalizedContent.endsWith("\n") ? normalizedContent : `${normalizedContent}\n`;
|
|
@@ -1602,7 +1632,7 @@ export async function previewPatch(input: PatchInput, options: ApplyPatchOptions
|
|
|
1602
1632
|
export async function computePatchDiff(
|
|
1603
1633
|
input: PatchInput,
|
|
1604
1634
|
cwd: string,
|
|
1605
|
-
options?: { fuzzyThreshold?: number; allowFuzzy?: boolean },
|
|
1635
|
+
options?: { fuzzyThreshold?: number; allowFuzzy?: boolean; allowCreateOverwrite?: boolean },
|
|
1606
1636
|
): Promise<
|
|
1607
1637
|
| {
|
|
1608
1638
|
diff: string;
|
|
@@ -1617,6 +1647,7 @@ export async function computePatchDiff(
|
|
|
1617
1647
|
cwd,
|
|
1618
1648
|
fuzzyThreshold: options?.fuzzyThreshold,
|
|
1619
1649
|
allowFuzzy: options?.allowFuzzy,
|
|
1650
|
+
allowCreateOverwrite: options?.allowCreateOverwrite,
|
|
1620
1651
|
});
|
|
1621
1652
|
const oldContent = result.change.oldContent ?? "";
|
|
1622
1653
|
const newContent = result.change.newContent ?? "";
|
|
@@ -1656,6 +1687,8 @@ export interface ExecutePatchSingleOptions {
|
|
|
1656
1687
|
batchRequest?: LspBatchRequest;
|
|
1657
1688
|
allowFuzzy: boolean;
|
|
1658
1689
|
fuzzyThreshold: number;
|
|
1690
|
+
/** See {@link ApplyPatchOptions.allowCreateOverwrite}; set by the JSON `patch` mode only. */
|
|
1691
|
+
allowCreateOverwrite?: boolean;
|
|
1659
1692
|
writethrough: WritethroughCallback;
|
|
1660
1693
|
beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
1661
1694
|
}
|
|
@@ -1763,6 +1796,7 @@ export async function executePatchSingle(
|
|
|
1763
1796
|
batchRequest,
|
|
1764
1797
|
allowFuzzy,
|
|
1765
1798
|
fuzzyThreshold,
|
|
1799
|
+
allowCreateOverwrite,
|
|
1766
1800
|
writethrough,
|
|
1767
1801
|
beginDeferredDiagnosticsForPath,
|
|
1768
1802
|
} = options;
|
|
@@ -1808,6 +1842,7 @@ export async function executePatchSingle(
|
|
|
1808
1842
|
fs: patchFileSystem,
|
|
1809
1843
|
fuzzyThreshold,
|
|
1810
1844
|
allowFuzzy,
|
|
1845
|
+
allowCreateOverwrite,
|
|
1811
1846
|
});
|
|
1812
1847
|
|
|
1813
1848
|
// Post-write verification: only meaningful for in-place updates where the
|
package/src/edit/streaming.ts
CHANGED
|
@@ -207,19 +207,18 @@ function groupApplyPatchEntriesByPath(entries: readonly ApplyPatchEntry[]): Map<
|
|
|
207
207
|
* `create` op), otherwise an empty string (grammar-only payloads).
|
|
208
208
|
*/
|
|
209
209
|
function extractAddedLines(text: string, fallbackToWhole: boolean): string {
|
|
210
|
-
|
|
210
|
+
const added: string[] = [];
|
|
211
211
|
let lineStart = 0;
|
|
212
212
|
while (lineStart <= text.length) {
|
|
213
213
|
let lineEnd = text.indexOf("\n", lineStart);
|
|
214
214
|
if (lineEnd === -1) lineEnd = text.length;
|
|
215
215
|
if (text.charCodeAt(lineStart) === 43 /* + */ && !text.startsWith("+++ ", lineStart)) {
|
|
216
|
-
|
|
217
|
-
added = added === undefined ? line : `${added}\n${line}`;
|
|
216
|
+
added.push(text.slice(lineStart + 1, lineEnd));
|
|
218
217
|
}
|
|
219
218
|
lineStart = lineEnd + 1;
|
|
220
219
|
}
|
|
221
|
-
if (added ===
|
|
222
|
-
return added;
|
|
220
|
+
if (added.length === 0) return fallbackToWhole ? text : "";
|
|
221
|
+
return added.join("\n");
|
|
223
222
|
}
|
|
224
223
|
|
|
225
224
|
/**
|
|
@@ -394,7 +393,9 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
|
|
|
394
393
|
const result = await computePatchDiff(
|
|
395
394
|
{ path: args.path, op: first.op ?? "update", rename: first.rename, diff: first.diff },
|
|
396
395
|
ctx.cwd,
|
|
397
|
-
|
|
396
|
+
// Match the apply path: JSON-mode `op: "create"` is a sanctioned
|
|
397
|
+
// full-file overwrite, so the preview must not reject it either.
|
|
398
|
+
{ fuzzyThreshold: ctx.fuzzyThreshold, allowFuzzy: ctx.allowFuzzy, allowCreateOverwrite: true },
|
|
398
399
|
);
|
|
399
400
|
ctx.signal.throwIfAborted();
|
|
400
401
|
return [toPerFilePreview(args.path, result)];
|
|
@@ -219,6 +219,48 @@ describe("runEvalAgent", () => {
|
|
|
219
219
|
expect(runSpy.mock.calls[0]?.[0].agent.name).toBe("reviewer");
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
it("honors task.maxRecursionDepth on top of the hard eval ceiling", async () => {
|
|
223
|
+
mockAgents();
|
|
224
|
+
const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
|
|
225
|
+
|
|
226
|
+
// task.maxRecursionDepth=0 means "no spawning at all" — even depth 0 (the
|
|
227
|
+
// top-level agent) must be blocked, matching canSpawnAtDepth().
|
|
228
|
+
await expect(
|
|
229
|
+
runEvalAgent(
|
|
230
|
+
{ prompt: "hello" },
|
|
231
|
+
{
|
|
232
|
+
session: makeSession({
|
|
233
|
+
settings: Settings.isolated({
|
|
234
|
+
"async.enabled": false,
|
|
235
|
+
"task.isolation.mode": "none",
|
|
236
|
+
"task.maxRecursionDepth": 0,
|
|
237
|
+
}),
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
),
|
|
241
|
+
).rejects.toThrow("maximum depth is 0");
|
|
242
|
+
|
|
243
|
+
// task.maxRecursionDepth=1 ("Single") lets the top spawn but a depth-1
|
|
244
|
+
// subagent cannot spawn further — even though the hard ceiling is 3.
|
|
245
|
+
await expect(
|
|
246
|
+
runEvalAgent(
|
|
247
|
+
{ prompt: "hello" },
|
|
248
|
+
{
|
|
249
|
+
session: makeSession({
|
|
250
|
+
depth: 1,
|
|
251
|
+
settings: Settings.isolated({
|
|
252
|
+
"async.enabled": false,
|
|
253
|
+
"task.isolation.mode": "none",
|
|
254
|
+
"task.maxRecursionDepth": 1,
|
|
255
|
+
}),
|
|
256
|
+
}),
|
|
257
|
+
},
|
|
258
|
+
),
|
|
259
|
+
).rejects.toThrow("maximum depth is 1");
|
|
260
|
+
|
|
261
|
+
expect(runSpy).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
|
|
222
264
|
it("throws instead of spawning from plan mode", async () => {
|
|
223
265
|
mockAgents();
|
|
224
266
|
const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
|
|
@@ -234,7 +276,19 @@ describe("runEvalAgent", () => {
|
|
|
234
276
|
const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
|
|
235
277
|
const abortController = new AbortController();
|
|
236
278
|
const schema = { type: "object", properties: { ok: { type: "boolean" } } };
|
|
237
|
-
const session = makeSession({
|
|
279
|
+
const session = makeSession({
|
|
280
|
+
depth: 2,
|
|
281
|
+
activeModel: "p/current",
|
|
282
|
+
modelString: "p/fallback",
|
|
283
|
+
settings: Settings.isolated({
|
|
284
|
+
"async.enabled": false,
|
|
285
|
+
"task.isolation.mode": "none",
|
|
286
|
+
"task.enableLsp": true,
|
|
287
|
+
// Default task.maxRecursionDepth is 2, which would now (correctly)
|
|
288
|
+
// block depth=2 — widen it so the test still exercises depth=2.
|
|
289
|
+
"task.maxRecursionDepth": -1,
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
238
292
|
|
|
239
293
|
await runEvalAgent(
|
|
240
294
|
{ prompt: " hello ", label: "My Agent", model: "p/override", schema },
|
|
@@ -249,10 +303,12 @@ describe("runEvalAgent", () => {
|
|
|
249
303
|
expect(firstOptions.signal).toBe(abortController.signal);
|
|
250
304
|
expect(firstOptions.parentActiveModelPattern).toBe("p/current");
|
|
251
305
|
expect(firstOptions.outputSchema).toBe(schema);
|
|
306
|
+
expect(firstOptions.outputSchemaOverridesAgent).toBe(true);
|
|
252
307
|
expect(firstOptions.assignment).toBe("hello");
|
|
253
308
|
expect(firstOptions.description).toBe("My Agent");
|
|
254
309
|
expect(firstOptions.modelOverride).toEqual(["p/override"]);
|
|
255
310
|
expect(secondOptions.outputSchema).toBeUndefined();
|
|
311
|
+
expect(secondOptions.outputSchemaOverridesAgent).toBeUndefined();
|
|
256
312
|
});
|
|
257
313
|
|
|
258
314
|
it("forces LSP off for bridge subagents even when task.enableLsp is on", async () => {
|
package/src/eval/agent-bridge.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from "../task/isolation-runner";
|
|
26
26
|
import { AgentOutputManager } from "../task/output-manager";
|
|
27
27
|
import { resolveSpawnPolicy } from "../task/spawn-policy";
|
|
28
|
-
import type
|
|
28
|
+
import { type AgentDefinition, type AgentProgress, canSpawnAtDepth, type SingleResult } from "../task/types";
|
|
29
29
|
import { type NestedRepoPatch, parseIsolationMode } from "../task/worktree";
|
|
30
30
|
import type { ToolSession } from "../tools";
|
|
31
31
|
import { ToolError } from "../tools/tool-errors";
|
|
@@ -37,7 +37,11 @@ import "../tools/review";
|
|
|
37
37
|
/** Synthetic bridge name reserved for the `agent()` helper across both runtimes. */
|
|
38
38
|
export const EVAL_AGENT_BRIDGE_NAME = "__agent__";
|
|
39
39
|
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Hard recursion ceiling for eval-driven subagents. The user setting
|
|
42
|
+
* `task.maxRecursionDepth` is honored on top of this — whichever is tighter
|
|
43
|
+
* wins, so a maintainer-friendly cap can't get raised by a user setting.
|
|
44
|
+
*/
|
|
41
45
|
export const EVAL_AGENT_MAX_DEPTH = 3;
|
|
42
46
|
|
|
43
47
|
const DEFAULT_AGENT_LABEL = "EvalAgent";
|
|
@@ -131,9 +135,15 @@ function parseAgentArgs(args: unknown): EvalAgentArgs {
|
|
|
131
135
|
|
|
132
136
|
function assertDepthAllowed(session: ToolSession): void {
|
|
133
137
|
const taskDepth = session.taskDepth ?? 0;
|
|
134
|
-
|
|
138
|
+
// Honor the user's `task.maxRecursionDepth` (mirroring the task tool's gate
|
|
139
|
+
// in tools/index.ts) but never above the hard ceiling. `< 0` means
|
|
140
|
+
// "Unlimited" in the same schema `canSpawnAtDepth` reads, so it falls back
|
|
141
|
+
// to the hard ceiling instead of going past it.
|
|
142
|
+
const settingMax = session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
143
|
+
const effectiveMax = settingMax < 0 ? EVAL_AGENT_MAX_DEPTH : Math.min(settingMax, EVAL_AGENT_MAX_DEPTH);
|
|
144
|
+
if (!canSpawnAtDepth(effectiveMax, taskDepth)) {
|
|
135
145
|
throw new ToolError(
|
|
136
|
-
`agent() cannot spawn another agent at task depth ${taskDepth}; maximum depth is ${EVAL_AGENT_MAX_DEPTH}.`,
|
|
146
|
+
`agent() cannot spawn another agent at task depth ${taskDepth}; maximum depth is ${effectiveMax} (task.maxRecursionDepth=${settingMax}, hard ceiling=${EVAL_AGENT_MAX_DEPTH}).`,
|
|
137
147
|
);
|
|
138
148
|
}
|
|
139
149
|
}
|
|
@@ -368,7 +378,7 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
368
378
|
modelOverride,
|
|
369
379
|
parentActiveModelPattern,
|
|
370
380
|
thinkingLevel: effectiveAgent.thinkingLevel,
|
|
371
|
-
|
|
381
|
+
...(structured ? { outputSchema: parsed.schema, outputSchemaOverridesAgent: true } : {}),
|
|
372
382
|
sessionFile,
|
|
373
383
|
persistArtifacts: Boolean(sessionFile),
|
|
374
384
|
artifactsDir,
|