mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-24 21:41:08 +02:00
refactor(x): drop limits DI, read setting at default site; async fs
Address review: remove ITurnLimitsResolver, its bridge and container registration. createTurn now defaults an omitted maxModelCalls straight from the turn-limits config (lazy import, built-in fallback), with no chat/headless distinction in the runtime — the chat UI passes its optional chat override explicitly via the existing sessions:sendMessage maxModelCalls field. turn_limits.ts now uses fs/promises throughout.
This commit is contained in:
parent
7cb9c478ac
commit
7b05bf80cb
12 changed files with 91 additions and 145 deletions
|
|
@ -2561,10 +2561,10 @@ export function setupIpcHandlers() {
|
|||
return { success: true };
|
||||
},
|
||||
'turnLimits:getSettings': async () => {
|
||||
return loadTurnLimitsSettings();
|
||||
return await loadTurnLimitsSettings();
|
||||
},
|
||||
'turnLimits:setSettings': async (_event, args) => {
|
||||
saveTurnLimitsSettings(args);
|
||||
await saveTurnLimitsSettings(args);
|
||||
return { success: true };
|
||||
},
|
||||
// Embedded browser handlers (WebContentsView + navigation)
|
||||
|
|
|
|||
|
|
@ -3021,6 +3021,13 @@ function App() {
|
|||
// via the agent resolver; keep them session-sticky where possible so the
|
||||
// provider prefix cache survives across turns.
|
||||
const reasoningEffort = reasoningEffortByTabRef.current.get(submitTabId)
|
||||
// The runtime defaults omitted maxModelCalls to the global limit; the
|
||||
// chat-specific override is the UI's job to pass explicitly. A failed
|
||||
// settings read just falls back to the global limit.
|
||||
const chatMaxModelCalls = await window.ipc
|
||||
.invoke('turnLimits:getSettings', null)
|
||||
.then((settings) => settings.chatMaxModelCalls)
|
||||
.catch(() => undefined)
|
||||
const sendConfig = {
|
||||
agent: {
|
||||
agentId,
|
||||
|
|
@ -3039,6 +3046,7 @@ function App() {
|
|||
},
|
||||
autoPermission: (permissionMode ?? 'manual') === 'auto',
|
||||
...(reasoningEffort ? { reasoningEffort } : {}),
|
||||
...(chatMaxModelCalls !== undefined ? { maxModelCalls: chatMaxModelCalls } : {}),
|
||||
}
|
||||
const userMessageContextFor = (middlePane: Awaited<ReturnType<typeof buildMiddlePaneContext>>) => ({
|
||||
currentDateTime: new Date().toISOString(),
|
||||
|
|
|
|||
|
|
@ -512,13 +512,13 @@ Rules:
|
|||
- `input` is the user message that defines this turn boundary.
|
||||
- `autoPermission` defaults to `false` before persistence.
|
||||
- `humanAvailable` is required explicitly.
|
||||
- `maxModelCalls`, when omitted, is resolved at creation from the user's
|
||||
turn-limit settings (`config/turn_limits.json`) via the injected
|
||||
`ITurnLimitsResolver`: turns with `humanAvailable: true` use the chat
|
||||
override when set, otherwise the global limit; headless turns always use
|
||||
the global limit. Without a resolver (tests) it defaults to `50`. Spawned
|
||||
sub-agents default to the global limit, which also caps the budget a
|
||||
parent may grant them. Settings changes affect only newly created turns.
|
||||
- `maxModelCalls`, when omitted, defaults at creation to the user's global
|
||||
model-call limit (`config/turn_limits.json`, built-in default `50`). The
|
||||
runtime makes no chat/headless distinction: the optional chat override is
|
||||
the chat UI's job — its send path passes an explicit `maxModelCalls`.
|
||||
Spawned sub-agents default to the global limit, which also caps the
|
||||
budget a parent may grant them. Settings changes affect only newly
|
||||
created turns.
|
||||
- Persisted values are fully resolved and immutable.
|
||||
|
||||
The capability is named `humanAvailable`, not `headless`. `headless` describes
|
||||
|
|
|
|||
|
|
@ -45,13 +45,13 @@ async function writeSettings(content: string): Promise<void> {
|
|||
describe("loadTurnLimitsSettings", () => {
|
||||
it("defaults to the built-in limit (50) when no file exists", async () => {
|
||||
const { loadTurnLimitsSettings } = await loadTurnLimits();
|
||||
expect(loadTurnLimitsSettings()).toEqual({ maxModelCalls: 50 });
|
||||
expect(await loadTurnLimitsSettings()).toEqual({ maxModelCalls: 50 });
|
||||
});
|
||||
|
||||
it("reads persisted settings", async () => {
|
||||
await writeSettings(JSON.stringify({ maxModelCalls: 60, chatMaxModelCalls: 10 }));
|
||||
const { loadTurnLimitsSettings } = await loadTurnLimits();
|
||||
expect(loadTurnLimitsSettings()).toEqual({
|
||||
expect(await loadTurnLimitsSettings()).toEqual({
|
||||
maxModelCalls: 60,
|
||||
chatMaxModelCalls: 10,
|
||||
});
|
||||
|
|
@ -60,7 +60,7 @@ describe("loadTurnLimitsSettings", () => {
|
|||
it("fills a missing global limit from the default", async () => {
|
||||
await writeSettings(JSON.stringify({ chatMaxModelCalls: 5 }));
|
||||
const { loadTurnLimitsSettings } = await loadTurnLimits();
|
||||
expect(loadTurnLimitsSettings()).toEqual({
|
||||
expect(await loadTurnLimitsSettings()).toEqual({
|
||||
maxModelCalls: 50,
|
||||
chatMaxModelCalls: 5,
|
||||
});
|
||||
|
|
@ -69,21 +69,21 @@ describe("loadTurnLimitsSettings", () => {
|
|||
it("falls back to defaults on a corrupt file", async () => {
|
||||
await writeSettings("{not json");
|
||||
const { loadTurnLimitsSettings } = await loadTurnLimits();
|
||||
expect(loadTurnLimitsSettings()).toEqual({ maxModelCalls: 50 });
|
||||
expect(await loadTurnLimitsSettings()).toEqual({ maxModelCalls: 50 });
|
||||
});
|
||||
|
||||
it("falls back to defaults on out-of-range values", async () => {
|
||||
await writeSettings(JSON.stringify({ maxModelCalls: 5000 }));
|
||||
const { loadTurnLimitsSettings } = await loadTurnLimits();
|
||||
expect(loadTurnLimitsSettings()).toEqual({ maxModelCalls: 50 });
|
||||
expect(await loadTurnLimitsSettings()).toEqual({ maxModelCalls: 50 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveTurnLimitsSettings", () => {
|
||||
it("persists valid settings for the next load", async () => {
|
||||
const { saveTurnLimitsSettings, loadTurnLimitsSettings } = await loadTurnLimits();
|
||||
saveTurnLimitsSettings({ maxModelCalls: 80, chatMaxModelCalls: 15 });
|
||||
expect(loadTurnLimitsSettings()).toEqual({
|
||||
await saveTurnLimitsSettings({ maxModelCalls: 80, chatMaxModelCalls: 15 });
|
||||
expect(await loadTurnLimitsSettings()).toEqual({
|
||||
maxModelCalls: 80,
|
||||
chatMaxModelCalls: 15,
|
||||
});
|
||||
|
|
@ -91,36 +91,10 @@ describe("saveTurnLimitsSettings", () => {
|
|||
|
||||
it("rejects out-of-range values", async () => {
|
||||
const { saveTurnLimitsSettings } = await loadTurnLimits();
|
||||
expect(() => saveTurnLimitsSettings({ maxModelCalls: 0 })).toThrow();
|
||||
expect(() => saveTurnLimitsSettings({ maxModelCalls: 501 })).toThrow();
|
||||
expect(() =>
|
||||
await expect(saveTurnLimitsSettings({ maxModelCalls: 0 })).rejects.toThrow();
|
||||
await expect(saveTurnLimitsSettings({ maxModelCalls: 501 })).rejects.toThrow();
|
||||
await expect(
|
||||
saveTurnLimitsSettings({ maxModelCalls: 20, chatMaxModelCalls: 501 }),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMaxModelCalls", () => {
|
||||
it("uses the global limit for headless work", async () => {
|
||||
await writeSettings(JSON.stringify({ maxModelCalls: 60, chatMaxModelCalls: 10 }));
|
||||
const { resolveMaxModelCalls } = await loadTurnLimits();
|
||||
expect(resolveMaxModelCalls({ humanAvailable: false })).toBe(60);
|
||||
});
|
||||
|
||||
it("uses the chat override for interactive turns when set", async () => {
|
||||
await writeSettings(JSON.stringify({ maxModelCalls: 60, chatMaxModelCalls: 10 }));
|
||||
const { resolveMaxModelCalls } = await loadTurnLimits();
|
||||
expect(resolveMaxModelCalls({ humanAvailable: true })).toBe(10);
|
||||
});
|
||||
|
||||
it("falls back to the global limit for chat when no override is set", async () => {
|
||||
await writeSettings(JSON.stringify({ maxModelCalls: 60 }));
|
||||
const { resolveMaxModelCalls } = await loadTurnLimits();
|
||||
expect(resolveMaxModelCalls({ humanAvailable: true })).toBe(60);
|
||||
});
|
||||
|
||||
it("resolves 50 everywhere with no settings file", async () => {
|
||||
const { resolveMaxModelCalls } = await loadTurnLimits();
|
||||
expect(resolveMaxModelCalls({ humanAvailable: true })).toBe(50);
|
||||
expect(resolveMaxModelCalls({ humanAvailable: false })).toBe(50);
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import fs from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import {
|
||||
TurnLimitsSettingsSchema,
|
||||
|
|
@ -14,41 +14,29 @@ const TURN_LIMITS_CONFIG_PATH = path.join(WorkDir, 'config', 'turn_limits.json')
|
|||
* limit DEFAULT_MAX_MODEL_CALLS, no chat override) when the file is absent
|
||||
* or malformed.
|
||||
*/
|
||||
export function loadTurnLimitsSettings(): TurnLimitsSettings {
|
||||
export async function loadTurnLimitsSettings(): Promise<TurnLimitsSettings> {
|
||||
try {
|
||||
if (fs.existsSync(TURN_LIMITS_CONFIG_PATH)) {
|
||||
const content = fs.readFileSync(TURN_LIMITS_CONFIG_PATH, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
return TurnLimitsSettingsSchema.parse({
|
||||
...DEFAULT_TURN_LIMITS_SETTINGS,
|
||||
...(parsed && typeof parsed === 'object' ? parsed : {}),
|
||||
});
|
||||
}
|
||||
const content = await fs.readFile(TURN_LIMITS_CONFIG_PATH, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
return TurnLimitsSettingsSchema.parse({
|
||||
...DEFAULT_TURN_LIMITS_SETTINGS,
|
||||
...(parsed && typeof parsed === 'object' ? parsed : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[TurnLimits] Error loading turn limit settings:', error);
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('[TurnLimits] Error loading turn limit settings:', error);
|
||||
}
|
||||
return DEFAULT_TURN_LIMITS_SETTINGS;
|
||||
}
|
||||
return DEFAULT_TURN_LIMITS_SETTINGS;
|
||||
}
|
||||
|
||||
export function saveTurnLimitsSettings(settings: TurnLimitsSettings): void {
|
||||
export async function saveTurnLimitsSettings(
|
||||
settings: TurnLimitsSettings,
|
||||
): Promise<void> {
|
||||
const validated = TurnLimitsSettingsSchema.parse(settings);
|
||||
const dir = path.dirname(TURN_LIMITS_CONFIG_PATH);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(TURN_LIMITS_CONFIG_PATH, JSON.stringify(validated, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* The effective model-call limit for a new turn, by execution context.
|
||||
* Interactive chat turns (a human is present) use the chat override when
|
||||
* set; everything else — headless/knowledge work, sub-agents — uses the
|
||||
* global limit.
|
||||
*/
|
||||
export function resolveMaxModelCalls(context: { humanAvailable: boolean }): number {
|
||||
const settings = loadTurnLimitsSettings();
|
||||
if (context.humanAvailable && settings.chatMaxModelCalls !== undefined) {
|
||||
return settings.chatMaxModelCalls;
|
||||
}
|
||||
return settings.maxModelCalls;
|
||||
await fs.mkdir(path.dirname(TURN_LIMITS_CONFIG_PATH), { recursive: true });
|
||||
await fs.writeFile(
|
||||
TURN_LIMITS_CONFIG_PATH,
|
||||
JSON.stringify(validated, null, 2),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,8 +50,6 @@ import { RealModelRegistry } from "../runtime/turns/bridges/real-model-registry.
|
|||
import { RealToolRegistry } from "../runtime/turns/bridges/real-tool-registry.js";
|
||||
import { RealPermissionChecker } from "../runtime/turns/bridges/real-permission-checker.js";
|
||||
import { RealPermissionClassifier } from "../runtime/turns/bridges/real-permission-classifier.js";
|
||||
import { RealTurnLimitsResolver } from "../runtime/turns/bridges/real-turn-limits-resolver.js";
|
||||
import type { ITurnLimitsResolver } from "../runtime/turns/turn-limits-resolver.js";
|
||||
import { FSSessionRepo } from "../runtime/sessions/fs-repo.js";
|
||||
import type { ISessionRepo } from "../runtime/sessions/repo.js";
|
||||
import { EmitterSessionBus, type ISessionBus } from "../runtime/sessions/bus.js";
|
||||
|
|
@ -142,7 +140,6 @@ container.register({
|
|||
toolRegistry: asFunction<IToolRegistry>(() => new RealToolRegistry()).singleton(),
|
||||
permissionChecker: asFunction<IPermissionChecker>(() => new RealPermissionChecker()).singleton(),
|
||||
permissionClassifier: asFunction<IPermissionClassifier>(() => new RealPermissionClassifier()).singleton(),
|
||||
turnLimitsResolver: asFunction<ITurnLimitsResolver>(() => new RealTurnLimitsResolver()).singleton(),
|
||||
turnRuntime: asClass<ITurnRuntime>(TurnRuntime).singleton(),
|
||||
sessionRepo: asClass<ISessionRepo>(FSSessionRepo).singleton(),
|
||||
sessionBus: asClass<ISessionBus>(EmitterSessionBus).singleton(),
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ async function resolveServices(): Promise<
|
|||
headlessRunner: await lazyResolve<
|
||||
import("./headless.js").IHeadlessAgentRunner
|
||||
>("headlessAgentRunner"),
|
||||
globalMaxModelCalls: loadTurnLimitsSettings().maxModelCalls,
|
||||
globalMaxModelCalls: (await loadTurnLimitsSettings()).maxModelCalls,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,4 +3,3 @@ export * from "./real-model-registry.js";
|
|||
export * from "./real-permission-checker.js";
|
||||
export * from "./real-permission-classifier.js";
|
||||
export * from "./real-tool-registry.js";
|
||||
export * from "./real-turn-limits-resolver.js";
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import { resolveMaxModelCalls } from "../../../config/turn_limits.js";
|
||||
import type { ITurnLimitsResolver } from "../turn-limits-resolver.js";
|
||||
|
||||
// Settings-backed limits resolver: reads config/turn_limits.json on every
|
||||
// resolve so a settings change applies to the next created turn without a
|
||||
// restart. Turns that already exist keep the limit persisted in their
|
||||
// turn_created event.
|
||||
export class RealTurnLimitsResolver implements ITurnLimitsResolver {
|
||||
resolve(context: { humanAvailable: boolean }): number {
|
||||
return resolveMaxModelCalls(context);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
MODEL_CALL_LIMIT_ERROR_CODE,
|
||||
|
|
@ -40,7 +40,16 @@ import type {
|
|||
SyncRuntimeTool,
|
||||
ToolExecutionContext,
|
||||
} from "./tool-registry.js";
|
||||
import type { ITurnLimitsResolver } from "./turn-limits-resolver.js";
|
||||
|
||||
// createTurn defaults an omitted maxModelCalls from the fs-backed settings
|
||||
// module; mock it so tests stay hermetic (no real WorkDir reads) and can
|
||||
// steer the configured global limit.
|
||||
const turnLimitsMock = vi.hoisted(() => ({ maxModelCalls: 50 }));
|
||||
vi.mock("../../config/turn_limits.js", () => ({
|
||||
loadTurnLimitsSettings: async () => ({
|
||||
maxModelCalls: turnLimitsMock.maxModelCalls,
|
||||
}),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixtures
|
||||
|
|
@ -323,7 +332,6 @@ function makeRuntime(opts: {
|
|||
idStart?: number;
|
||||
modelRegistry?: IModelRegistry;
|
||||
toolRegistry?: IToolRegistry;
|
||||
turnLimitsResolver?: ITurnLimitsResolver;
|
||||
} = {}) {
|
||||
const repo = opts.repo ?? new InMemoryTurnRepo();
|
||||
const models = new FakeModelRegistry(opts.models ?? []);
|
||||
|
|
@ -345,7 +353,6 @@ function makeRuntime(opts: {
|
|||
lifecycleBus: bus,
|
||||
turnEventBus,
|
||||
usageReporter: usage,
|
||||
turnLimitsResolver: opts.turnLimitsResolver,
|
||||
});
|
||||
return { runtime, repo, models, checker, classifier, bus, turnEventBus, usage };
|
||||
}
|
||||
|
|
@ -3069,13 +3076,6 @@ describe("turn event bus", () => {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("model-call limit resolution", () => {
|
||||
// Chat turns (humanAvailable) and headless turns resolve through the
|
||||
// injected settings-backed resolver; an explicit maxModelCalls always
|
||||
// wins; without a resolver the built-in default (20) applies.
|
||||
const contextualResolver: ITurnLimitsResolver = {
|
||||
resolve: ({ humanAvailable }) => (humanAvailable ? 7 : 42),
|
||||
};
|
||||
|
||||
async function createdMaxModelCalls(
|
||||
runtime: TurnRuntime,
|
||||
repo: InMemoryTurnRepo,
|
||||
|
|
@ -3087,22 +3087,23 @@ describe("model-call limit resolution", () => {
|
|||
return created.config.maxModelCalls;
|
||||
}
|
||||
|
||||
it("resolves an omitted limit by execution context", async () => {
|
||||
const { runtime, repo } = makeRuntime({
|
||||
turnLimitsResolver: contextualResolver,
|
||||
});
|
||||
expect(
|
||||
await createdMaxModelCalls(runtime, repo, { humanAvailable: true }),
|
||||
).toBe(7);
|
||||
expect(
|
||||
await createdMaxModelCalls(runtime, repo, { humanAvailable: false }),
|
||||
).toBe(42);
|
||||
it("an omitted limit defaults to the configured global limit", async () => {
|
||||
turnLimitsMock.maxModelCalls = 42;
|
||||
try {
|
||||
const { runtime, repo } = makeRuntime();
|
||||
expect(
|
||||
await createdMaxModelCalls(runtime, repo, { humanAvailable: true }),
|
||||
).toBe(42);
|
||||
expect(
|
||||
await createdMaxModelCalls(runtime, repo, { humanAvailable: false }),
|
||||
).toBe(42);
|
||||
} finally {
|
||||
turnLimitsMock.maxModelCalls = 50;
|
||||
}
|
||||
});
|
||||
|
||||
it("an explicit per-call limit wins over the resolver", async () => {
|
||||
const { runtime, repo } = makeRuntime({
|
||||
turnLimitsResolver: contextualResolver,
|
||||
});
|
||||
it("an explicit per-call limit wins over the setting", async () => {
|
||||
const { runtime, repo } = makeRuntime();
|
||||
expect(
|
||||
await createdMaxModelCalls(runtime, repo, {
|
||||
humanAvailable: true,
|
||||
|
|
@ -3110,11 +3111,4 @@ describe("model-call limit resolution", () => {
|
|||
}),
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
it("falls back to the built-in default without a resolver", async () => {
|
||||
const { runtime, repo } = makeRuntime();
|
||||
expect(
|
||||
await createdMaxModelCalls(runtime, repo, { humanAvailable: true }),
|
||||
).toBe(50);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ import type { IPermissionChecker, IPermissionClassifier } from "./permission.js"
|
|||
import type { ITurnRepo } from "./repo.js";
|
||||
import { HotStream } from "./stream.js";
|
||||
import type { IToolRegistry, RuntimeTool, SyncRuntimeTool } from "./tool-registry.js";
|
||||
import type { ITurnLimitsResolver } from "./turn-limits-resolver.js";
|
||||
|
||||
type TEvent = z.infer<typeof TurnEvent>;
|
||||
|
||||
|
|
@ -74,8 +73,6 @@ export interface TurnRuntimeDependencies {
|
|||
lifecycleBus: ITurnLifecycleBus;
|
||||
turnEventBus: ITurnEventBus;
|
||||
usageReporter: IUsageReporter;
|
||||
// Optional: absent (tests) falls back to DEFAULT_MAX_MODEL_CALLS.
|
||||
turnLimitsResolver?: ITurnLimitsResolver;
|
||||
}
|
||||
|
||||
// Immutable dependency container: holds no mutable per-turn state. All active
|
||||
|
|
@ -93,7 +90,6 @@ export class TurnRuntime implements ITurnRuntime {
|
|||
private readonly lifecycleBus: ITurnLifecycleBus;
|
||||
private readonly turnEventBus: ITurnEventBus;
|
||||
private readonly usageReporter: IUsageReporter;
|
||||
private readonly turnLimitsResolver?: ITurnLimitsResolver;
|
||||
|
||||
constructor({
|
||||
turnRepo,
|
||||
|
|
@ -108,7 +104,6 @@ export class TurnRuntime implements ITurnRuntime {
|
|||
lifecycleBus,
|
||||
turnEventBus,
|
||||
usageReporter,
|
||||
turnLimitsResolver,
|
||||
}: TurnRuntimeDependencies) {
|
||||
this.turnRepo = turnRepo;
|
||||
this.idGenerator = idGenerator;
|
||||
|
|
@ -122,7 +117,6 @@ export class TurnRuntime implements ITurnRuntime {
|
|||
this.lifecycleBus = lifecycleBus;
|
||||
this.turnEventBus = turnEventBus;
|
||||
this.usageReporter = usageReporter;
|
||||
this.turnLimitsResolver = turnLimitsResolver;
|
||||
}
|
||||
|
||||
async createTurn(input: CreateTurnInput): Promise<string> {
|
||||
|
|
@ -171,10 +165,7 @@ export class TurnRuntime implements ITurnRuntime {
|
|||
humanAvailable: input.config.humanAvailable,
|
||||
maxModelCalls:
|
||||
input.config.maxModelCalls ??
|
||||
this.turnLimitsResolver?.resolve({
|
||||
humanAvailable: input.config.humanAvailable,
|
||||
}) ??
|
||||
DEFAULT_MAX_MODEL_CALLS,
|
||||
(await defaultMaxModelCalls()),
|
||||
...(input.config.reasoningEffort === undefined
|
||||
? {}
|
||||
: { reasoningEffort: input.config.reasoningEffort }),
|
||||
|
|
@ -1370,6 +1361,21 @@ function parseToolAdditions(
|
|||
return parsed.success ? parsed.data.toolAdditions : undefined;
|
||||
}
|
||||
|
||||
// The default budget for turns created without an explicit maxModelCalls:
|
||||
// the user's configured global limit (config/turn_limits.json). Loaded
|
||||
// lazily so this module doesn't statically pull in the fs-backed config;
|
||||
// any load problem falls back to the built-in default.
|
||||
async function defaultMaxModelCalls(): Promise<number> {
|
||||
try {
|
||||
const { loadTurnLimitsSettings } = await import(
|
||||
"../../config/turn_limits.js"
|
||||
);
|
||||
return (await loadTurnLimitsSettings()).maxModelCalls;
|
||||
} catch {
|
||||
return DEFAULT_MAX_MODEL_CALLS;
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
// Provider API errors carry the actual upstream failure in responseBody
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
// Resolves the effective model-call limit for a new turn when the caller
|
||||
// didn't pass an explicit maxModelCalls. The execution context is the
|
||||
// humanAvailable flag: true for interactive chat turns, false for
|
||||
// headless/autonomous work. The real bridge reads the user's settings;
|
||||
// tests construct TurnRuntime without one and get the built-in default.
|
||||
export interface ITurnLimitsResolver {
|
||||
resolve(context: { humanAvailable: boolean }): number;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue