FN-8616: make agent tool-output limits configurable

Expose the shared agent tool-output budget as a scoped operator setting with an explicit no-limit option.

- Resolve global and project output caps with a safe finite default and zero sentinel.
- Propagate configured budgets through PI and plugin runtime tool wrappers.
- Add settings controls, localized labels, documentation, and regression coverage.

Files changed:
 .changeset/fn-8616-tool-output-budget-setting.md   |  7 ++++
 docs/agents.md                                     |  4 +-
 docs/settings-reference.md                         |  1 +
 .../core/src/__tests__/tool-output-budget.test.ts  | 23 ++++++++---
 packages/core/src/index.gate.ts                    |  2 +
 packages/core/src/index.ts                         |  2 +
 packages/core/src/settings-schema.ts               | 12 ++++++
 packages/core/src/tool-output-budget.ts            | 31 +++++++++++++--
 packages/core/src/types/settings-scope.ts          |  8 ++++
 .../app/components/settings/save-split.ts          |  1 +
 .../sections/GlobalGeneralSection.search.ts        | 20 ++++++++++
 .../settings/sections/GlobalGeneralSection.tsx     | 26 ++++++++++++
 ...lobalGeneralSection.tool-output-budget.test.tsx | 46 ++++++++++++++++++++++
 .../settings-default-descriptions.test.tsx         |  1 +
 .../src/__tests__/agent-session-helpers.test.ts    | 20 ++++++++++
 .../src/__tests__/runtime-resolution.test.ts       | 15 +++++++
 .../__tests__/tool-output-budget-wrapper.test.ts   | 45 ++++++++++++++++-----
 packages/engine/src/agent-runtime.ts               |  2 +
 packages/engine/src/agent-session-helpers.ts       | 18 +++++++--
 packages/engine/src/pi.ts                          | 29 ++++++++++----
 packages/engine/src/runtime-resolution.ts          | 10 ++++-
 packages/i18n/locales/en/app.json                  |  4 ++
 packages/i18n/locales/es/app.json                  |  6 ++-
 packages/i18n/locales/fr/app.json                  |  6 ++-
 packages/i18n/locales/ko/app.json                  |  6 ++-
 packages/i18n/locales/zh-CN/app.json               |  6 ++-
 packages/i18n/locales/zh-TW/app.json               |  6 ++-
 packages/i18n/src/resources.d.ts                   |  4 ++
 28 files changed, 323 insertions(+), 38 deletions(-)

Fusion-Task-Id: FN-8616

Fusion-Task-Lineage: 3ca99a61-d6ae-48ff-98d2-f14a153aa2b7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-26 16:16:27 -07:00
parent d3002d1453
commit 2bb8537352
28 changed files with 323 additions and 38 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Add a Settings control for the agent tool-output limit, including a no-limit option.
category: feature
dev: `agentToolOutputMaxChars` accepts `0` as the unlimited sentinel.

View File

@@ -54,7 +54,9 @@ Every engine-injected tool result has a **16,000-character** budget across the c
When a result overflows, Fusion reserves the canonical marker (`[Tool output truncated to fit the context budget; narrow your query or use limit/offset for more.]`) inside that budget. Text is allocated deterministically in document order: complete earlier text blocks are retained while room remains, the first block that no longer fits receives the retained prefix and marker, and later text blocks become empty strings. Results already ending in that exact marker are not marked again, making the clamp idempotent; pre-existing markers from other tool-specific clamps remain ordinary counted text. When a result overflows, Fusion reserves the canonical marker (`[Tool output truncated to fit the context budget; narrow your query or use limit/offset for more.]`) inside that budget. Text is allocated deterministically in document order: complete earlier text blocks are retained while room remains, the first block that no longer fits receives the retained prefix and marker, and later text blocks become empty strings. Results already ending in that exact marker are not marked again, making the clamp idempotent; pre-existing markers from other tool-specific clamps remain ordinary counted text.
`TOOL_OUTPUT_BUDGET_OVERRIDES` may set a named tool to a different **finite positive integer** budget, either larger or smaller. There is no opt-out, sentinel, `Infinity`, zero, or negative budget: a missing entry uses the default, and invalid entries throw in development/test or fall back to the default in production. Pi applies the clamp as its outermost tool wrapper, while non-pi plugin runtimes apply it once in their custom-tool wrapper. Operators configure this shared budget with the global or project `agentToolOutputMaxChars` setting. An unset, `null`, or invalid value uses the 16,000-character default; a positive integer sets a custom cap; and the explicit `0` sentinel means **no limit**, so Fusion skips this shared clamp entirely. No limit can let one tool result consume the agent context window; it does not disable existing tool-local clamps such as task-list text, web-fetch bytes, or workflow-script output.
`TOOL_OUTPUT_BUDGET_OVERRIDES` may set a named tool to a different **finite positive integer** budget, either larger or smaller, and still wins over a custom shared cap. The unlimited option exists only at the operator-setting level: overrides cannot use a sentinel, `Infinity`, zero, or negative budget. A missing override uses the resolved shared budget, and invalid overrides throw in development/test or fall back to that budget in production. Pi applies the clamp as its outermost tool wrapper, while non-pi plugin runtimes apply it once in their custom-tool wrapper.
### Artifact registry tools ### Artifact registry tools

View File

@@ -160,6 +160,7 @@ Fusion automatically falls back to ntfy's JSON publish format when a notificatio
> Mesh lifecycle note: settings sync is executed by the process-level `PeerExchangeService` started by `fn serve`/`fn dashboard`. `InProcessRuntime` does not instantiate settings-sync mesh services per project. > Mesh lifecycle note: settings sync is executed by the process-level `PeerExchangeService` started by `fn serve`/`fn dashboard`. `InProcessRuntime` does not instantiate settings-sync mesh services per project.
| `dashboardCurrentProjectIdByNode` | `Record<string, string>` | `undefined` | Map of node ID to last-selected project ID. Use key `"local"` for the local node. Persists project context across browser restarts and PWA sessions. | | `dashboardCurrentProjectIdByNode` | `Record<string, string>` | `undefined` | Map of node ID to last-selected project ID. Use key `"local"` for the local node. Persists project context across browser restarts and PWA sessions. |
| `persistAgentToolOutput` | `boolean` | `false` | Controls whether detailed `detail` payloads are persisted for `tool`, `tool_result`, and `tool_error` agent log entries. Tool timeline rows are still recorded by default; verbose tool arguments/results require opting in with `persistAgentToolOutput: true`. | | `persistAgentToolOutput` | `boolean` | `false` | Controls whether detailed `detail` payloads are persisted for `tool`, `tool_result`, and `tool_error` agent log entries. Tool timeline rows are still recorded by default; verbose tool arguments/results require opting in with `persistAgentToolOutput: true`. |
| `agentToolOutputMaxChars` | `number` | `undefined` (16,000 effective) | Global and project scope. A positive integer caps each engine-injected tool result; unset, `null`, or invalid values use the 16,000-character default; `0` explicitly means no limit and skips the shared clamp. This does not disable tool-local output limits. |
| `proactiveTaskChatEnabled` | `boolean` | `false` | Enables engine-authored Task chat updates for step progress, failures, review outcomes, and rollbacks. This global operator preference is off by default and is available in **Settings → Global General**. | | `proactiveTaskChatEnabled` | `boolean` | `false` | Enables engine-authored Task chat updates for step progress, failures, review outcomes, and rollbacks. This global operator preference is off by default and is available in **Settings → Global General**. |
| `persistAgentThinkingLogPermanent` | `boolean` | `false` | Controls whether `thinking`/reasoning rows are persisted for permanent (non-ephemeral) agents. | | `persistAgentThinkingLogPermanent` | `boolean` | `false` | Controls whether `thinking`/reasoning rows are persisted for permanent (non-ephemeral) agents. |
| `persistAgentThinkingLogEphemeral` | `boolean` | `false` | Controls whether `thinking`/reasoning rows are persisted for ephemeral/task-worker/spawned agents. | | `persistAgentThinkingLogEphemeral` | `boolean` | `false` | Controls whether `thinking`/reasoning rows are persisted for ephemeral/task-worker/spawned agents. |

View File

@@ -4,6 +4,7 @@ import {
buildToolOutputTruncationMarker, buildToolOutputTruncationMarker,
clampToolOutputBlocks, clampToolOutputBlocks,
clampToolOutputText, clampToolOutputText,
resolveAgentToolOutputMaxChars,
resolveToolOutputBudget, resolveToolOutputBudget,
} from "../tool-output-budget.js"; } from "../tool-output-budget.js";
@@ -47,12 +48,24 @@ describe("tool output budget", () => {
expect(output.join("")).toHaveLength(maxChars); expect(output.join("")).toHaveLength(maxChars);
}); });
it("resolves only finite positive integer overrides", () => { it("resolves the setting's default, bounded, and explicit unlimited states", () => {
expect(resolveToolOutputBudget("missing", {})).toBe(DEFAULT_TOOL_OUTPUT_MAX_CHARS); for (const value of [undefined, null]) {
expect(resolveToolOutputBudget("large", { large: 20_000 })).toBe(20_000); expect(resolveAgentToolOutputMaxChars({ agentToolOutputMaxChars: value })).toBe(DEFAULT_TOOL_OUTPUT_MAX_CHARS);
expect(resolveToolOutputBudget("small", { small: 10 })).toBe(10); }
expect(resolveAgentToolOutputMaxChars({})).toBe(DEFAULT_TOOL_OUTPUT_MAX_CHARS);
expect(resolveAgentToolOutputMaxChars({ agentToolOutputMaxChars: 0 })).toBeNull();
expect(resolveAgentToolOutputMaxChars({ agentToolOutputMaxChars: 500 })).toBe(500);
for (const value of [-1, 1.5, Number.NaN, Infinity, "500"]) {
expect(resolveAgentToolOutputMaxChars({ agentToolOutputMaxChars: value })).toBe(DEFAULT_TOOL_OUTPUT_MAX_CHARS);
}
});
it("resolves only finite positive integer overrides over a supplied base budget", () => {
expect(resolveToolOutputBudget("missing", {}, 500)).toBe(500);
expect(resolveToolOutputBudget("large", { large: 20_000 }, 500)).toBe(20_000);
expect(resolveToolOutputBudget("small", { small: 10 }, 500)).toBe(10);
for (const value of [Infinity, 0, -1, Number.NaN, null]) { for (const value of [Infinity, 0, -1, Number.NaN, null]) {
expect(() => resolveToolOutputBudget("bad", { bad: value })).toThrow(/finite positive integers/); expect(() => resolveToolOutputBudget("bad", { bad: value }, 500)).toThrow(/finite positive integers/);
} }
}); });
}); });

View File

