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:
7
.changeset/fn-8616-tool-output-budget-setting.md
Normal file
7
.changeset/fn-8616-tool-output-budget-setting.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
`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
|
||||
|
||||
|
||||
@@ -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.
|
||||
| `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`. |
|
||||
| `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**. |
|
||||
| `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. |
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
buildToolOutputTruncationMarker,
|
||||
clampToolOutputBlocks,
|
||||
clampToolOutputText,
|
||||
resolveAgentToolOutputMaxChars,
|
||||
resolveToolOutputBudget,
|
||||
} from "../tool-output-budget.js";
|
||||
|
||||
@@ -47,12 +48,24 @@ describe("tool output budget", () => {
|
||||
expect(output.join("")).toHaveLength(maxChars);
|
||||
});
|
||||
|
||||
it("resolves only finite positive integer overrides", () => {
|
||||
expect(resolveToolOutputBudget("missing", {})).toBe(DEFAULT_TOOL_OUTPUT_MAX_CHARS);
|
||||
expect(resolveToolOutputBudget("large", { large: 20_000 })).toBe(20_000);
|
||||
expect(resolveToolOutputBudget("small", { small: 10 })).toBe(10);
|
||||
it("resolves the setting's default, bounded, and explicit unlimited states", () => {
|
||||
for (const value of [undefined, null]) {
|
||||
expect(resolveAgentToolOutputMaxChars({ agentToolOutputMaxChars: value })).toBe(DEFAULT_TOOL_OUTPUT_MAX_CHARS);
|
||||
}
|
||||
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]) {
|
||||
expect(() => resolveToolOutputBudget("bad", { bad: value })).toThrow(/finite positive integers/);
|
||||
expect(() => resolveToolOutputBudget("bad", { bad: value }, 500)).toThrow(/finite positive integers/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,9 +107,11 @@ export type {
|
||||
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js";
|
||||
export {
|
||||
DEFAULT_TOOL_OUTPUT_MAX_CHARS,
|
||||
TOOL_OUTPUT_UNLIMITED_SETTING_VALUE,
|
||||
buildToolOutputTruncationMarker,
|
||||
clampToolOutputText,
|
||||
clampToolOutputBlocks,
|
||||
resolveAgentToolOutputMaxChars,
|
||||
resolveToolOutputBudget,
|
||||
} from "./tool-output-budget.js";
|
||||
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
|
||||
|
||||
@@ -121,9 +121,11 @@ export * from "./file-scope-classification.js";
|
||||
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js";
|
||||
export {
|
||||
DEFAULT_TOOL_OUTPUT_MAX_CHARS,
|
||||
TOOL_OUTPUT_UNLIMITED_SETTING_VALUE,
|
||||
buildToolOutputTruncationMarker,
|
||||
clampToolOutputText,
|
||||
clampToolOutputBlocks,
|
||||
resolveAgentToolOutputMaxChars,
|
||||
resolveToolOutputBudget,
|
||||
} from "./tool-output-budget.js";
|
||||
export {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
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
|
||||
// explicitly to add engine-authored lifecycle narration to the transcript.
|
||||
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.
|
||||
autoResolveReviewComments: true,
|
||||
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,
|
||||
mergeRequestContractShadowEnabled: false,
|
||||
mergeStrategy: "direct",
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
/** Default maximum model-visible characters in one engine-injected tool result. */
|
||||
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";
|
||||
|
||||
/**
|
||||
* FNXC:ToolOutputBudget 2026-08-06-12:00:
|
||||
* 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.
|
||||
* 16,000 characters keeps ordinary PROMPT.md and durable-document reads useful while
|
||||
* placing a finite low-tens-of-thousands ceiling on one result.
|
||||
* 16,000 characters remains the default while operators can use
|
||||
* `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 {
|
||||
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. */
|
||||
export function resolveToolOutputBudget(
|
||||
toolName: string,
|
||||
overrides: Readonly<Record<string, number | null | undefined>> | undefined,
|
||||
defaultMaxChars = DEFAULT_TOOL_OUTPUT_MAX_CHARS,
|
||||
): number {
|
||||
if (!overrides || !Object.prototype.hasOwnProperty.call(overrides, toolName)) {
|
||||
return DEFAULT_TOOL_OUTPUT_MAX_CHARS;
|
||||
return defaultMaxChars;
|
||||
}
|
||||
const candidate = overrides[toolName];
|
||||
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.`);
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
console.warn(error.message);
|
||||
return DEFAULT_TOOL_OUTPUT_MAX_CHARS;
|
||||
return defaultMaxChars;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -797,6 +797,10 @@ export interface GlobalSettings {
|
||||
* verbose `detail` payload is omitted to reduce log size/noise. Distinct
|
||||
* from `persistAgentThinkingLog`, which controls `thinking` rows. */
|
||||
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. */
|
||||
proactiveTaskChatEnabled?: boolean;
|
||||
/** 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.
|
||||
*/
|
||||
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
|
||||
* (triage specification, task execution, and merge operations). */
|
||||
maxConcurrent: number;
|
||||
|
||||
@@ -149,6 +149,7 @@ export const GLOBAL_SECTION_KEYS: Record<string, ReadonlySet<string>> = {
|
||||
"dismissModalsOnOutsideClick",
|
||||
"skipConfirmationDialogs",
|
||||
"persistAgentToolOutput",
|
||||
"agentToolOutputMaxChars",
|
||||
"proactiveTaskChatEnabled",
|
||||
"persistAgentThinkingLogPermanent",
|
||||
"persistAgentThinkingLogEphemeral",
|
||||
|
||||
@@ -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. ",
|
||||
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",
|
||||
key: "proactiveTaskChatEnabled",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { resolvePersistAgentThinkingLog } from "@fusion/core";
|
||||
import { SettingsToggleRow } from "../SettingsToggleRow";
|
||||
import { SettingsNumberRow } from "../SettingsNumberRow";
|
||||
import { SettingsSelectRow } from "../SettingsSelectRow";
|
||||
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||
import type { SectionBaseProps } from "./context";
|
||||
@@ -56,6 +57,31 @@ export function GlobalGeneralSection({ form, setForm }: GlobalGeneralSectionProp
|
||||
value={form.persistAgentToolOutput === 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
|
||||
descriptor={{
|
||||
key: "proactiveTaskChatEnabled",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -79,6 +79,7 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
|
||||
dismissModalsOnOutsideClick: "globalGeneral.dismissModalsByClickingOutsideHint",
|
||||
skipConfirmationDialogs: "globalGeneral.skipConfirmationDialogsHint",
|
||||
persistAgentToolOutput: "globalGeneral.whenDisabledToolRowsAreStillLoggedBut",
|
||||
agentToolOutputMaxChars: "globalGeneral.agentToolOutputLimitHint",
|
||||
proactiveTaskChatEnabled: "globalGeneral.enableProactiveTaskChatHint",
|
||||
persistAgentThinkingLogPermanent: "globalGeneral.rowsAndDoesNotAffectAssistantTextOr",
|
||||
persistAgentThinkingLogEphemeral: "globalGeneral.rowsAndDoesNotAffectAssistantTextOr",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_TOOL_OUTPUT_MAX_CHARS } from "@fusion/core";
|
||||
import {
|
||||
extractRuntimeHint,
|
||||
extractRuntimeModel,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
resolveValidatorSessionModel,
|
||||
resolveValidatorThinkingLevel,
|
||||
resolveValidatorFallbackThinkingLevel,
|
||||
createResolvedAgentSession,
|
||||
wrapCustomToolsForPluginRuntime,
|
||||
} from "../agent-session-helpers.js";
|
||||
|
||||
@@ -621,6 +623,24 @@ describe("createResolvedAgentSession", () => {
|
||||
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 () => {
|
||||
const mockSession = { prompt: vi.fn() } as any;
|
||||
const createSessionMock = vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type ResolvedRuntime,
|
||||
} from "../runtime-resolution.js";
|
||||
import type { PluginRunner } from "../plugin-runner.js";
|
||||
import { createFnAgent } from "../pi.js";
|
||||
import type { PluginRuntimeRegistration } from "@fusion/core";
|
||||
|
||||
// Mock the logger to suppress output during tests
|
||||
@@ -114,6 +115,20 @@ describe("runtime-resolution", () => {
|
||||
const runtime = getDefaultPiRuntime();
|
||||
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()", () => {
|
||||
|
||||
@@ -44,24 +44,51 @@ describe("tool output budget wrapper", () => {
|
||||
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 () => {
|
||||
const source = "x".repeat(200);
|
||||
const large = await execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { overrides: { fn_budget_test: 300 } })[0]);
|
||||
const small = await execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { overrides: { fn_budget_test: 100 } })[0]);
|
||||
expect(large.content[0].text).toBe(source);
|
||||
expect(small.content[0].text.length).toBeLessThanOrEqual(100);
|
||||
it("honors a custom base budget and finite named overrides without double-marking", async () => {
|
||||
const source = "x".repeat(700);
|
||||
const custom = await execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { maxChars: 500 })[0]);
|
||||
const override = await execute(wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], {
|
||||
maxChars: 500,
|
||||
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/);
|
||||
const once = wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { overrides: { fn_budget_test: 100 } });
|
||||
const twice = wrapToolsWithOutputBudget(once, { overrides: { fn_budget_test: 100 } });
|
||||
const once = wrapToolsWithOutputBudget([toolWithResult({ content: [{ type: "text", text: source }] })], { maxChars: 100 });
|
||||
const twice = wrapToolsWithOutputBudget(once, { maxChars: 100 });
|
||||
const result = await execute(twice[0]);
|
||||
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([
|
||||
toolWithResult({ content: [{ type: "text", text: "x".repeat(DEFAULT_TOOL_OUTPUT_MAX_CHARS + 1) }] }),
|
||||
], {})![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.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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,6 +89,8 @@ export interface AgentRuntimeOptions {
|
||||
tools?: "coding" | "readonly";
|
||||
/** Additional custom tools to merge with the base toolset */
|
||||
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 */
|
||||
onText?: (delta: string) => void;
|
||||
/** Callback for thinking/thought output from the agent */
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
GROK_CLI_PROVIDER_ID,
|
||||
isGrokApiKeyFusionVisible,
|
||||
isTestModeActive,
|
||||
resolveAgentToolOutputMaxChars,
|
||||
resolveExecutionSettingsModel,
|
||||
resolveExecutorFallbackModel,
|
||||
resolveMergerSettingsModel,
|
||||
@@ -81,7 +82,7 @@ const RUNTIMES_WITH_INTERNAL_TOOL_GATING = new Set(["pi"]);
|
||||
*/
|
||||
export function wrapCustomToolsForPluginRuntime(
|
||||
tools: ToolDefinition[] | undefined,
|
||||
options: Pick<AgentRuntimeOptions, "actionGateContext" | "permanentAgentGating">,
|
||||
options: Pick<AgentRuntimeOptions, "actionGateContext" | "permanentAgentGating" | "toolOutputMaxChars">,
|
||||
logContext?: { runtimeId: string; sessionPurpose: string },
|
||||
): ToolDefinition[] | undefined {
|
||||
if (!tools || tools.length === 0) {
|
||||
@@ -97,10 +98,10 @@ export function wrapCustomToolsForPluginRuntime(
|
||||
const withRtk = wrapToolsWithRtkRewrite(tools);
|
||||
const withPermanent = wrapToolsWithPermanentAgentGating(withRtk, options.permanentAgentGating);
|
||||
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
|
||||
// per-result clamp here exactly once rather than letting plugin tool output bypass it.
|
||||
return wrapToolsWithOutputBudget(withActionGate);
|
||||
// setting-derived clamp here exactly once. A null budget leaves tools byte-identical.
|
||||
return wrapToolsWithOutputBudget(withActionGate, { maxChars: options.toolOutputMaxChars });
|
||||
}
|
||||
|
||||
function shouldWrapCustomToolsForRuntime(runtimeId: string): boolean {
|
||||
@@ -878,8 +879,17 @@ export async function createResolvedAgentSession(
|
||||
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).
|
||||
*/
|
||||
/*
|
||||
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 = {
|
||||
...runtimeOptionsRaw,
|
||||
toolOutputMaxChars: runtimeOptionsRaw.toolOutputMaxChars !== undefined
|
||||
? runtimeOptionsRaw.toolOutputMaxChars
|
||||
: resolveAgentToolOutputMaxChars(settings ?? {}),
|
||||
...(mergedSkillNames.length > 0 ? { skills: mergedSkillNames } : {}),
|
||||
};
|
||||
// FNXC:McpConfig 2026-06-25-22:06:
|
||||
|
||||
@@ -1037,6 +1037,8 @@ export interface AgentOptions {
|
||||
systemPromptLayers?: SystemPromptLayers;
|
||||
tools?: "coding" | "readonly";
|
||||
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:
|
||||
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:
|
||||
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
|
||||
integer cap; there is deliberately no unbounded sentinel or opt-out. The audit
|
||||
found no legitimate result that needs more than the shared 16,000-character cap.
|
||||
engine-injected tool result. Per-tool overrides remain finite positive integers;
|
||||
only the operator-level setting can select the explicit unlimited mode.
|
||||
|
||||
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>> = {};
|
||||
|
||||
@@ -1890,8 +1895,9 @@ export const TOOL_OUTPUT_BUDGET_OVERRIDES: Readonly<Record<string, number>> = {}
|
||||
*/
|
||||
export function wrapToolsWithOutputBudget(
|
||||
tools: ToolDefinition[],
|
||||
options: { overrides?: Readonly<Record<string, number | null | undefined>> } = {},
|
||||
options: { overrides?: Readonly<Record<string, number | null | undefined>>; maxChars?: number | null } = {},
|
||||
): ToolDefinition[] {
|
||||
if (options.maxChars === null) return tools;
|
||||
return tools.map((tool) => {
|
||||
const originalExecute = tool.execute as any;
|
||||
return {
|
||||
@@ -1910,7 +1916,11 @@ export function wrapToolsWithOutputBudget(
|
||||
});
|
||||
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 content = result.content.map((block: unknown) => block);
|
||||
textPositions.forEach((position, index) => {
|
||||
@@ -2574,9 +2584,12 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
|
||||
boundaryContext.worktreeProjectRoot,
|
||||
normalizedAdditionalSkillPaths,
|
||||
);
|
||||
// FNXC:ToolOutputBudget 2026-08-06-12:00:
|
||||
// Keep this outermost so policy-gate and boundary rejection text is bounded too.
|
||||
const customToolList: ToolDefinition[] = wrapToolsWithOutputBudget(boundaryWrappedTools);
|
||||
// FNXC:ToolOutputBudget 2026-08-06-16:00:
|
||||
// Keep this outermost so policy-gate and boundary rejection text is bounded too;
|
||||
// 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.
|
||||
// Prompt caching requires the tool list to be byte-identical across
|
||||
// sessions — reordering breaks cache prefix matching.
|
||||
|
||||
@@ -103,9 +103,15 @@ export class DefaultPiRuntime implements AgentRuntime {
|
||||
async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> {
|
||||
// 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.
|
||||
/*
|
||||
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({
|
||||
...options,
|
||||
mcpServers: normalizeAgentRuntimeMcpServers(options.mcpServers),
|
||||
...agentOptions,
|
||||
toolOutputMaxChars,
|
||||
mcpServers: normalizeAgentRuntimeMcpServers(mcpServers),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6049,6 +6049,10 @@
|
||||
"saveAIThinkingForPermanentAgents": " Save AI thinking for permanent agents ",
|
||||
"saveAIThinkingLogs": "Save AI thinking 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 ",
|
||||
"enableProactiveTaskChatHint": " When enabled, Task chat reports step progress, failures, reviews, and rollbacks in real time. Default: disabled. ",
|
||||
"updates": "Updates",
|
||||
|
||||
@@ -6044,7 +6044,11 @@
|
||||
"releaseChannel": "",
|
||||
"releaseChannelHelp": "",
|
||||
"channelStable": "",
|
||||
"channelBeta": ""
|
||||
"channelBeta": "",
|
||||
"agentToolOutputLimit": "",
|
||||
"agentToolOutputLimitHint": "",
|
||||
"noLimitOnAgentToolOutput": "",
|
||||
"noLimitOnAgentToolOutputHint": ""
|
||||
},
|
||||
"globalModels": {
|
||||
"allow": "",
|
||||
|
||||
@@ -6044,7 +6044,11 @@
|
||||
"releaseChannel": "",
|
||||
"releaseChannelHelp": "",
|
||||
"channelStable": "",
|
||||
"channelBeta": ""
|
||||
"channelBeta": "",
|
||||
"agentToolOutputLimit": "",
|
||||
"agentToolOutputLimitHint": "",
|
||||
"noLimitOnAgentToolOutput": "",
|
||||
"noLimitOnAgentToolOutputHint": ""
|
||||
},
|
||||
"globalModels": {
|
||||
"allow": "",
|
||||
|
||||
@@ -6044,7 +6044,11 @@
|
||||
"releaseChannel": "",
|
||||
"releaseChannelHelp": "",
|
||||
"channelStable": "",
|
||||
"channelBeta": ""
|
||||
"channelBeta": "",
|
||||
"agentToolOutputLimit": "",
|
||||
"agentToolOutputLimitHint": "",
|
||||
"noLimitOnAgentToolOutput": "",
|
||||
"noLimitOnAgentToolOutputHint": ""
|
||||
},
|
||||
"globalModels": {
|
||||
"allow": "",
|
||||
|
||||
@@ -6044,7 +6044,11 @@
|
||||
"releaseChannel": "",
|
||||
"releaseChannelHelp": "",
|
||||
"channelStable": "",
|
||||
"channelBeta": ""
|
||||
"channelBeta": "",
|
||||
"agentToolOutputLimit": "",
|
||||
"agentToolOutputLimitHint": "",
|
||||
"noLimitOnAgentToolOutput": "",
|
||||
"noLimitOnAgentToolOutputHint": ""
|
||||
},
|
||||
"globalModels": {
|
||||
"allow": "",
|
||||
|
||||
@@ -6044,7 +6044,11 @@
|
||||
"releaseChannel": "",
|
||||
"releaseChannelHelp": "",
|
||||
"channelStable": "",
|
||||
"channelBeta": ""
|
||||
"channelBeta": "",
|
||||
"agentToolOutputLimit": "",
|
||||
"agentToolOutputLimitHint": "",
|
||||
"noLimitOnAgentToolOutput": "",
|
||||
"noLimitOnAgentToolOutputHint": ""
|
||||
},
|
||||
"globalModels": {
|
||||
"allow": "",
|
||||
|
||||
4
packages/i18n/src/resources.d.ts
vendored
4
packages/i18n/src/resources.d.ts
vendored
@@ -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)."
|
||||
},
|
||||
"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. ",
|
||||
"autoReloadDashboardOnVersionChange": " Auto-reload dashboard on version change ",
|
||||
"cLIBinaryOnPATH": " CLI binary on PATH ",
|
||||
@@ -6059,6 +6061,8 @@ export default interface Resources {
|
||||
"globalDefaultTrackingRepo": "Global default tracking repo",
|
||||
"leaveBothThinkingTogglesOffToKeepThe": " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ",
|
||||
"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",
|
||||
"ownerRepo": "owner/repo",
|
||||
"projectsInheritThisValueWhenTheyDoNot": "Projects inherit this value when they do not set a project default tracking repo. No default — unset.",
|
||||
|
||||
Reference in New Issue
Block a user