Bump to 0.2.0 and make max tool-call iterations configurable
The 8-iteration-per-turn cap was hardcoded from before this session's feature work and too tight for genuine multi-file tasks, causing "Max tool-call iterations reached" on legitimately multi-step work. Raised the default to 25 and made it configurable via `locode config set maxIterations <n>` / $LOCODE_MAX_ITERATIONS, mirroring the existing contextWindow config pattern. Also bumps the version to 0.2.0 (package.json, --version, and the MCP client's self-reported identity) to reflect the substantial feature additions since 0.1.0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -98,12 +98,13 @@ Note: even models with genuine native tool-calling support occasionally emit a t
|
|||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
Config precedence: CLI flags > env vars (`LOCODE_BACKEND`, `LOCODE_MODEL`, `LOCODE_BASE_URL`) > persisted config file > defaults.
|
Config precedence: CLI flags > env vars (`LOCODE_BACKEND`, `LOCODE_MODEL`, `LOCODE_BASE_URL`, `LOCODE_CONTEXT_WINDOW`, `LOCODE_MAX_ITERATIONS`) > persisted config file > defaults.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
locode config set backend ollama
|
locode config set backend ollama
|
||||||
locode config set model qwen3-coder:30b
|
locode config set model qwen3-coder:30b
|
||||||
locode config set contextWindow 32768 # fallback size when auto-detection fails
|
locode config set contextWindow 32768 # fallback size when auto-detection fails
|
||||||
|
locode config set maxIterations 40 # max tool calls per turn before locode gives up (default 25)
|
||||||
locode config get
|
locode config get
|
||||||
locode config path
|
locode config path
|
||||||
```
|
```
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "locode",
|
"name": "locode",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Agentic coding CLI for local models served via Ollama and LM Studio",
|
"description": "Agentic coding CLI for local models served via Ollama and LM Studio",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|||||||
import type OpenAI from "openai";
|
import type OpenAI from "openai";
|
||||||
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
|
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
|
||||||
import type { ToolCallMode } from "../backend/capabilityProbe.js";
|
import type { ToolCallMode } from "../backend/capabilityProbe.js";
|
||||||
import { DEFAULT_CONTEXT_WINDOW } from "../config/defaults.js";
|
import { DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_ITERATIONS } from "../config/defaults.js";
|
||||||
import type { SessionRecord } from "../persistence/sessionStore.js";
|
import type { SessionRecord } from "../persistence/sessionStore.js";
|
||||||
import { PermissionManager } from "../permissions/permissionManager.js";
|
import { PermissionManager } from "../permissions/permissionManager.js";
|
||||||
import type { ConfirmFn } from "../permissions/types.js";
|
import type { ConfirmFn } from "../permissions/types.js";
|
||||||
@@ -49,6 +49,7 @@ export function createSession(
|
|||||||
tools: ToolDef[] = TOOLS,
|
tools: ToolDef[] = TOOLS,
|
||||||
contextWindow: number = DEFAULT_CONTEXT_WINDOW,
|
contextWindow: number = DEFAULT_CONTEXT_WINDOW,
|
||||||
contextWindowIsEstimate: boolean = true,
|
contextWindowIsEstimate: boolean = true,
|
||||||
|
maxIterations: number = DEFAULT_MAX_ITERATIONS,
|
||||||
): Session {
|
): Session {
|
||||||
const toolset = buildToolSet(tools);
|
const toolset = buildToolSet(tools);
|
||||||
const messages: ChatCompletionMessageParam[] = [{ role: "system", content: buildSystemPrompt(toolset.tools, mode) }];
|
const messages: ChatCompletionMessageParam[] = [{ role: "system", content: buildSystemPrompt(toolset.tools, mode) }];
|
||||||
@@ -60,7 +61,7 @@ export function createSession(
|
|||||||
cwd,
|
cwd,
|
||||||
mode,
|
mode,
|
||||||
messages,
|
messages,
|
||||||
maxIterations: 8,
|
maxIterations,
|
||||||
permissions: new PermissionManager(),
|
permissions: new PermissionManager(),
|
||||||
confirm,
|
confirm,
|
||||||
toolset,
|
toolset,
|
||||||
@@ -82,6 +83,7 @@ export function createSessionFromRecord(
|
|||||||
tools: ToolDef[] = TOOLS,
|
tools: ToolDef[] = TOOLS,
|
||||||
contextWindow: number = DEFAULT_CONTEXT_WINDOW,
|
contextWindow: number = DEFAULT_CONTEXT_WINDOW,
|
||||||
contextWindowIsEstimate: boolean = true,
|
contextWindowIsEstimate: boolean = true,
|
||||||
|
maxIterations: number = DEFAULT_MAX_ITERATIONS,
|
||||||
): Session {
|
): Session {
|
||||||
const toolset = buildToolSet(tools);
|
const toolset = buildToolSet(tools);
|
||||||
const messages: ChatCompletionMessageParam[] = [
|
const messages: ChatCompletionMessageParam[] = [
|
||||||
@@ -96,7 +98,7 @@ export function createSessionFromRecord(
|
|||||||
cwd,
|
cwd,
|
||||||
mode: record.mode,
|
mode: record.mode,
|
||||||
messages,
|
messages,
|
||||||
maxIterations: 8,
|
maxIterations,
|
||||||
permissions: new PermissionManager(),
|
permissions: new PermissionManager(),
|
||||||
confirm,
|
confirm,
|
||||||
toolset,
|
toolset,
|
||||||
|
|||||||
+7
-7
@@ -18,7 +18,7 @@ function addBackendOptions(cmd: Command): Command {
|
|||||||
program
|
program
|
||||||
.name("locode")
|
.name("locode")
|
||||||
.description("Agentic coding CLI for local models via Ollama and LM Studio")
|
.description("Agentic coding CLI for local models via Ollama and LM Studio")
|
||||||
.version("0.1.0");
|
.version("0.2.0");
|
||||||
|
|
||||||
addBackendOptions(program)
|
addBackendOptions(program)
|
||||||
.option("-m, --model <name>", "model name as known to the backend")
|
.option("-m, --model <name>", "model name as known to the backend")
|
||||||
@@ -108,20 +108,20 @@ configCmd
|
|||||||
|
|
||||||
configCmd
|
configCmd
|
||||||
.command("set <key> <value>")
|
.command("set <key> <value>")
|
||||||
.description("Persist a config value (backend, model, baseUrl, contextWindow)")
|
.description("Persist a config value (backend, model, baseUrl, contextWindow, maxIterations)")
|
||||||
.action((key: string, value: string) => {
|
.action((key: string, value: string) => {
|
||||||
if (key !== "backend" && key !== "model" && key !== "baseUrl" && key !== "contextWindow") {
|
if (key !== "backend" && key !== "model" && key !== "baseUrl" && key !== "contextWindow" && key !== "maxIterations") {
|
||||||
console.error(`Unknown config key "${key}". Valid keys: backend, model, baseUrl, contextWindow`);
|
console.error(`Unknown config key "${key}". Valid keys: backend, model, baseUrl, contextWindow, maxIterations`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const stored = loadStoredConfig();
|
const stored = loadStoredConfig();
|
||||||
if (key === "contextWindow") {
|
if (key === "contextWindow" || key === "maxIterations") {
|
||||||
const n = Number(value);
|
const n = Number(value);
|
||||||
if (!Number.isFinite(n) || n <= 0) {
|
if (!Number.isFinite(n) || n <= 0) {
|
||||||
console.error(`contextWindow must be a positive number, got "${value}".`);
|
console.error(`${key} must be a positive number, got "${value}".`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
stored.contextWindow = n;
|
stored[key] = n;
|
||||||
} else {
|
} else {
|
||||||
stored[key] = value;
|
stored[key] = value;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -1,4 +1,4 @@
|
|||||||
import { DEFAULT_CONTEXT_WINDOW, KNOWN_BACKENDS, type BackendName } from "./defaults.js";
|
import { DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_ITERATIONS, KNOWN_BACKENDS, type BackendName } from "./defaults.js";
|
||||||
import { loadStoredConfig } from "./store.js";
|
import { loadStoredConfig } from "./store.js";
|
||||||
|
|
||||||
export class ConfigError extends Error {}
|
export class ConfigError extends Error {}
|
||||||
@@ -41,3 +41,12 @@ export function resolveContextWindowDefault(): number {
|
|||||||
if (typeof stored.contextWindow === "number" && stored.contextWindow > 0) return stored.contextWindow;
|
if (typeof stored.contextWindow === "number" && stored.contextWindow > 0) return stored.contextWindow;
|
||||||
return DEFAULT_CONTEXT_WINDOW;
|
return DEFAULT_CONTEXT_WINDOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Max tool calls allowed per turn before locode gives up. */
|
||||||
|
export function resolveMaxIterations(): number {
|
||||||
|
const stored = loadStoredConfig();
|
||||||
|
const envValue = Number(process.env.LOCODE_MAX_ITERATIONS);
|
||||||
|
if (Number.isFinite(envValue) && envValue > 0) return envValue;
|
||||||
|
if (typeof stored.maxIterations === "number" && stored.maxIterations > 0) return stored.maxIterations;
|
||||||
|
return DEFAULT_MAX_ITERATIONS;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,3 +11,7 @@ export type BackendName = keyof typeof KNOWN_BACKENDS;
|
|||||||
/** Used when the context window can't be auto-detected from the backend (see backend/contextWindow.ts)
|
/** Used when the context window can't be auto-detected from the backend (see backend/contextWindow.ts)
|
||||||
* and the user hasn't configured one — a conservative size common among smaller local models. */
|
* and the user hasn't configured one — a conservative size common among smaller local models. */
|
||||||
export const DEFAULT_CONTEXT_WINDOW = 8192;
|
export const DEFAULT_CONTEXT_WINDOW = 8192;
|
||||||
|
|
||||||
|
/** Max tool calls per turn before locode gives up rather than looping forever. 25 gives real
|
||||||
|
* multi-file tasks room to breathe; still bounded so a genuinely stuck model fails fast. */
|
||||||
|
export const DEFAULT_MAX_ITERATIONS = 25;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export interface StoredConfig {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
/** Fallback context window size (in tokens) used when it can't be auto-detected from the backend. */
|
/** Fallback context window size (in tokens) used when it can't be auto-detected from the backend. */
|
||||||
contextWindow?: number;
|
contextWindow?: number;
|
||||||
|
/** Max tool calls allowed per turn before locode gives up rather than looping forever. */
|
||||||
|
maxIterations?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paths = envPaths("locode", { suffix: "" });
|
const paths = envPaths("locode", { suffix: "" });
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ export async function connectMcpServer(name: string, config: McpServerConfig): P
|
|||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new Client({ name: "locode", version: "0.1.0" });
|
const client = new Client({ name: "locode", version: "0.2.0" });
|
||||||
await client.connect(transport);
|
await client.connect(transport);
|
||||||
const { tools } = await client.listTools();
|
const { tools } = await client.listTools();
|
||||||
return { name, client, transport, tools: tools as McpToolInfo[] };
|
return { name, client, transport, tools: tools as McpToolInfo[] };
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type { ToolCallMode } from "../../backend/capabilityProbe.js";
|
|||||||
import { setCachedMode } from "../../backend/capabilityCache.js";
|
import { setCachedMode } from "../../backend/capabilityCache.js";
|
||||||
import { resolveContextWindow } from "../../backend/contextWindow.js";
|
import { resolveContextWindow } from "../../backend/contextWindow.js";
|
||||||
import { resolveToolCallMode } from "../../backend/resolveMode.js";
|
import { resolveToolCallMode } from "../../backend/resolveMode.js";
|
||||||
|
import { resolveMaxIterations } from "../../config/config.js";
|
||||||
import { KNOWN_BACKENDS, type BackendName } from "../../config/defaults.js";
|
import { KNOWN_BACKENDS, type BackendName } from "../../config/defaults.js";
|
||||||
import { getMcpStatuses } from "../../mcp/manager.js";
|
import { getMcpStatuses } from "../../mcp/manager.js";
|
||||||
import type { PermissionDecision, PermissionMode } from "../../permissions/types.js";
|
import type { PermissionDecision, PermissionMode } from "../../permissions/types.js";
|
||||||
@@ -150,6 +151,7 @@ export function App({
|
|||||||
[...TOOLS, ...mcpTools],
|
[...TOOLS, ...mcpTools],
|
||||||
contextWindow.value,
|
contextWindow.value,
|
||||||
contextWindow.isEstimate,
|
contextWindow.isEstimate,
|
||||||
|
resolveMaxIterations(),
|
||||||
);
|
);
|
||||||
push({ kind: "banner", cwd, model, backend: baseURLRef.current });
|
push({ kind: "banner", cwd, model, backend: baseURLRef.current });
|
||||||
setPhase("input");
|
setPhase("input");
|
||||||
@@ -191,6 +193,7 @@ export function App({
|
|||||||
[...TOOLS, ...mcpTools],
|
[...TOOLS, ...mcpTools],
|
||||||
contextWindow.value,
|
contextWindow.value,
|
||||||
contextWindow.isEstimate,
|
contextWindow.isEstimate,
|
||||||
|
resolveMaxIterations(),
|
||||||
);
|
);
|
||||||
push({
|
push({
|
||||||
kind: "banner",
|
kind: "banner",
|
||||||
|
|||||||
Reference in New Issue
Block a user