@@ -107,9 +107,11 @@ export type {
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js"; export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js";
export { export {
DEFAULT_TOOL_OUTPUT_MAX_CHARS, DEFAULT_TOOL_OUTPUT_MAX_CHARS,
TOOL_OUTPUT_UNLIMITED_SETTING_VALUE,
buildToolOutputTruncationMarker, buildToolOutputTruncationMarker,
clampToolOutputText, clampToolOutputText,
clampToolOutputBlocks, clampToolOutputBlocks,
resolveAgentToolOutputMaxChars,
resolveToolOutputBudget, resolveToolOutputBudget,
} from "./tool-output-budget.js"; } from "./tool-output-budget.js";
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js"; export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";

View File

@@ -121,9 +121,11 @@ export * from "./file-scope-classification.js";
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js"; export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js";
export { export {
DEFAULT_TOOL_OUTPUT_MAX_CHARS, DEFAULT_TOOL_OUTPUT_MAX_CHARS,
TOOL_OUTPUT_UNLIMITED_SETTING_VALUE,
buildToolOutputTruncationMarker, buildToolOutputTruncationMarker,
clampToolOutputText, clampToolOutputText,
clampToolOutputBlocks, clampToolOutputBlocks,
resolveAgentToolOutputMaxChars,
resolveToolOutputBudget, resolveToolOutputBudget,
} from "./tool-output-budget.js"; } from "./tool-output-budget.js";
export { export {

View File

@@ -298,6 +298,12 @@ export const DEFAULT_GLOBAL_SETTINGS = {
Verbose tool arguments and results are default-off to reduce persisted log volume and payload exposure. Operators who need saved tool details can explicitly opt in with persistAgentToolOutput: true; tool timeline rows remain logged either way. Verbose tool arguments and results are default-off to reduce persisted log volume and payload exposure. Operators who need saved tool details can explicitly opt in with persistAgentToolOutput: true; tool timeline rows remain logged either way.
*/ */
persistAgentToolOutput: false, persistAgentToolOutput: false,
/*
FNXC:ToolOutputBudget 2026-08-06-16:00:
FN-8616 lets operators raise, lower, or disable the FN-8614 per-result tool-output
budget. Undefined preserves the finite 16,000-character default; only 0 means no limit.
*/
agentToolOutputMaxChars: undefined,
// Task chat remains an operator-directed conversation by default. Enable this // Task chat remains an operator-directed conversation by default. Enable this
// explicitly to add engine-authored lifecycle narration to the transcript. // explicitly to add engine-authored lifecycle narration to the transcript.
proactiveTaskChatEnabled: false, proactiveTaskChatEnabled: false,
@@ -443,6 +449,12 @@ export const DEFAULT_PROJECT_SETTINGS = {
// with this on but auto-merge off, review threads are resolved but the PR is not merged. // with this on but auto-merge off, review threads are resolved but the PR is not merged.
autoResolveReviewComments: true, autoResolveReviewComments: true,
testMode: undefined, testMode: undefined,
/*
FNXC:ToolOutputBudget 2026-08-06-16:00:
Project settings participate in the existing effective-settings merge, allowing a
project-specific tool-output cap or explicit no-limit sentinel to override global policy.
*/
agentToolOutputMaxChars: undefined,
voiceInput: undefined, voiceInput: undefined,
mergeRequestContractShadowEnabled: false, mergeRequestContractShadowEnabled: false,
mergeStrategy: "direct", mergeStrategy: "direct",

View File

@@ -1,14 +1,21 @@
/** Default maximum model-visible characters in one engine-injected tool result. */ /** Default maximum model-visible characters in one engine-injected tool result. */
export const DEFAULT_TOOL_OUTPUT_MAX_CHARS = 16_000; export const DEFAULT_TOOL_OUTPUT_MAX_CHARS = 16_000;
/** Explicit `agentToolOutputMaxChars` value that disables the shared output wrapper. */
export const TOOL_OUTPUT_UNLIMITED_SETTING_VALUE = 0;
const DEFAULT_TRUNCATION_HINT = "narrow your query or use limit/offset for more"; const DEFAULT_TRUNCATION_HINT = "narrow your query or use limit/offset for more";
/** /**
* FNXC:ToolOutputBudget 2026-08-06-12:00: * FNXC:ToolOutputBudget 2026-08-06-12:00:
* FN-8614 bounds the total text returned by each engine-injected tool result so a * FN-8614 bounds the total text returned by each engine-injected tool result so a
* large log, document, or JSON response cannot consume an agent's context window. * large log, document, or JSON response cannot consume an agent's context window.
* 16,000 characters keeps ordinary PROMPT.md and durable-document reads useful while * 16,000 characters remains the default while operators can use
* placing a finite low-tens-of-thousands ceiling on one result. * `agentToolOutputMaxChars` to select a positive cap or the explicit no-limit value.
*
* FNXC:ToolOutputBudget 2026-08-06-16:00:
* FN-8616 requires an operator-controlled opt-out without making an unset or invalid
* value unbounded. Only the `0` setting sentinel disables this shared wrapper.
*/ */
export function buildToolOutputTruncationMarker(hint = DEFAULT_TRUNCATION_HINT): string { export function buildToolOutputTruncationMarker(hint = DEFAULT_TRUNCATION_HINT): string {
return `\n[Tool output truncated to fit the context budget; ${hint}.]`; return `\n[Tool output truncated to fit the context budget; ${hint}.]`;
@@ -75,13 +82,29 @@ export function clampToolOutputBlocks(
}); });
} }
/**
* Resolve the operator setting. Invalid values always retain the finite default;
* only the explicit zero sentinel represents unlimited output.
*/
export function resolveAgentToolOutputMaxChars(
settings: { agentToolOutputMaxChars?: number | null | unknown },
): number | null {
const candidate = settings.agentToolOutputMaxChars;
if (candidate === TOOL_OUTPUT_UNLIMITED_SETTING_VALUE) return null;
if (typeof candidate === "number" && Number.isFinite(candidate) && Number.isInteger(candidate) && candidate > 0) {
return candidate;
}
return DEFAULT_TOOL_OUTPUT_MAX_CHARS;
}
/** Resolve an optional named override; every valid result remains finitely bounded. */ /** Resolve an optional named override; every valid result remains finitely bounded. */
export function resolveToolOutputBudget( export function resolveToolOutputBudget(
toolName: string, toolName: string,
overrides: Readonly<Record<string, number | null | undefined>> | undefined, overrides: Readonly<Record<string, number | null | undefined>> | undefined,
defaultMaxChars = DEFAULT_TOOL_OUTPUT_MAX_CHARS,
): number { ): number {
if (!overrides || !Object.prototype.hasOwnProperty.call(overrides, toolName)) { if (!overrides || !Object.prototype.hasOwnProperty.call(overrides, toolName)) {
return DEFAULT_TOOL_OUTPUT_MAX_CHARS; return defaultMaxChars;
} }
const candidate = overrides[toolName]; const candidate = overrides[toolName];
if (typeof candidate === "number" && Number.isFinite(candidate) && Number.isInteger(candidate) && candidate > 0) { if (typeof candidate === "number" && Number.isFinite(candidate) && Number.isInteger(candidate) && candidate > 0) {
@@ -91,7 +114,7 @@ export function resolveToolOutputBudget(
const error = new Error(`Invalid tool output budget for ${toolName}; overrides must be finite positive integers.`); const error = new Error(`Invalid tool output budget for ${toolName}; overrides must be finite positive integers.`);
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
console.warn(error.message); console.warn(error.message);
return DEFAULT_TOOL_OUTPUT_MAX_CHARS; return defaultMaxChars;
} }
throw error; throw error;
} }

View File

@@ -797,6 +797,10 @@ export interface GlobalSettings {
* verbose `detail` payload is omitted to reduce log size/noise. Distinct * verbose `detail` payload is omitted to reduce log size/noise. Distinct
* from `persistAgentThinkingLog`, which controls `thinking` rows. */ * from `persistAgentThinkingLog`, which controls `thinking` rows. */
persistAgentToolOutput?: boolean; persistAgentToolOutput?: boolean;
/** Per-result engine-injected tool-output budget. Unset/null uses 16,000 characters;
* positive integers set a custom cap; 0 disables the shared clamp; invalid values
* fall back to the finite default. */
agentToolOutputMaxChars?: number | null;
/** When true, task chat receives engine-authored progress, failure, and rollback updates. Default: false. */ /** When true, task chat receives engine-authored progress, failure, and rollback updates. Default: false. */
proactiveTaskChatEnabled?: boolean; proactiveTaskChatEnabled?: boolean;
/** When true, persist `thinking` log entries from agent reasoning deltas for /** When true, persist `thinking` log entries from agent reasoning deltas for
@@ -1071,6 +1075,10 @@ export interface ProjectSettings {
* Records the last time the engine process proved it was alive so startup recovery can exclude process-down wall-clock time from active task duration without changing firstExecutionAt. * Records the last time the engine process proved it was alive so startup recovery can exclude process-down wall-clock time from active task duration without changing firstExecutionAt.
*/ */
engineLastActiveAt?: string; engineLastActiveAt?: string;
/** Per-result engine-injected tool-output budget. Unset/null uses 16,000 characters;
* positive integers set a custom cap; 0 disables the shared clamp; invalid values
* fall back to the finite default. */
agentToolOutputMaxChars?: number | null;
/** Maximum number of concurrent AI agents across all activity types /** Maximum number of concurrent AI agents across all activity types
* (triage specification, task execution, and merge operations). */ * (triage specification, task execution, and merge operations). */
maxConcurrent: number; maxConcurrent: number;

View File

@@ -149,6 +149,7 @@ export const GLOBAL_SECTION_KEYS: Record<string, ReadonlySet<string>> = {
"dismissModalsOnOutsideClick", "dismissModalsOnOutsideClick",
"skipConfirmationDialogs", "skipConfirmationDialogs",
"persistAgentToolOutput", "persistAgentToolOutput",
"agentToolOutputMaxChars",
"proactiveTaskChatEnabled", "proactiveTaskChatEnabled",
"persistAgentThinkingLogPermanent", "persistAgentThinkingLogPermanent",
"persistAgentThinkingLogEphemeral", "persistAgentThinkingLogEphemeral",

View File

@@ -41,6 +41,26 @@ export const globalGeneralSearchEntries: SettingsSearchEntry[] = [
" When disabled, tool rows are still logged but detailed tool payloads are omitted. Very large tool payloads may still be clipped even when this stays enabled. Default: disabled. ", " When disabled, tool rows are still logged but detailed tool payloads are omitted. Very large tool payloads may still be clipped even when this stays enabled. Default: disabled. ",
keywords: ["persist", "transcript", "disk usage"], keywords: ["persist", "transcript", "disk usage"],
}, },
{
sectionId: "global-general",
key: "agentToolOutputMaxChars",
labelKey: "settings.globalGeneral.agentToolOutputLimit",
labelFallback: " Agent tool-output limit ",
helpKey: "settings.globalGeneral.agentToolOutputLimitHint",
helpFallback:
" Maximum characters returned from each engine-injected tool result. When unset, inherits the 16,000-character engine default. Leave empty to use the default. ",
keywords: ["tokens", "context", "truncate", "tool output", "agent"],
},
{
sectionId: "global-general",
key: "agentToolOutputMaxCharsNoLimit",
labelKey: "settings.globalGeneral.noLimitOnAgentToolOutput",
labelFallback: " No limit on agent tool output ",
helpKey: "settings.globalGeneral.noLimitOnAgentToolOutputHint",
helpFallback:
" Disable the shared tool-output clamp. A single tool result can consume the agent context window. Default: disabled; when unset, the budget inherits the 16,000-character engine default. ",
keywords: ["unlimited", "tokens", "context", "truncate", "tool output"],
},
{ {
sectionId: "global-general", sectionId: "global-general",
key: "proactiveTaskChatEnabled", key: "proactiveTaskChatEnabled",

View File

@@ -1,5 +1,6 @@
import { resolvePersistAgentThinkingLog } from "@fusion/core"; import { resolvePersistAgentThinkingLog } from "@fusion/core";
import { SettingsToggleRow } from "../SettingsToggleRow"; import { SettingsToggleRow } from "../SettingsToggleRow";
import { SettingsNumberRow } from "../SettingsNumberRow";
import { SettingsSelectRow } from "../SettingsSelectRow"; import { SettingsSelectRow } from "../SettingsSelectRow";
import { SettingsHelpTip } from "../SettingsHelpTip"; import { SettingsHelpTip } from "../SettingsHelpTip";
import type { SectionBaseProps } from "./context"; import type { SectionBaseProps } from "./context";
@@ -56,6 +57,31 @@ export function GlobalGeneralSection({ form, setForm }: GlobalGeneralSectionProp
value={form.persistAgentToolOutput === true} value={form.persistAgentToolOutput === true}
onChange={(v) => setForm((f) => ({ ...f, persistAgentToolOutput: v === true }))} onChange={(v) => setForm((f) => ({ ...f, persistAgentToolOutput: v === true }))}
/> />
<SettingsToggleRow
descriptor={{
key: "agentToolOutputMaxCharsNoLimit",
label: t("settings.globalGeneral.noLimitOnAgentToolOutput", " No limit on agent tool output "),
help: t("settings.globalGeneral.noLimitOnAgentToolOutputHint", " Disable the shared tool-output clamp. A single tool result can consume the agent context window. Default: disabled; when unset, the budget inherits the 16,000-character engine default. "),
scope: "global",
}}
value={form.agentToolOutputMaxChars === 0}
onChange={(v) => setForm((f) => ({ ...f, agentToolOutputMaxChars: v ? 0 : null }))}
/>
<SettingsNumberRow
descriptor={{
key: "agentToolOutputMaxChars",
label: t("settings.globalGeneral.agentToolOutputLimit", " Agent tool-output limit "),
help: t("settings.globalGeneral.agentToolOutputLimitHint", " Maximum characters returned from each engine-injected tool result. When unset, inherits the 16,000-character engine default. Leave empty to use the default. "),
scope: "global",
min: 1,
step: 1000,
placeholder: "16000",
disabled: form.agentToolOutputMaxChars === 0,
}}
value={form.agentToolOutputMaxChars === 0 ? null : (form.agentToolOutputMaxChars ?? null)}
clearable
onChange={(v) => setForm((f) => ({ ...f, agentToolOutputMaxChars: v }))}
/>
<SettingsToggleRow <SettingsToggleRow
descriptor={{ descriptor={{
key: "proactiveTaskChatEnabled", key: "proactiveTaskChatEnabled",

View File

@@ -0,0 +1,46 @@
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import type { Settings } from "@fusion/core";
import { GlobalGeneralSection } from "../GlobalGeneralSection";
import type { SettingsFormState } from "../context";
function renderSection(formOverrides: Partial<Settings> = {}) {
let form = { ...formOverrides } as SettingsFormState;
const setForm = vi.fn((updater: SettingsFormState | ((previous: SettingsFormState) => SettingsFormState)) => {
form = typeof updater === "function" ? updater(form) : updater;
});
const view = render(<GlobalGeneralSection form={form} setForm={setForm} />);
setForm.mockImplementation((updater: SettingsFormState | ((previous: SettingsFormState) => SettingsFormState)) => {
form = typeof updater === "function" ? updater(form) : updater;
view.rerender(<GlobalGeneralSection form={form} setForm={setForm} />);
});
return { getForm: () => form };
}
describe("GlobalGeneralSection agent tool-output budget", () => {
it("stores the explicit unlimited sentinel and disables the numeric input", () => {
const { getForm } = renderSection();
const noLimit = screen.getByLabelText("No limit on agent tool output");
const limit = screen.getByLabelText("Agent tool-output limit");
fireEvent.click(noLimit);
expect(getForm().agentToolOutputMaxChars).toBe(0);
expect(limit).toBeDisabled();
fireEvent.click(noLimit);
expect(getForm().agentToolOutputMaxChars).toBeNull();
expect(limit).not.toBeDisabled();
});
it("stores a number and clears back to the inherited default", () => {
const { getForm } = renderSection();
const limit = screen.getByLabelText("Agent tool-output limit");
fireEvent.change(limit, { target: { value: "500" } });
expect(getForm().agentToolOutputMaxChars).toBe(500);
fireEvent.change(limit, { target: { value: "" } });
expect(getForm().agentToolOutputMaxChars).toBeNull();
});
});

View File

@@ -79,6 +79,7 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
dismissModalsOnOutsideClick: "globalGeneral.dismissModalsByClickingOutsideHint", dismissModalsOnOutsideClick: "globalGeneral.dismissModalsByClickingOutsideHint",
skipConfirmationDialogs: "globalGeneral.skipConfirmationDialogsHint", skipConfirmationDialogs: "globalGeneral.skipConfirmationDialogsHint",
persistAgentToolOutput: "globalGeneral.whenDisabledToolRowsAreStillLoggedBut", persistAgentToolOutput: "globalGeneral.whenDisabledToolRowsAreStillLoggedBut",
agentToolOutputMaxChars: "globalGeneral.agentToolOutputLimitHint",
proactiveTaskChatEnabled: "globalGeneral.enableProactiveTaskChatHint", proactiveTaskChatEnabled: "globalGeneral.enableProactiveTaskChatHint",
persistAgentThinkingLogPermanent: "globalGeneral.rowsAndDoesNotAffectAssistantTextOr", persistAgentThinkingLogPermanent: "globalGeneral.rowsAndDoesNotAffectAssistantTextOr",
persistAgentThinkingLogEphemeral: "globalGeneral.rowsAndDoesNotAffectAssistantTextOr", persistAgentThinkingLogEphemeral: "globalGeneral.rowsAndDoesNotAffectAssistantTextOr",

View File

@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_TOOL_OUTPUT_MAX_CHARS } from "@fusion/core";
import { import {
extractRuntimeHint, extractRuntimeHint,
extractRuntimeModel, extractRuntimeModel,
@@ -18,6 +19,7 @@ import {
resolveValidatorSessionModel, resolveValidatorSessionModel,
resolveValidatorThinkingLevel, resolveValidatorThinkingLevel,
resolveValidatorFallbackThinkingLevel, resolveValidatorFallbackThinkingLevel,
createResolvedAgentSession,
wrapCustomToolsForPluginRuntime, wrapCustomToolsForPluginRuntime,
} from "../agent-session-helpers.js"; } from "../agent-session-helpers.js";
@@ -621,6 +623,24 @@ describe("createResolvedAgentSession", () => {
resolveRuntimeMock.mockReset(); resolveRuntimeMock.mockReset();
}); });
it("resolves the merged tool-output setting once before forwarding to runtime sessions", async () => {
const createSessionMock = vi.fn().mockResolvedValue({ session: { prompt: vi.fn() }, sessionFile: "session.json" });
resolveRuntimeMock.mockResolvedValue({
runtime: { id: "pi", name: "Default PI Runtime", createSession: createSessionMock, promptWithFallback: vi.fn(), describeModel: vi.fn(() => "mock/model") },
runtimeId: "pi",
wasConfigured: false,
});
for (const [settings, expected] of [
[{ agentToolOutputMaxChars: 500 }, 500],
[{ agentToolOutputMaxChars: 0 }, null],
[{}, DEFAULT_TOOL_OUTPUT_MAX_CHARS],
] as const) {
await createResolvedAgentSession({ sessionPurpose: "executor", cwd: "/tmp/project", systemPrompt: "system", settings });
expect(createSessionMock).toHaveBeenLastCalledWith(expect.objectContaining({ toolOutputMaxChars: expected }));
}
});
it("forwards taskEnv unchanged to runtime session factory", async () => { it("forwards taskEnv unchanged to runtime session factory", async () => {
const mockSession = { prompt: vi.fn() } as any; const mockSession = { prompt: vi.fn() } as any;
const createSessionMock = vi.fn().mockResolvedValue({ const createSessionMock = vi.fn().mockResolvedValue({

View File

@@ -16,6 +16,7 @@ import {
type ResolvedRuntime, type ResolvedRuntime,
} from "../runtime-resolution.js"; } from "../runtime-resolution.js";
import type { PluginRunner } from "../plugin-runner.js"; import type { PluginRunner } from "../plugin-runner.js";
import { createFnAgent } from "../pi.js";
import type { PluginRuntimeRegistration } from "@fusion/core"; import type { PluginRuntimeRegistration } from "@fusion/core";
// Mock the logger to suppress output during tests // Mock the logger to suppress output during tests
@@ -114,6 +115,20 @@ describe("runtime-resolution", () => {
const runtime = getDefaultPiRuntime(); const runtime = getDefaultPiRuntime();
expect(typeof runtime.describeModel).toBe("function"); expect(typeof runtime.describeModel).toBe("function");
}); });
it("forwards bounded and unlimited tool-output budgets into the pi agent", async () => {
const runtime = getDefaultPiRuntime();
const sessionOptions = {
cwd: "/tmp/project",
systemPrompt: "system",
} as AgentRuntimeOptions;
await runtime.createSession({ ...sessionOptions, toolOutputMaxChars: 500 });
expect(createFnAgent).toHaveBeenLastCalledWith(expect.objectContaining({ toolOutputMaxChars: 500 }));
await runtime.createSession({ ...sessionOptions, toolOutputMaxChars: null });
expect(createFnAgent).toHaveBeenLastCalledWith(expect.objectContaining({ toolOutputMaxChars: null }));
});
}); });
describe("resolveRuntime()", () => { describe("resolveRuntime()", () => {

View File

@@ -44,24 +44,51 @@ describe("tool output budget wrapper", () => {
expect(result.content.map((block: any) => block.text)).toEqual(["abcd", marker, ""]); expect(result.content.map((block: any) => block.text)).toEqual(["abcd", marker, ""]);
}); });
it("honors finite larger and smaller overrides, rejects invalid values, and does not double-mark", async () => { it("honors a custom base budget and finite named overrides without double-marking", async () => {
const source = "x".repeat(200); const source = "x".repeat(700);
const large = await execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { overrides: { fn_budget_test: 300 } })[0]); const custom = await execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { maxChars: 500 })[0]);
const small = await execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { overrides: { fn_budget_test: 100 } })[0]); const override = await execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], {
expect(large.content[0].text).toBe(source); maxChars: 500,
expect(small.content[0].text.length).toBeLessThanOrEqual(100); overrides: { fn_budget_test: 100 },
})[0]);
expect(custom.content[0].text.length).toBeLessThanOrEqual(500);
expect(override.content[0].text.length).toBeLessThanOrEqual(100);
await expect(execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { overrides: { fn_budget_test: Infinity } })[0])).rejects.toThrow(/finite positive integers/); await expect(execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { overrides: { fn_budget_test: Infinity } })[0])).rejects.toThrow(/finite positive integers/);
const once = wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { overrides: { fn_budget_test: 100 } }); const once = wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { maxChars: 100 });
const twice = wrapToolsWithOutputBudget(once, { overrides: { fn_budget_test: 100 } }); const twice = wrapToolsWithOutputBudget(once, { maxChars: 100 });
const result = await execute(twice[0]); const result = await execute(twice[0]);
expect(result.content[0].text.match(/Tool output truncated/g)).toHaveLength(1); expect(result.content[0].text.match(/Tool output truncated/g)).toHaveLength(1);
}); });
it("applies the same clamp on the non-pi plugin-runtime path exactly once", async () => { it("applies the same configurable clamp on the non-pi plugin-runtime path exactly once", async () => {
const result = await execute(wrapCustomToolsForPluginRuntime([ const result = await execute(wrapCustomToolsForPluginRuntime([
toolWithResult({ content: [{ type: "text", text: "x".repeat(DEFAULT_TOOL_OUTPUT_MAX_CHARS + 1) }] }), toolWithResult({ content: [{ type: "text", text: "x".repeat(DEFAULT_TOOL_OUTPUT_MAX_CHARS + 1) }] }),
], {})![0]); ], {})![0]);
const custom = await execute(wrapCustomToolsForPluginRuntime([
toolWithResult({ content: [{ type: "text", text: "x".repeat(700) }] }),
], { toolOutputMaxChars: 500 })![0]);
expect(result.content[0].text.length).toBeLessThanOrEqual(DEFAULT_TOOL_OUTPUT_MAX_CHARS); expect(result.content[0].text.length).toBeLessThanOrEqual(DEFAULT_TOOL_OUTPUT_MAX_CHARS);
expect(result.content[0].text.match(/Tool output truncated/g)).toHaveLength(1); expect(result.content[0].text.match(/Tool output truncated/g)).toHaveLength(1);
expect(custom.content[0].text.length).toBeLessThanOrEqual(500);
expect(custom.content[0].text.match(/Tool output truncated/g)).toHaveLength(1);
});
it("returns pi and plugin tool lists unchanged when the resolved setting is unlimited", async () => {
const original = {
content: [
{ type: "text", text: "a".repeat(800) },
{ type: "image", data: "unchanged" },
{ type: "text", text: "b".repeat(800) },
],
details: { retained: true },
isError: true,
};
const piTools = [toolWithResult(original)];
const pluginTools = [toolWithResult(original)];
expect(wrapToolsWithOutputBudget(piTools, { maxChars: null })).toBe(piTools);
const piResult = await execute(wrapToolsWithOutputBudget(piTools, { maxChars: null })[0]);
const pluginResult = await execute(wrapCustomToolsForPluginRuntime(pluginTools, { toolOutputMaxChars: null })![0]);
expect(piResult).toBe(original);
expect(pluginResult).toBe(original);
}); });
}); });

View File

@@ -89,6 +89,8 @@ export interface AgentRuntimeOptions {
tools?: "coding" | "readonly"; tools?: "coding" | "readonly";
/** Additional custom tools to merge with the base toolset */ /** Additional custom tools to merge with the base toolset */
customTools?: ToolDefinition[]; customTools?: ToolDefinition[];
/** Per-result shared tool-output cap. `null` disables the wrapper; undefined uses the built-in default. */
toolOutputMaxChars?: number | null;
/** Callback for text output from the agent */ /** Callback for text output from the agent */
onText?: (delta: string) => void; onText?: (delta: string) => void;
/** Callback for thinking/thought output from the agent */ /** Callback for thinking/thought output from the agent */

View File

@@ -15,6 +15,7 @@ import {
GROK_CLI_PROVIDER_ID, GROK_CLI_PROVIDER_ID,
isGrokApiKeyFusionVisible, isGrokApiKeyFusionVisible,
isTestModeActive, isTestModeActive,
resolveAgentToolOutputMaxChars,
resolveExecutionSettingsModel, resolveExecutionSettingsModel,
resolveExecutorFallbackModel, resolveExecutorFallbackModel,
resolveMergerSettingsModel, resolveMergerSettingsModel,
@@ -81,7 +82,7 @@ const RUNTIMES_WITH_INTERNAL_TOOL_GATING = new Set(["pi"]);
*/ */
export function wrapCustomToolsForPluginRuntime( export function wrapCustomToolsForPluginRuntime(
tools: ToolDefinition[] | undefined, tools: ToolDefinition[] | undefined,
options: Pick<AgentRuntimeOptions, "actionGateContext" | "permanentAgentGating">, options: Pick<AgentRuntimeOptions, "actionGateContext" | "permanentAgentGating" | "toolOutputMaxChars">,
logContext?: { runtimeId: string; sessionPurpose: string }, logContext?: { runtimeId: string; sessionPurpose: string },
): ToolDefinition[] | undefined { ): ToolDefinition[] | undefined {
if (!tools || tools.length === 0) { if (!tools || tools.length === 0) {
@@ -97,10 +98,10 @@ export function wrapCustomToolsForPluginRuntime(
const withRtk = wrapToolsWithRtkRewrite(tools); const withRtk = wrapToolsWithRtkRewrite(tools);
const withPermanent = wrapToolsWithPermanentAgentGating(withRtk, options.permanentAgentGating); const withPermanent = wrapToolsWithPermanentAgentGating(withRtk, options.permanentAgentGating);
const withActionGate = wrapToolsWithActionGate(withPermanent, options.actionGateContext); const withActionGate = wrapToolsWithActionGate(withPermanent, options.actionGateContext);
// FNXC:ToolOutputBudget 2026-08-06-12:00: // FNXC:ToolOutputBudget 2026-08-06-16:00:
// Non-pi runtimes do not pass through createFnAgent, so apply the same outermost // Non-pi runtimes do not pass through createFnAgent, so apply the same outermost
// per-result clamp here exactly once rather than letting plugin tool output bypass it. // setting-derived clamp here exactly once. A null budget leaves tools byte-identical.
return wrapToolsWithOutputBudget(withActionGate); return wrapToolsWithOutputBudget(withActionGate, { maxChars: options.toolOutputMaxChars });
} }
function shouldWrapCustomToolsForRuntime(runtimeId: string): boolean { function shouldWrapCustomToolsForRuntime(runtimeId: string): boolean {
@@ -878,8 +879,17 @@ export async function createResolvedAgentSession(
createResolvedAgentSession must preserve additionalSkillPaths into runtime.createSession. createResolvedAgentSession must preserve additionalSkillPaths into runtime.createSession.
Plugin skill names alone never deliver bodies; chat, step, and cron lanes require both halves of the #2017 contract (FN-8443 / #2364). Plugin skill names alone never deliver bodies; chat, step, and cron lanes require both halves of the #2017 contract (FN-8443 / #2364).
*/ */
/*
FNXC:ToolOutputBudget 2026-08-06-16:00:
Resolve the merged settings once at this all-lanes seam. A caller can still provide
an explicit runtime value (including null for unlimited); otherwise settings supply
the finite default, custom cap, or explicit no-limit sentinel interpretation.
*/
const runtimeOptions: AgentRuntimeOptions = { const runtimeOptions: AgentRuntimeOptions = {
...runtimeOptionsRaw, ...runtimeOptionsRaw,
toolOutputMaxChars: runtimeOptionsRaw.toolOutputMaxChars !== undefined
? runtimeOptionsRaw.toolOutputMaxChars
: resolveAgentToolOutputMaxChars(settings ?? {}),
...(mergedSkillNames.length > 0 ? { skills: mergedSkillNames } : {}), ...(mergedSkillNames.length > 0 ? { skills: mergedSkillNames } : {}),
}; };
// FNXC:McpConfig 2026-06-25-22:06: // FNXC:McpConfig 2026-06-25-22:06:

View File

@@ -1037,6 +1037,8 @@ export interface AgentOptions {
systemPromptLayers?: SystemPromptLayers; systemPromptLayers?: SystemPromptLayers;
tools?: "coding" | "readonly"; tools?: "coding" | "readonly";
customTools?: ToolDefinition[]; customTools?: ToolDefinition[];
/** Per-result shared tool-output cap. `null` disables the wrapper; undefined uses the built-in default. */
toolOutputMaxChars?: number | null;
/* /*
FNXC:MergeQueue 2026-07-15-11:08: FNXC:MergeQueue 2026-07-15-11:08:
Merger sessions must not load host @runfusion/fusion extension tools. Extension fn_task_show boots a second PostgreSQL TaskStore (createTaskStoreForBackend) inside the engine process and can hang indefinitely without a tool timeout, wedging the single-flight merge pump (observed FN-7956). Merger sessions must not load host @runfusion/fusion extension tools. Extension fn_task_show boots a second PostgreSQL TaskStore (createTaskStoreForBackend) inside the engine process and can hang indefinitely without a tool timeout, wedging the single-flight merge pump (observed FN-7956).
@@ -1878,9 +1880,12 @@ export function wrapToolsWithBoundary(
/* /*
FNXC:ToolOutputBudget 2026-08-06-12:00: FNXC:ToolOutputBudget 2026-08-06-12:00:
FN-8614 requires one finite budget for the total model-visible text in every FN-8614 requires one finite budget for the total model-visible text in every
engine-injected tool result. Overrides may choose only another finite positive engine-injected tool result. Per-tool overrides remain finite positive integers;
integer cap; there is deliberately no unbounded sentinel or opt-out. The audit only the operator-level setting can select the explicit unlimited mode.
found no legitimate result that needs more than the shared 16,000-character cap.
FNXC:ToolOutputBudget 2026-08-06-16:00:
FN-8616 makes the shared cap configurable at the session seam. A `null` resolved
value returns the original tool list, avoiding all clamp/marker rewriting.
*/ */
export const TOOL_OUTPUT_BUDGET_OVERRIDES: Readonly<Record<string, number>> = {}; export const TOOL_OUTPUT_BUDGET_OVERRIDES: Readonly<Record<string, number>> = {};
@@ -1890,8 +1895,9 @@ export const TOOL_OUTPUT_BUDGET_OVERRIDES: Readonly<Record<string, number>> = {}
*/ */
export function wrapToolsWithOutputBudget( export function wrapToolsWithOutputBudget(
tools: ToolDefinition[], tools: ToolDefinition[],
options: { overrides?: Readonly<Record<string, number | null | undefined>> } = {}, options: { overrides?: Readonly<Record<string, number | null | undefined>>; maxChars?: number | null } = {},
): ToolDefinition[] { ): ToolDefinition[] {
if (options.maxChars === null) return tools;
return tools.map((tool) => { return tools.map((tool) => {
const originalExecute = tool.execute as any; const originalExecute = tool.execute as any;
return { return {
@@ -1910,7 +1916,11 @@ export function wrapToolsWithOutputBudget(
}); });
if (textPositions.length === 0) return result; if (textPositions.length === 0) return result;
const budget = resolveToolOutputBudget(tool.name, options.overrides ?? TOOL_OUTPUT_BUDGET_OVERRIDES); const budget = resolveToolOutputBudget(
tool.name,
options.overrides ?? TOOL_OUTPUT_BUDGET_OVERRIDES,
options.maxChars ?? undefined,
);
const clamped = clampToolOutputBlocks(texts, { maxChars: budget }); const clamped = clampToolOutputBlocks(texts, { maxChars: budget });
const content = result.content.map((block: unknown) => block); const content = result.content.map((block: unknown) => block);
textPositions.forEach((position, index) => { textPositions.forEach((position, index) => {
@@ -2574,9 +2584,12 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
boundaryContext.worktreeProjectRoot, boundaryContext.worktreeProjectRoot,
normalizedAdditionalSkillPaths, normalizedAdditionalSkillPaths,
); );
// FNXC:ToolOutputBudget 2026-08-06-12:00: // FNXC:ToolOutputBudget 2026-08-06-16:00:
// Keep this outermost so policy-gate and boundary rejection text is bounded too. // Keep this outermost so policy-gate and boundary rejection text is bounded too;
const customToolList: ToolDefinition[] = wrapToolsWithOutputBudget(boundaryWrappedTools); // a null setting-derived budget intentionally returns the chain unchanged.
const customToolList: ToolDefinition[] = wrapToolsWithOutputBudget(boundaryWrappedTools, {
maxChars: options.toolOutputMaxChars,
});
// Sort tools alphabetically by name for deterministic ordering. // Sort tools alphabetically by name for deterministic ordering.
// Prompt caching requires the tool list to be byte-identical across // Prompt caching requires the tool list to be byte-identical across
// sessions — reordering breaks cache prefix matching. // sessions — reordering breaks cache prefix matching.

View File

@@ -103,9 +103,15 @@ export class DefaultPiRuntime implements AgentRuntime {
async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> { async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> {
// FNXC:McpConfig 2026-06-25-22:04: // FNXC:McpConfig 2026-06-25-22:04:
// DefaultPiRuntime is the typed bridge from shared AgentRuntimeOptions into createFnAgent. Normalize the legacy stdio ACP shape here so all lanes can pass the FN-7022 three-transport shape without breaking older Route A callers. // DefaultPiRuntime is the typed bridge from shared AgentRuntimeOptions into createFnAgent. Normalize the legacy stdio ACP shape here so all lanes can pass the FN-7022 three-transport shape without breaking older Route A callers.
/*
FNXC:ToolOutputBudget 2026-08-06-16:30:
Forward the resolved session budget explicitly across the default-pi bridge. A project setting of 0 becomes null before this point and must reach createFnAgent so the pi wrapper skips clamping just like plugin runtimes.
*/
const { toolOutputMaxChars, mcpServers, ...agentOptions } = options;
return createFnAgent({ return createFnAgent({
...options, ...agentOptions,
mcpServers: normalizeAgentRuntimeMcpServers(options.mcpServers), toolOutputMaxChars,
mcpServers: normalizeAgentRuntimeMcpServers(mcpServers),
}); });
} }

View File

@@ -6049,6 +6049,10 @@
"saveAIThinkingForPermanentAgents": " Save AI thinking for permanent agents ", "saveAIThinkingForPermanentAgents": " Save AI thinking for permanent agents ",
"saveAIThinkingLogs": "Save AI thinking logs", "saveAIThinkingLogs": "Save AI thinking logs",
"saveToolOutputInAgentLogs": " Save tool output in agent logs ", "saveToolOutputInAgentLogs": " Save tool output in agent logs ",
"agentToolOutputLimit": " Agent tool-output limit ",
"agentToolOutputLimitHint": " Maximum characters returned from each engine-injected tool result. When unset, inherits the 16,000-character engine default. Leave empty to use the default. ",
"noLimitOnAgentToolOutput": " No limit on agent tool output ",
"noLimitOnAgentToolOutputHint": " Disable the shared tool-output clamp. A single tool result can consume the agent context window. Default: disabled; when unset, the budget inherits the 16,000-character engine default. ",
"enableProactiveTaskChat": " Enable proactive task-chat updates ", "enableProactiveTaskChat": " Enable proactive task-chat updates ",
"enableProactiveTaskChatHint": " When enabled, Task chat reports step progress, failures, reviews, and rollbacks in real time. Default: disabled. ", "enableProactiveTaskChatHint": " When enabled, Task chat reports step progress, failures, reviews, and rollbacks in real time. Default: disabled. ",
"updates": "Updates", "updates": "Updates",

View File

@@ -6044,7 +6044,11 @@
"releaseChannel": "", "releaseChannel": "",
"releaseChannelHelp": "", "releaseChannelHelp": "",
"channelStable": "", "channelStable": "",
"channelBeta": "" "channelBeta": "",
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
}, },
"globalModels": { "globalModels": {
"allow": "", "allow": "",

View File

@@ -6044,7 +6044,11 @@
"releaseChannel": "", "releaseChannel": "",
"releaseChannelHelp": "", "releaseChannelHelp": "",
"channelStable": "", "channelStable": "",
"channelBeta": "" "channelBeta": "",
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
}, },
"globalModels": { "globalModels": {
"allow": "", "allow": "",

View File

@@ -6044,7 +6044,11 @@
"releaseChannel": "", "releaseChannel": "",
"releaseChannelHelp": "", "releaseChannelHelp": "",
"channelStable": "", "channelStable": "",
"channelBeta": "" "channelBeta": "",
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
}, },
"globalModels": { "globalModels": {
"allow": "", "allow": "",

View File

@@ -6044,7 +6044,11 @@
"releaseChannel": "", "releaseChannel": "",
"releaseChannelHelp": "", "releaseChannelHelp": "",
"channelStable": "", "channelStable": "",
"channelBeta": "" "channelBeta": "",
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
}, },
"globalModels": { "globalModels": {
"allow": "", "allow": "",

View File

@@ -6044,7 +6044,11 @@
"releaseChannel": "", "releaseChannel": "",
"releaseChannelHelp": "", "releaseChannelHelp": "",
"channelStable": "", "channelStable": "",
"channelBeta": "" "channelBeta": "",
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
}, },
"globalModels": { "globalModels": {
"allow": "", "allow": "",

View File

@@ -6035,6 +6035,8 @@ export default interface Resources {
"workspaceModeHint": "When enabled, the project root is treated as a workspace containing multiple git sub-repos. Tasks run per-sub-repo and no git repo is created at the root. Disable for single-repo projects. No default — unset (disabled)." "workspaceModeHint": "When enabled, the project root is treated as a workspace containing multiple git sub-repos. Tasks run per-sub-repo and no git repo is created at the root. Disable for single-repo projects. No default — unset (disabled)."
}, },
"globalGeneral": { "globalGeneral": {
"agentToolOutputLimit": " Agent tool-output limit ",
"agentToolOutputLimitHint": " Maximum characters returned from each engine-injected tool result. Default: 16,000 characters per tool result. Leave empty to use the default. ",
"andShowsUpdateNoticesInTheCLIAnd": " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. Default: enabled. ", "andShowsUpdateNoticesInTheCLIAnd": " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. Default: enabled. ",
"autoReloadDashboardOnVersionChange": " Auto-reload dashboard on version change ", "autoReloadDashboardOnVersionChange": " Auto-reload dashboard on version change ",
"cLIBinaryOnPATH": " CLI binary on PATH ", "cLIBinaryOnPATH": " CLI binary on PATH ",
@@ -6059,6 +6061,8 @@ export default interface Resources {
"globalDefaultTrackingRepo": "Global default tracking repo", "globalDefaultTrackingRepo": "Global default tracking repo",
"leaveBothThinkingTogglesOffToKeepThe": " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ", "leaveBothThinkingTogglesOffToKeepThe": " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ",
"manualOnlyNeverAutoCheck": "Manual only — never auto-check", "manualOnlyNeverAutoCheck": "Manual only — never auto-check",
"noLimitOnAgentToolOutput": " No limit on agent tool output ",
"noLimitOnAgentToolOutputHint": " Disable the shared tool-output clamp. A single tool result can consume the agent context window. Default: disabled; the default budget is 16,000 characters per tool result. ",
"onStartupOncePerServerLaunch": "On startup — once per server launch", "onStartupOncePerServerLaunch": "On startup — once per server launch",
"ownerRepo": "owner/repo", "ownerRepo": "owner/repo",
"projectsInheritThisValueWhenTheyDoNot": "Projects inherit this value when they do not set a project default tracking repo. No default — unset.", "projectsInheritThisValueWhenTheyDoNot": "Projects inherit this value when they do not set a project default tracking repo. No default — unset.",