@kmugalkhod/lightcode 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +28 -0
  2. package/cli.js +2083 -1115
  3. package/package.json +1 -1
  4. package/server.js +571 -316
package/server.js CHANGED
@@ -147,7 +147,7 @@ var init_src = __esm(() => {
147
147
  });
148
148
 
149
149
  // packages/ai/src/constants.ts
150
- var MAX_TOOL_TEXT_OUTPUT_CHARS = 12000, DEFAULT_TOOL_TEXT_OUTPUT_CHARS = 6000, MAX_TOOL_LIST_ENTRIES = 200, MAX_TOOL_SEARCH_RESULTS = 200, ANTHROPIC_TOOL_OPTIONAL_PARAMETER_BUDGET = 24;
150
+ var MAX_TOOL_TEXT_OUTPUT_CHARS = 12000, DEFAULT_TOOL_TEXT_OUTPUT_CHARS = 6000, MAX_TOOL_LIST_ENTRIES = 200, MAX_TOOL_SEARCH_RESULTS = 200, ANTHROPIC_TOOL_OPTIONAL_PARAMETER_BUDGET = 24, TODO_CONTENT_MAX_CHARS = 200, TODO_ACTIVE_FORM_MAX_CHARS = 80, PROGRESS_NOTE_PRUNE_MIN_CHARS = 200;
151
151
 
152
152
  // packages/ai/src/coding-agent-modes.ts
153
153
  import { z } from "zod";
@@ -747,9 +747,10 @@ var init_schema = __esm(() => {
747
747
 
748
748
  // packages/ai/src/edit-file/schema.ts
749
749
  import { z as z6 } from "zod";
750
- var editFileDescription = "Edit a file via literal search and replace. Use this for focused, small edits.", editFileInputSchema, editFileProviderInputSchema, editFileOutputSchema;
750
+ var editFileDescription, editFileInputSchema, editFileProviderInputSchema, editFileOutputSchema;
751
751
  var init_schema2 = __esm(() => {
752
752
  init_base_schemas();
753
+ editFileDescription = "Edit a file via search and replace. Use this for focused, small edits. " + "Line endings (CRLF/LF) and surrounding whitespace are matched tolerantly, so " + "copy the lines as shown by read_file. The search text must match exactly one " + "place \u2014 include enough surrounding context to make it unique, or set " + "replaceAll: true to change every occurrence.";
753
754
  editFileInputSchema = z6.object({
754
755
  path: boundedPathSchema,
755
756
  search: z6.string().min(1),
@@ -1044,20 +1045,22 @@ var init_schema10 = __esm(() => {
1044
1045
  todoPrioritySchema = z14.enum(["low", "medium", "high"]);
1045
1046
  todoItemSchema = z14.object({
1046
1047
  id: z14.string().min(1).max(120).optional(),
1047
- content: z14.string().min(1).max(500),
1048
+ content: z14.string().min(1).max(TODO_CONTENT_MAX_CHARS),
1048
1049
  status: todoStatusSchema,
1049
1050
  priority: todoPrioritySchema.optional().default("medium"),
1050
- notes: z14.string().max(1000).optional()
1051
+ notes: z14.string().max(1000).optional(),
1052
+ activeForm: z14.string().min(1).max(TODO_ACTIVE_FORM_MAX_CHARS).optional()
1051
1053
  });
1052
1054
  todoItemProviderSchema = z14.object({
1053
- content: z14.string().min(1).max(500),
1054
- status: todoStatusSchema
1055
+ content: z14.string().min(1).max(TODO_CONTENT_MAX_CHARS),
1056
+ status: todoStatusSchema,
1057
+ activeForm: z14.string().min(1).max(TODO_ACTIVE_FORM_MAX_CHARS).optional().describe(`Short present-tense active label for this task, e.g. "Adding tests" or "Editing schema". Set it primarily for the in_progress item so the UI can show what's actively happening; the UI falls back to content when omitted.`)
1055
1058
  });
1056
1059
  todoWriteInputSchema = z14.object({
1057
1060
  todos: z14.array(todoItemSchema).max(50)
1058
1061
  });
1059
1062
  todoWriteProviderInputSchema = z14.object({
1060
- todos: z14.array(todoItemProviderSchema)
1063
+ todos: z14.array(todoItemProviderSchema).max(20)
1061
1064
  });
1062
1065
  todoWriteOutputSchema = z14.object({
1063
1066
  sessionId: z14.string(),
@@ -1069,7 +1072,8 @@ var init_schema10 = __esm(() => {
1069
1072
  inProgress: z14.number().int().nonnegative(),
1070
1073
  completed: z14.number().int().nonnegative(),
1071
1074
  canceled: z14.number().int().nonnegative()
1072
- })
1075
+ }),
1076
+ warnings: z14.array(z14.string()).optional()
1073
1077
  });
1074
1078
  });
1075
1079
 
@@ -1903,14 +1907,139 @@ ${environmentContext.trim()}` : "");
1903
1907
  var cwdPromptPlaceholder = "{cwd}", defaultCodingAgentSystemPrompt, toolCallingDisciplineAddendum;
1904
1908
  var init_coding_agent_prompt = __esm(() => {
1905
1909
  init_coding_agent_modes();
1906
- defaultCodingAgentSystemPrompt = "You are a coding agent working inside the user's project at this working directory: {cwd}. " + 'You can already see the project \u2014 proactively read it. When the user asks you to review, explain, or work on "my code"/"this", inspect the working directory yourself with list_files, glob_search, grep, and read_file; NEVER ask the user to paste or share code that lives in this directory \u2014 open it. ' + "Use tools for filesystem and codebase tasks instead of guessing, and only interact with files under the working directory. " + "Prefer dedicated tools \u2014 glob_search for paths, grep for text search, structured git tools for repository inspection \u2014 and bash only when those are not enough. " + `Use todo_write to keep multi-step work visible and current, inspect context before risky operations, and emit a brief, natural progress note before major tool actions and after important findings.
1910
+ defaultCodingAgentSystemPrompt = "You are a coding agent working inside the user's project at this working directory: {cwd}. " + 'You can already see the project \u2014 proactively read it. When the user asks you to review, explain, or work on "my code"/"this", inspect the working directory yourself with list_files, glob_search, grep, and read_file; NEVER ask the user to paste or share code that lives in this directory \u2014 open it. ' + "Use tools for filesystem and codebase tasks instead of guessing, and only interact with files under the working directory. " + "Prefer dedicated tools \u2014 glob_search for paths, grep for text search, structured git tools for repository inspection \u2014 and bash only when those are not enough. " + `Inspect context before risky operations. Progress notes: before a major tool action you may emit ONE short note (one sentence, ~15 words) saying what you are about to do. After a tool returns, mention a finding only if it changes your plan, and state it as fact grounded in what the tool actually returned \u2014 cite the file/line/symbol. NEVER claim you found, changed, or fixed something before a tool result confirms it. Skip notes for routine reads, and do not restate the todo list in prose.
1911
+
1912
+ ` + `Task list discipline \u2014 use the todo_write tool to keep a visible, evolving checklist:
1913
+ ` + `- Create the list FIRST. For any task with roughly three or more distinct steps, or any multi-file or multi-stage work, call todo_write as your very first action \u2014 before reading or editing \u2014 with every step listed and all of them set to pending.
1914
+ ` + `- Keep exactly ONE item in_progress at a time. Mark an item in_progress immediately before you start it; never have two items in_progress at once.
1915
+ ` + `- Mark an item completed only after a tool result confirms it is actually done (test passed, file written, build clean) \u2014 not on intent. Do this the MOMENT it is confirmed; do not batch completions or wait until the end of the turn.
1916
+ ` + `- Completions are monotonic: once a task is completed, do not silently move it back to pending or in_progress. Reopen it only if it genuinely regressed.
1917
+ ` + `- Every todo_write call is a FULL REWRITE of the list: always send every task with its current status (re-state the completed and pending ones too \u2014 never send only the changed item). Keep each item terse (a short label, not a paragraph).
1918
+ ` + `- Skip the list for trivial, single-step, or purely conversational requests \u2014 a checklist there is just noise.
1907
1919
 
1908
1920
  ` + `You are an agent \u2014 keep working until the request is fully resolved. Never stop with a partial answer; if you say you will do something, do it in the same turn by calling a tool. Do not finish while any todo is pending or in_progress \u2014 end your turn only when the task is complete or you are blocked on input that only the user can provide.
1909
1921
 
1922
+ ` + `When the task is complete, end with a short summary (a few lines) grounded in the todos you actually completed and the real file diffs \u2014 name the files you edited, do not list steps you did not verify, and do not repeat the todo card verbatim. If anything is incomplete or you are blocked, say so plainly.
1923
+
1910
1924
  ` + "Long-running processes (dev servers, watchers): never run them as plain foreground bash \u2014 the bash tool enforces a timeout and will kill them. Start them detached with output redirected to a log " + '(Windows: start /B cmd /c "npm run dev > dev.log 2>&1"; Unix: nohup npm run dev > dev.log 2>&1 &), then read the log to confirm startup and the actual port (do not assume the default). ' + "Never kill processes or free ports you did not open yourself.";
1911
1925
  toolCallingDisciplineAddendum = "Invoke tools only through the function-calling interface. " + "Never write tool calls as XML or plain text such as <tool_call>, <function_call>, <invoke>, or bare JSON in your message. " + "Tool arguments must be valid JSON matching the tool's schema. " + "Call one tool at a time and wait for its result before deciding the next step.";
1912
1926
  });
1913
1927
 
1928
+ // packages/ai/src/common/coerce-tool-input.ts
1929
+ function coercePrimitive(current, expected) {
1930
+ if (current === undefined || current === null) {
1931
+ return NO_CHANGE;
1932
+ }
1933
+ switch (expected) {
1934
+ case "number": {
1935
+ if (typeof current === "string") {
1936
+ const trimmed = current.trim();
1937
+ if (trimmed !== "" && Number.isFinite(Number(trimmed))) {
1938
+ return Number(trimmed);
1939
+ }
1940
+ }
1941
+ return NO_CHANGE;
1942
+ }
1943
+ case "boolean": {
1944
+ if (typeof current === "string") {
1945
+ const lowered = current.trim().toLowerCase();
1946
+ if (lowered === "true" || lowered === "1" || lowered === "yes") {
1947
+ return true;
1948
+ }
1949
+ if (lowered === "false" || lowered === "0" || lowered === "no") {
1950
+ return false;
1951
+ }
1952
+ }
1953
+ if (current === 1)
1954
+ return true;
1955
+ if (current === 0)
1956
+ return false;
1957
+ return NO_CHANGE;
1958
+ }
1959
+ case "string": {
1960
+ if (typeof current === "number" || typeof current === "boolean") {
1961
+ return String(current);
1962
+ }
1963
+ return NO_CHANGE;
1964
+ }
1965
+ case "object":
1966
+ case "array": {
1967
+ if (typeof current === "string") {
1968
+ try {
1969
+ const parsed = JSON.parse(current);
1970
+ const isArray = Array.isArray(parsed);
1971
+ const matches = expected === "array" ? isArray : parsed !== null && typeof parsed === "object" && !isArray;
1972
+ if (matches) {
1973
+ return parsed;
1974
+ }
1975
+ } catch {}
1976
+ }
1977
+ return NO_CHANGE;
1978
+ }
1979
+ default:
1980
+ return NO_CHANGE;
1981
+ }
1982
+ }
1983
+ function coerceAtPath(root, path3, expected) {
1984
+ if (path3.length === 0 || root === null || typeof root !== "object") {
1985
+ return false;
1986
+ }
1987
+ let parent = root;
1988
+ for (let index = 0;index < path3.length - 1; index += 1) {
1989
+ const next = parent[path3[index]];
1990
+ if (next === null || typeof next !== "object") {
1991
+ return false;
1992
+ }
1993
+ parent = next;
1994
+ }
1995
+ const key = path3[path3.length - 1];
1996
+ const coerced = coercePrimitive(parent[key], expected);
1997
+ if (coerced === NO_CHANGE) {
1998
+ return false;
1999
+ }
2000
+ parent[key] = coerced;
2001
+ return true;
2002
+ }
2003
+ function coerceToolInputToSchema(value, schema) {
2004
+ if (value === null || typeof value !== "object") {
2005
+ return null;
2006
+ }
2007
+ let current;
2008
+ try {
2009
+ current = structuredClone(value);
2010
+ } catch {
2011
+ current = value;
2012
+ }
2013
+ for (let pass = 0;pass < MAX_PASSES; pass += 1) {
2014
+ const result = schema.safeParse(current);
2015
+ if (result.success) {
2016
+ return result.data;
2017
+ }
2018
+ let changed = false;
2019
+ for (const issue of result.error.issues) {
2020
+ if (issue.code !== "invalid_type") {
2021
+ continue;
2022
+ }
2023
+ const expected = issue.expected;
2024
+ if (typeof expected !== "string") {
2025
+ continue;
2026
+ }
2027
+ if (coerceAtPath(current, issue.path, expected)) {
2028
+ changed = true;
2029
+ }
2030
+ }
2031
+ if (!changed) {
2032
+ break;
2033
+ }
2034
+ }
2035
+ const final = schema.safeParse(current);
2036
+ return final.success ? final.data : null;
2037
+ }
2038
+ var NO_CHANGE, MAX_PASSES = 4;
2039
+ var init_coerce_tool_input = __esm(() => {
2040
+ NO_CHANGE = Symbol("no-change");
2041
+ });
2042
+
1914
2043
  // packages/ai/src/providers/tool-call-json-repair.ts
1915
2044
  import { parsePartialJson } from "ai";
1916
2045
  async function repairToolJson(raw) {
@@ -2105,15 +2234,19 @@ async function repairCodingAgentToolCall({
2105
2234
  if (!repairedInput || typeof repairedInput !== "object") {
2106
2235
  return null;
2107
2236
  }
2237
+ const schema = codingToolProviderInputSchemas[toolCall.toolName];
2238
+ const coerced = schema ? coerceToolInputToSchema(repairedInput, schema) : null;
2108
2239
  return {
2109
2240
  ...toolCall,
2110
- input: JSON.stringify(repairedInput)
2241
+ input: JSON.stringify(coerced ?? repairedInput)
2111
2242
  };
2112
2243
  }
2113
2244
  return null;
2114
2245
  }
2115
2246
  var toolNameAliases;
2116
2247
  var init_coding_agent_tool_repair = __esm(() => {
2248
+ init_agent_tools();
2249
+ init_coerce_tool_input();
2117
2250
  init_tool_call_json_repair();
2118
2251
  toolNameAliases = {
2119
2252
  read: "read_file",
@@ -2202,19 +2335,6 @@ function getLastUserText(messages, prompt) {
2202
2335
  }
2203
2336
  return collectTextFromUnknown(prompt).trim();
2204
2337
  }
2205
- function pushUniqueTool(tools, toolName) {
2206
- if (!tools.includes(toolName)) {
2207
- tools.push(toolName);
2208
- }
2209
- }
2210
- function includesAny(text, tokens) {
2211
- return tokens.some((token) => {
2212
- if (token.includes("://") || token.includes(" ")) {
2213
- return text.includes(token);
2214
- }
2215
- return new RegExp(`(^|[^a-z0-9])${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^a-z0-9]|$)`).test(text);
2216
- });
2217
- }
2218
2338
  function selectCodingAgentIntentTools({
2219
2339
  mode,
2220
2340
  prompt,
@@ -2225,225 +2345,7 @@ function selectCodingAgentIntentTools({
2225
2345
  if (!normalizedText || casualPromptPattern.test(normalizedText)) {
2226
2346
  return [];
2227
2347
  }
2228
- const selectedTools = [];
2229
- const hasContinuationIntent = includesAny(normalizedText, [
2230
- "continue",
2231
- "proceed",
2232
- "go on",
2233
- "go ahead",
2234
- "keep going",
2235
- "carry on",
2236
- "resume",
2237
- "finish",
2238
- "next step"
2239
- ]);
2240
- const hasCodeIntent = includesAny(normalizedText, [
2241
- "file",
2242
- "folder",
2243
- "code",
2244
- "repo",
2245
- "project",
2246
- "implement",
2247
- "fix",
2248
- "add",
2249
- "build",
2250
- "refactor",
2251
- "change",
2252
- "create",
2253
- "update",
2254
- "edit",
2255
- "delete",
2256
- "run",
2257
- "test",
2258
- "typecheck",
2259
- "debug",
2260
- "error",
2261
- "issue",
2262
- "problem",
2263
- "fails",
2264
- "failed",
2265
- "failing",
2266
- "bug",
2267
- "crash",
2268
- "slow",
2269
- "slowness",
2270
- "not working",
2271
- "epic",
2272
- "ticket"
2273
- ]);
2274
- const hasReadIntent = includesAny(normalizedText, [
2275
- "read",
2276
- "list",
2277
- "find",
2278
- "search",
2279
- "grep",
2280
- "glob",
2281
- "inspect",
2282
- "analyse",
2283
- "analyze",
2284
- "check"
2285
- ]);
2286
- const workspaceReferents = [
2287
- "this",
2288
- "these",
2289
- "that",
2290
- "here",
2291
- "code",
2292
- "codebase",
2293
- "repo",
2294
- "repository",
2295
- "project",
2296
- "file",
2297
- "files",
2298
- "directory",
2299
- "folder",
2300
- "app",
2301
- "application",
2302
- "module",
2303
- "my"
2304
- ];
2305
- const strongReviewIntent = includesAny(normalizedText, [
2306
- "review",
2307
- "audit",
2308
- "walk through",
2309
- "walkthrough",
2310
- "code review"
2311
- ]);
2312
- const softReviewIntent = includesAny(normalizedText, [
2313
- "explain",
2314
- "understand",
2315
- "summarize",
2316
- "summarise",
2317
- "overview",
2318
- "describe",
2319
- "explore",
2320
- "look at"
2321
- ]) && includesAny(normalizedText, workspaceReferents);
2322
- const hasReviewIntent = strongReviewIntent || softReviewIntent;
2323
- const hasWriteIntent = mode === "build" && (hasContinuationIntent || includesAny(normalizedText, [
2324
- "write",
2325
- "edit",
2326
- "change",
2327
- "create",
2328
- "implement",
2329
- "fix",
2330
- "update",
2331
- "delete",
2332
- "remove",
2333
- "rename"
2334
- ]));
2335
- const hasShellIntent = mode === "build" && (hasContinuationIntent || includesAny(normalizedText, [
2336
- "run",
2337
- "command",
2338
- "terminal",
2339
- "shell",
2340
- "bash",
2341
- "powershell",
2342
- "bun",
2343
- "tests",
2344
- "run test",
2345
- "run tests",
2346
- "typecheck"
2347
- ]));
2348
- const hasGitIntent = includesAny(normalizedText, [
2349
- "git",
2350
- "diff",
2351
- "status",
2352
- "commit",
2353
- "log",
2354
- "show",
2355
- "revision",
2356
- "changes"
2357
- ]);
2358
- const hasTodoIntent = includesAny(normalizedText, [
2359
- "todo",
2360
- "tasks",
2361
- "checklist",
2362
- "steps",
2363
- "epic",
2364
- "ticket"
2365
- ]);
2366
- const hasWebIntent = includesAny(normalizedText, [
2367
- "http://",
2368
- "https://",
2369
- "url",
2370
- "fetch",
2371
- "website",
2372
- "web",
2373
- "online",
2374
- "search internet",
2375
- "latest"
2376
- ]);
2377
- const explicitToolSearch = includesAny(normalizedText, [
2378
- "tool_search",
2379
- "tool search",
2380
- "find tool",
2381
- "available tool"
2382
- ]);
2383
- const hasSkillIntent = includesAny(normalizedText, [
2384
- "skill",
2385
- "skills",
2386
- "load skill"
2387
- ]);
2388
- const hasMcpIntent = includesAny(normalizedText, [
2389
- "mcp",
2390
- "resource",
2391
- "resources",
2392
- "mcp tool",
2393
- "mcp server"
2394
- ]);
2395
- if (hasCodeIntent || hasContinuationIntent || hasReviewIntent || hasReadIntent && !hasGitIntent && !hasWebIntent && !explicitToolSearch) {
2396
- for (const toolName of baseReadTools) {
2397
- pushUniqueTool(selectedTools, toolName);
2398
- }
2399
- }
2400
- if (hasGitIntent) {
2401
- if (includesAny(normalizedText, ["status", "state", "dirty", "working tree"]) || !includesAny(normalizedText, ["diff", "log", "show", "history", "commit"])) {
2402
- pushUniqueTool(selectedTools, "git_status");
2403
- }
2404
- if (includesAny(normalizedText, ["diff", "changes", "patch"])) {
2405
- pushUniqueTool(selectedTools, "git_diff");
2406
- }
2407
- if (includesAny(normalizedText, ["log", "history", "commits"])) {
2408
- pushUniqueTool(selectedTools, "git_log");
2409
- }
2410
- if (includesAny(normalizedText, ["show", "revision", "commit"])) {
2411
- pushUniqueTool(selectedTools, "git_show");
2412
- }
2413
- }
2414
- if (hasTodoIntent && mode === "build") {
2415
- pushUniqueTool(selectedTools, "todo_write");
2416
- }
2417
- if (explicitToolSearch) {
2418
- pushUniqueTool(selectedTools, "tool_search");
2419
- }
2420
- if (hasSkillIntent) {
2421
- pushUniqueTool(selectedTools, "skill");
2422
- }
2423
- if (hasMcpIntent) {
2424
- pushUniqueTool(selectedTools, "list_mcp_resources");
2425
- if (includesAny(normalizedText, ["read", "resource", "resources"])) {
2426
- pushUniqueTool(selectedTools, "read_mcp_resource");
2427
- }
2428
- if (mode === "build" && includesAny(normalizedText, ["call", "execute", "run"])) {
2429
- pushUniqueTool(selectedTools, "call_mcp_tool");
2430
- }
2431
- }
2432
- if (hasWriteIntent) {
2433
- pushUniqueTool(selectedTools, "write_file");
2434
- pushUniqueTool(selectedTools, "edit_file");
2435
- }
2436
- if (hasShellIntent) {
2437
- pushUniqueTool(selectedTools, "bash");
2438
- }
2439
- if (hasWebIntent) {
2440
- pushUniqueTool(selectedTools, "web_fetch");
2441
- pushUniqueTool(selectedTools, "web_search");
2442
- }
2443
- if (mode === "plan" && includesAny(normalizedText, ["question", "ask", "clarify"])) {
2444
- pushUniqueTool(selectedTools, "request_user_input");
2445
- }
2446
- return selectedTools;
2348
+ return [...getCodingAgentModeDefinition(mode).activeTools];
2447
2349
  }
2448
2350
  function limitProviderActiveTools(tools, maxTools = maxProviderActiveTools) {
2449
2351
  if (tools.length <= maxTools) {
@@ -2452,15 +2354,10 @@ function limitProviderActiveTools(tools, maxTools = maxProviderActiveTools) {
2452
2354
  const toolSet = new Set(tools);
2453
2355
  return providerToolPriority.filter((toolName) => toolSet.has(toolName)).slice(0, maxTools);
2454
2356
  }
2455
- var casualPromptPattern, baseReadTools, maxProviderActiveTools = 7, providerToolPriority;
2357
+ var casualPromptPattern, maxProviderActiveTools = 24, providerToolPriority;
2456
2358
  var init_intent_tool_selection = __esm(() => {
2359
+ init_coding_agent_modes();
2457
2360
  casualPromptPattern = /^(hi|hello|hey|how are you|how r you|what'?s up|whats up|sup|thanks|thank you|ok|okay|yes|no|nice|cool|great)[\s?!.,]*$/i;
2458
- baseReadTools = [
2459
- "list_files",
2460
- "glob_search",
2461
- "read_file",
2462
- "grep"
2463
- ];
2464
2361
  providerToolPriority = [
2465
2362
  "list_files",
2466
2363
  "glob_search",
@@ -2714,17 +2611,33 @@ function normalizeContextOptimizerConfig(config) {
2714
2611
  }
2715
2612
  function resolveContextWindowTokens({
2716
2613
  config,
2717
- modelContextWindow
2614
+ modelContextWindow,
2615
+ endpointLimitTokens
2616
+ }) {
2617
+ const resolved = config.contextWindowOverride ?? modelContextWindow ?? DEFAULT_CONTEXT_WINDOW_TOKENS;
2618
+ if (typeof endpointLimitTokens === "number" && Number.isFinite(endpointLimitTokens) && endpointLimitTokens > 0) {
2619
+ return Math.min(resolved, endpointLimitTokens);
2620
+ }
2621
+ return resolved;
2622
+ }
2623
+ function resolveInputBudgetTokens({
2624
+ contextWindow,
2625
+ reservedOutputTokens,
2626
+ safetyFraction = DEFAULT_INPUT_BUDGET_SAFETY_FRACTION
2718
2627
  }) {
2719
- return config.contextWindowOverride ?? modelContextWindow ?? DEFAULT_CONTEXT_WINDOW_TOKENS;
2628
+ const reserved = typeof reservedOutputTokens === "number" && Number.isFinite(reservedOutputTokens) && reservedOutputTokens > 0 ? reservedOutputTokens : 0;
2629
+ const clampedFraction = Math.min(1, Math.max(0.1, safetyFraction));
2630
+ const usable = Math.max(0, contextWindow - reserved);
2631
+ return Math.max(0, Math.floor(usable * clampedFraction));
2720
2632
  }
2721
- var DEFAULT_CONTEXT_WINDOW_TOKENS = 128000, contextOptimizerConfigSchema, resolvedContextOptimizerConfigSchema, defaultContextOptimizerConfig;
2633
+ var DEFAULT_CONTEXT_WINDOW_TOKENS = 128000, contextOptimizerConfigSchema, resolvedContextOptimizerConfigSchema, defaultContextOptimizerConfig, DEFAULT_INPUT_BUDGET_SAFETY_FRACTION = 0.92;
2722
2634
  var init_config3 = __esm(() => {
2723
2635
  contextOptimizerConfigSchema = z26.object({
2724
2636
  autoCompact: z26.boolean().optional(),
2725
2637
  compactAtFraction: z26.number().min(0.3).max(0.95).optional(),
2726
2638
  pruneAtFraction: z26.number().min(0.2).max(0.9).optional(),
2727
2639
  preserveRecentMessages: z26.number().int().min(2).max(100).optional(),
2640
+ maxCoverageTokensPerCompaction: z26.number().int().min(2000).max(200000).optional(),
2728
2641
  contextWindowOverride: z26.number().int().min(8000).max(1e7).optional(),
2729
2642
  summaryMaxChars: z26.number().int().min(200).max(50000).optional(),
2730
2643
  aggressivePruneWhenUncached: z26.boolean().optional(),
@@ -2738,6 +2651,7 @@ var init_config3 = __esm(() => {
2738
2651
  compactAtFraction: z26.number().min(0.3).max(0.95),
2739
2652
  pruneAtFraction: z26.number().min(0.2).max(0.9),
2740
2653
  preserveRecentMessages: z26.number().int().min(2).max(100),
2654
+ maxCoverageTokensPerCompaction: z26.number().int().min(2000).max(200000),
2741
2655
  contextWindowOverride: z26.number().int().min(8000).max(1e7).nullable(),
2742
2656
  summaryMaxChars: z26.number().int().min(200).max(50000),
2743
2657
  aggressivePruneWhenUncached: z26.boolean(),
@@ -2750,6 +2664,7 @@ var init_config3 = __esm(() => {
2750
2664
  compactAtFraction: 0.7,
2751
2665
  pruneAtFraction: 0.6,
2752
2666
  preserveRecentMessages: 6,
2667
+ maxCoverageTokensPerCompaction: 12000,
2753
2668
  contextWindowOverride: null,
2754
2669
  summaryMaxChars: 4000,
2755
2670
  aggressivePruneWhenUncached: true,
@@ -2829,10 +2744,44 @@ var init_estimate = __esm(() => {
2829
2744
  totalTokens: z27.number().nonnegative().optional()
2830
2745
  }),
2831
2746
  modelId: z27.string().optional(),
2832
- finishReason: languageModelFinishReasonSchema.optional()
2747
+ finishReason: languageModelFinishReasonSchema.optional(),
2748
+ context: z27.object({
2749
+ inputTokens: z27.number().nonnegative(),
2750
+ contextWindow: z27.number().positive(),
2751
+ compactedMessages: z27.number().nonnegative().optional()
2752
+ }).optional()
2833
2753
  }).loose();
2834
2754
  });
2835
2755
 
2756
+ // packages/ai/src/common/output-utils.ts
2757
+ function omissionMarker(omitted) {
2758
+ return `
2759
+
2760
+ ... [${omitted} characters omitted; re-read a line range with read_file or raise maxChars] ...
2761
+
2762
+ `;
2763
+ }
2764
+ function truncateText(text, maxChars = DEFAULT_TOOL_TEXT_OUTPUT_CHARS) {
2765
+ if (text.length <= maxChars) {
2766
+ return {
2767
+ text,
2768
+ truncated: false
2769
+ };
2770
+ }
2771
+ const markerReserve = omissionMarker(text.length).length;
2772
+ const usable = Math.max(0, maxChars - markerReserve);
2773
+ const headChars = Math.floor(usable * 0.7);
2774
+ const tailChars = usable - headChars;
2775
+ const head = text.slice(0, headChars);
2776
+ const tail = tailChars > 0 ? text.slice(text.length - tailChars) : "";
2777
+ const omitted = text.length - head.length - tail.length;
2778
+ return {
2779
+ text: `${head}${omissionMarker(omitted)}${tail}`,
2780
+ truncated: true
2781
+ };
2782
+ }
2783
+ var init_output_utils = () => {};
2784
+
2836
2785
  // packages/ai/src/context/heuristic-summary.ts
2837
2786
  function truncateInline(text, maxChars) {
2838
2787
  const normalized = text.replace(/\s+/g, " ").trim();
@@ -3081,6 +3030,16 @@ function getPartState(part) {
3081
3030
  const state = isRecord3(part) ? Reflect.get(part, "state") : undefined;
3082
3031
  return typeof state === "string" ? state : null;
3083
3032
  }
3033
+ function getPlainTextPartContent(part) {
3034
+ if (!isRecord3(part) || Reflect.get(part, "type") !== "text") {
3035
+ return null;
3036
+ }
3037
+ const text = Reflect.get(part, "text");
3038
+ return typeof text === "string" ? text : null;
3039
+ }
3040
+ function messageHasToolPart(message) {
3041
+ return message.parts.some((part) => getToolNameFromPart(part) !== null);
3042
+ }
3084
3043
  function computePruneCutoffIndex(messages, preserveRecentMessages, quantizeUserTurns = DEFAULT_PRUNE_QUANTIZE_USER_TURNS) {
3085
3044
  const candidate = messages.length - preserveRecentMessages;
3086
3045
  if (candidate <= 0) {
@@ -3130,15 +3089,29 @@ function pruneToolOutputs(messages, options) {
3130
3089
  });
3131
3090
  let elidedToolOutputs = 0;
3132
3091
  let dedupedFileReads = 0;
3092
+ let elidedProgressNotes = 0;
3133
3093
  let savedChars = 0;
3134
3094
  const prunedMessages = messages.map((message, messageIndex) => {
3135
3095
  if (messageIndex >= cutoff) {
3136
3096
  return message;
3137
3097
  }
3098
+ const noteCandidate = message.role === "assistant" && messageHasToolPart(message);
3138
3099
  let changed = false;
3139
3100
  const parts = message.parts.map((part, partIndex) => {
3140
3101
  const toolName = getToolNameFromPart(part);
3141
3102
  if (!toolName || getPartState(part) !== "output-available") {
3103
+ if (noteCandidate && toolName === null) {
3104
+ const text = getPlainTextPartContent(part);
3105
+ if (text !== null && text !== PROGRESS_NOTE_ELISION_STUB && text.length > PROGRESS_NOTE_PRUNE_MIN_CHARS) {
3106
+ elidedProgressNotes += 1;
3107
+ savedChars += Math.max(0, text.length - PROGRESS_NOTE_ELISION_STUB.length);
3108
+ changed = true;
3109
+ return {
3110
+ ...part,
3111
+ text: PROGRESS_NOTE_ELISION_STUB
3112
+ };
3113
+ }
3114
+ }
3142
3115
  return part;
3143
3116
  }
3144
3117
  const output = isRecord3(part) ? Reflect.get(part, "output") : undefined;
@@ -3155,7 +3128,7 @@ function pruneToolOutputs(messages, options) {
3155
3128
  }
3156
3129
  const stub = {
3157
3130
  lightcodeElided: true,
3158
- note: isStaleRead ? "Earlier read of this file elided; a later read in this conversation has the current content. Re-run read_file if needed." : "Large tool output elided to preserve context budget. Re-run the tool if this output is needed again.",
3131
+ note: isStaleRead ? "Earlier read of this file elided; its current content is already shown later in this conversation \u2014 do not read it again." : "Large tool output elided to preserve context budget. Re-run the tool only if this output is needed again.",
3159
3132
  originalChars: serializedOutput.length
3160
3133
  };
3161
3134
  if (isStaleRead) {
@@ -3173,15 +3146,137 @@ function pruneToolOutputs(messages, options) {
3173
3146
  messages: prunedMessages,
3174
3147
  elidedToolOutputs,
3175
3148
  dedupedFileReads,
3149
+ elidedProgressNotes,
3176
3150
  savedChars
3177
3151
  };
3178
3152
  }
3179
- var DEFAULT_PRUNE_MIN_OUTPUT_CHARS = 2000, DEFAULT_PRUNE_QUANTIZE_USER_TURNS = 4;
3153
+ var DEFAULT_PRUNE_MIN_OUTPUT_CHARS = 2000, PROGRESS_NOTE_ELISION_STUB = "[progress note elided]", DEFAULT_PRUNE_QUANTIZE_USER_TURNS = 4;
3180
3154
  var init_tier1_prune = __esm(() => {
3181
3155
  init_estimate();
3182
3156
  init_message_parts();
3183
3157
  });
3184
3158
 
3159
+ // packages/ai/src/context/fit-to-budget.ts
3160
+ function estimateStructuralTokens(messages) {
3161
+ return messages.reduce((total, message) => total + estimateMessageTokens(message), 0);
3162
+ }
3163
+ function getPartState2(part) {
3164
+ const state = isRecord3(part) ? Reflect.get(part, "state") : undefined;
3165
+ return typeof state === "string" ? state : null;
3166
+ }
3167
+ function elideOutput(part, originalChars) {
3168
+ const stub = {
3169
+ lightcodeElided: true,
3170
+ note: "Tool output elided to fit the model's context budget. Re-run the tool if this output is needed again.",
3171
+ originalChars
3172
+ };
3173
+ return { ...part, output: stub };
3174
+ }
3175
+ function elideToolOutputsInMessage(message) {
3176
+ let elided = 0;
3177
+ const parts = message.parts.map((part) => {
3178
+ if (!getToolNameFromPart(part) || getPartState2(part) !== "output-available") {
3179
+ return part;
3180
+ }
3181
+ const output = isRecord3(part) ? Reflect.get(part, "output") : undefined;
3182
+ if (output === undefined || isElidedToolOutput(output)) {
3183
+ return part;
3184
+ }
3185
+ elided += 1;
3186
+ return elideOutput(part, safeStringify(output).length);
3187
+ });
3188
+ return elided > 0 ? { message: { ...message, parts }, elided } : { message, elided: 0 };
3189
+ }
3190
+ function truncateTextPartsInMessage(message, capChars) {
3191
+ let truncated = 0;
3192
+ const parts = message.parts.map((part) => {
3193
+ if (!isRecord3(part)) {
3194
+ return part;
3195
+ }
3196
+ const text = Reflect.get(part, "text");
3197
+ if (typeof text !== "string" || text.length <= capChars) {
3198
+ return part;
3199
+ }
3200
+ const result = truncateText(text, capChars);
3201
+ if (!result.truncated) {
3202
+ return part;
3203
+ }
3204
+ truncated += 1;
3205
+ return { ...part, text: result.text };
3206
+ });
3207
+ return truncated > 0 ? { message: { ...message, parts }, truncated } : { message, truncated: 0 };
3208
+ }
3209
+ function fitMessagesToBudget(messages, options) {
3210
+ const budget = Math.max(0, Math.floor(options.inputBudgetTokens));
3211
+ const preserveRecentMessages = Math.max(0, options.preserveRecentMessages);
3212
+ let working = [...messages];
3213
+ let elidedToolOutputs = 0;
3214
+ let droppedMessages = 0;
3215
+ let truncatedTextParts = 0;
3216
+ const fits = () => estimateStructuralTokens(working) <= budget;
3217
+ if (fits()) {
3218
+ return {
3219
+ messages: working,
3220
+ elidedToolOutputs: 0,
3221
+ droppedMessages: 0,
3222
+ truncatedTextParts: 0,
3223
+ estimatedTokens: estimateStructuralTokens(working),
3224
+ fitted: false,
3225
+ withinBudget: true
3226
+ };
3227
+ }
3228
+ const preserveFrom = Math.max(0, working.length - preserveRecentMessages);
3229
+ for (let index = 0;index < preserveFrom && !fits(); index += 1) {
3230
+ const { message, elided } = elideToolOutputsInMessage(working[index]);
3231
+ if (elided > 0) {
3232
+ working[index] = message;
3233
+ elidedToolOutputs += elided;
3234
+ }
3235
+ }
3236
+ const firstIsSummary = isSummaryMessage(working[0]);
3237
+ let dropFrom = firstIsSummary ? 1 : 0;
3238
+ while (!fits() && working.length - dropFrom > preserveRecentMessages && dropFrom < working.length) {
3239
+ working.splice(dropFrom, 1);
3240
+ droppedMessages += 1;
3241
+ }
3242
+ if (!fits()) {
3243
+ working = working.map((message) => {
3244
+ const { message: elidedMessage, elided } = elideToolOutputsInMessage(message);
3245
+ elidedToolOutputs += elided;
3246
+ return elidedMessage;
3247
+ });
3248
+ let cap = MAX_TOOL_TEXT_OUTPUT_CHARS;
3249
+ while (!fits() && cap >= MIN_TRUNCATION_CAP_CHARS) {
3250
+ working = working.map((message) => {
3251
+ const { message: truncatedMessage, truncated } = truncateTextPartsInMessage(message, cap);
3252
+ truncatedTextParts += truncated;
3253
+ return truncatedMessage;
3254
+ });
3255
+ cap = Math.floor(cap / 2);
3256
+ }
3257
+ }
3258
+ return {
3259
+ messages: working,
3260
+ elidedToolOutputs,
3261
+ droppedMessages,
3262
+ truncatedTextParts,
3263
+ estimatedTokens: estimateStructuralTokens(working),
3264
+ fitted: elidedToolOutputs > 0 || droppedMessages > 0 || truncatedTextParts > 0,
3265
+ withinBudget: fits()
3266
+ };
3267
+ }
3268
+ function isSummaryMessage(message) {
3269
+ return message?.id === contextSummaryMessageId;
3270
+ }
3271
+ var MIN_TRUNCATION_CAP_CHARS = 256;
3272
+ var init_fit_to_budget = __esm(() => {
3273
+ init_output_utils();
3274
+ init_estimate();
3275
+ init_heuristic_summary();
3276
+ init_message_parts();
3277
+ init_tier1_prune();
3278
+ });
3279
+
3185
3280
  // packages/ai/src/context/build-provider-view.ts
3186
3281
  function computeCompactionCutoffIndex(messages, preserveRecentMessages, afterAnchorIndex) {
3187
3282
  const candidate = messages.length - preserveRecentMessages;
@@ -3195,11 +3290,32 @@ function computeCompactionCutoffIndex(messages, preserveRecentMessages, afterAnc
3195
3290
  }
3196
3291
  return afterAnchorIndex;
3197
3292
  }
3293
+ function capCoverageToTokenBudget(coveredMessages, maxTokens) {
3294
+ if (coveredMessages.length <= 1) {
3295
+ return [...coveredMessages];
3296
+ }
3297
+ let total = 0;
3298
+ const capped = [];
3299
+ for (const message of coveredMessages) {
3300
+ const messageTokens = estimateStructuralTokens([message]);
3301
+ if (capped.length > 0 && total + messageTokens > maxTokens) {
3302
+ break;
3303
+ }
3304
+ capped.push(message);
3305
+ total += messageTokens;
3306
+ }
3307
+ return capped;
3308
+ }
3198
3309
  function buildProviderView(options) {
3199
3310
  const { messages, contextState, config } = options;
3200
3311
  const contextWindow = resolveContextWindowTokens({
3201
3312
  config,
3202
- modelContextWindow: options.modelContextWindow
3313
+ modelContextWindow: options.modelContextWindow,
3314
+ endpointLimitTokens: options.endpointLimitTokens
3315
+ });
3316
+ const inputBudgetTokens = resolveInputBudgetTokens({
3317
+ contextWindow,
3318
+ reservedOutputTokens: options.reservedOutputTokens
3203
3319
  });
3204
3320
  let anchorResolved = false;
3205
3321
  let afterAnchorIndex = 0;
@@ -3223,7 +3339,8 @@ function buildProviderView(options) {
3223
3339
  let tier1 = null;
3224
3340
  const uncachedActive = options.cacheActive === false && config.aggressivePruneWhenUncached;
3225
3341
  const pruneFraction = uncachedActive ? config.uncachedPruneAtFraction : config.pruneAtFraction;
3226
- if (estimate.tokens > pruneFraction * contextWindow) {
3342
+ const triggerTokens = (currentEstimateTokens) => Math.max(currentEstimateTokens, estimateStructuralTokens(providerMessages), typeof options.measuredInputTokens === "number" ? options.measuredInputTokens : 0);
3343
+ if (triggerTokens(estimate.tokens) > pruneFraction * inputBudgetTokens) {
3227
3344
  tier1 = pruneToolOutputs(providerMessages, {
3228
3345
  preserveRecentMessages: config.preserveRecentMessages,
3229
3346
  minOutputChars: uncachedActive ? config.uncachedPruneMinOutputChars : undefined,
@@ -3238,15 +3355,15 @@ function buildProviderView(options) {
3238
3355
  }
3239
3356
  }
3240
3357
  const compactionCutoffIndex = computeCompactionCutoffIndex(messages, config.preserveRecentMessages, afterAnchorIndex);
3241
- const coveredMessages = messages.slice(afterAnchorIndex, compactionCutoffIndex);
3242
- const overCompactThreshold = estimate.tokens > config.compactAtFraction * contextWindow;
3358
+ const coveredMessages = capCoverageToTokenBudget(messages.slice(afterAnchorIndex, compactionCutoffIndex), config.maxCoverageTokensPerCompaction);
3359
+ const overCompactThreshold = triggerTokens(estimate.tokens) > config.compactAtFraction * inputBudgetTokens;
3243
3360
  let compactionBlockedReason = null;
3244
3361
  if (overCompactThreshold) {
3245
3362
  if (coveredMessages.length === 0) {
3246
3363
  compactionBlockedReason = "not_enough_messages";
3247
3364
  } else if ((options.pendingInteractionCount ?? 0) > 0) {
3248
3365
  compactionBlockedReason = "pending_interactions";
3249
- } else if (hasUnresolvedToolWork(messages)) {
3366
+ } else if (hasUnresolvedToolWork(coveredMessages)) {
3250
3367
  compactionBlockedReason = "unresolved_tool_work";
3251
3368
  }
3252
3369
  }
@@ -3254,6 +3371,7 @@ function buildProviderView(options) {
3254
3371
  providerMessages,
3255
3372
  estimate,
3256
3373
  contextWindow,
3374
+ inputBudgetTokens,
3257
3375
  needsCompaction: overCompactThreshold && compactionBlockedReason === null,
3258
3376
  compactionBlockedReason,
3259
3377
  anchorResolved,
@@ -3265,6 +3383,7 @@ function buildProviderView(options) {
3265
3383
  var init_build_provider_view = __esm(() => {
3266
3384
  init_config3();
3267
3385
  init_estimate();
3386
+ init_fit_to_budget();
3268
3387
  init_heuristic_summary();
3269
3388
  init_message_parts();
3270
3389
  init_tier1_prune();
@@ -3310,6 +3429,7 @@ var init_context = __esm(() => {
3310
3429
  init_config3();
3311
3430
  init_context_state();
3312
3431
  init_estimate();
3432
+ init_fit_to_budget();
3313
3433
  init_heuristic_summary();
3314
3434
  init_message_parts();
3315
3435
  init_normalize_provider_messages();
@@ -3583,7 +3703,7 @@ var init_lightcode_config = __esm(() => {
3583
3703
  context: defaultContextOptimizerConfig,
3584
3704
  maxOutputTokens: 32768,
3585
3705
  maxSteps: 30,
3586
- maxRetries: 5,
3706
+ maxRetries: 3,
3587
3707
  autoContinue: defaultAutoContinueConfig,
3588
3708
  headroom: defaultHeadroomConfig
3589
3709
  };
@@ -4097,6 +4217,10 @@ async function extractToolCalls(text, knownToolNames) {
4097
4217
  while ((match = DEEPSEEK_WRAPPER_RE.exec(text)) !== null) {
4098
4218
  segments.push({ start: match.index, end: match.index + match[0].length, call: null });
4099
4219
  }
4220
+ FUNCTION_CALLS_WRAPPER_RE.lastIndex = 0;
4221
+ while ((match = FUNCTION_CALLS_WRAPPER_RE.exec(text)) !== null) {
4222
+ segments.push({ start: match.index, end: match.index + match[0].length, call: null });
4223
+ }
4100
4224
  const covered = (idx) => segments.some((s) => idx >= s.start && idx < s.end);
4101
4225
  const orphanIdx = findOrphanMarker(text, covered);
4102
4226
  if (orphanIdx !== -1) {
@@ -4184,7 +4308,7 @@ async function parseBareTrailingJson(text, knownToolNames) {
4184
4308
  return null;
4185
4309
  return { start: idx, payload };
4186
4310
  }
4187
- var TOOL_CALL_OPEN_MARKERS, MAX_MARKER_LENGTH, MARKER_CONFIRMATION_WINDOW = 256, BLOCK_CLOSE_MARKERS, CLOSED_BLOCK_RES, DEEPSEEK_WRAPPER_RE, PARAMETER_TAG_RE, PARAMETER_EQ_RE;
4311
+ var TOOL_CALL_OPEN_MARKERS, MAX_MARKER_LENGTH, MARKER_CONFIRMATION_WINDOW = 256, BLOCK_CLOSE_MARKERS, CLOSED_BLOCK_RES, DEEPSEEK_WRAPPER_RE, FUNCTION_CALLS_WRAPPER_RE, PARAMETER_TAG_RE, PARAMETER_EQ_RE;
4188
4312
  var init_xml_tool_call_parser = __esm(() => {
4189
4313
  init_tool_call_json_repair();
4190
4314
  TOOL_CALL_OPEN_MARKERS = [
@@ -4211,6 +4335,7 @@ var init_xml_tool_call_parser = __esm(() => {
4211
4335
  deepseek: /<\uFF5Ctool\u2581call\u2581begin\uFF5C>\s*(?:function)?\s*(?:<\uFF5Ctool\u2581sep\uFF5C>)?\s*([\w.-]+)\s*\n?([\s\S]*?)<\uFF5Ctool\u2581call\u2581end\uFF5C>/g
4212
4336
  };
4213
4337
  DEEPSEEK_WRAPPER_RE = /<\uFF5Ctool\u2581calls\u2581(?:begin|end)\uFF5C>/g;
4338
+ FUNCTION_CALLS_WRAPPER_RE = /<\/?function_calls\s*>/gi;
4214
4339
  PARAMETER_TAG_RE = /<parameter\s+name=["']?([\w.-]+)["']?\s*>([\s\S]*?)(?:<\/parameter>|(?=<parameter\s)|$)/gi;
4215
4340
  PARAMETER_EQ_RE = /<parameter=([\w.-]+)>([\s\S]*?)(?=<parameter=|<\/function>|$)/gi;
4216
4341
  });
@@ -5099,6 +5224,14 @@ function isProviderBillingOrQuotaError(error) {
5099
5224
  function isContextOverflowMessage(message) {
5100
5225
  return contextOverflowPattern.test(message);
5101
5226
  }
5227
+ function extractContextLimitTokens(message) {
5228
+ const match = contextLimitPattern.exec(message);
5229
+ if (!match) {
5230
+ return;
5231
+ }
5232
+ const value = Number.parseInt(match[1], 10);
5233
+ return Number.isFinite(value) && value > 0 ? value : undefined;
5234
+ }
5102
5235
  function isAbortError(error) {
5103
5236
  return error instanceof Error && error.name === "AbortError" || normalizeErrorMessage(getErrorMessage(error)).includes("aborted");
5104
5237
  }
@@ -5139,7 +5272,8 @@ function classifyChatError(error) {
5139
5272
  kind,
5140
5273
  statusCode: error.statusCode,
5141
5274
  retryable,
5142
- message: message2
5275
+ message: message2,
5276
+ contextLimitTokens: kind === "context_overflow" ? extractContextLimitTokens(message2) : undefined
5143
5277
  };
5144
5278
  }
5145
5279
  return {
@@ -5153,7 +5287,12 @@ function classifyChatError(error) {
5153
5287
  return { kind: "aborted", retryable: false, message };
5154
5288
  }
5155
5289
  if (isContextOverflowMessage(message)) {
5156
- return { kind: "context_overflow", retryable: true, message };
5290
+ return {
5291
+ kind: "context_overflow",
5292
+ retryable: true,
5293
+ message,
5294
+ contextLimitTokens: extractContextLimitTokens(message)
5295
+ };
5157
5296
  }
5158
5297
  if (isProviderBillingOrQuotaError(error)) {
5159
5298
  return { kind: "billing", retryable: false, message };
@@ -5219,7 +5358,7 @@ function logChatDisconnectEvent({
5219
5358
  message: getErrorMessage(error)
5220
5359
  });
5221
5360
  }
5222
- var chatFailureCounters, logger2, retryableStatusCodes, contextOverflowPattern;
5361
+ var chatFailureCounters, logger2, retryableStatusCodes, contextOverflowPattern, contextLimitPattern;
5223
5362
  var init_chat_observability = __esm(() => {
5224
5363
  init_src();
5225
5364
  chatFailureCounters = {
@@ -5238,6 +5377,7 @@ var init_chat_observability = __esm(() => {
5238
5377
  logger2 = createLogger("chat");
5239
5378
  retryableStatusCodes = new Set([408, 409, 429, 500, 502, 503, 504]);
5240
5379
  contextOverflowPattern = /(context window|context length|context_length|maximum context|prompt is too long|input is too long|too many tokens|exceeds? the (?:maximum|model'?s) (?:context|token)|max(?:imum)? input tokens)/i;
5380
+ contextLimitPattern = /(?:maximum context (?:length|window)|max(?:imum)? input (?:length|tokens)|context (?:length|window) of)\D{0,40}?(\d{4,})/i;
5241
5381
  });
5242
5382
 
5243
5383
  // packages/db/generated/prisma/internal/class.ts
@@ -6035,6 +6175,22 @@ function mergeFinishedMessagesIntoFullHistory({
6035
6175
  return [...fullMessages, ...appendedMessages];
6036
6176
  }
6037
6177
 
6178
+ // packages/ai/src/tool-call-repeat-guard.ts
6179
+ var guardedTools, lastCallByTurn;
6180
+ var init_tool_call_repeat_guard = __esm(() => {
6181
+ guardedTools = new Set([
6182
+ "read_file",
6183
+ "list_files",
6184
+ "glob_search",
6185
+ "grep",
6186
+ "git_status",
6187
+ "git_diff",
6188
+ "git_log",
6189
+ "git_show"
6190
+ ]);
6191
+ lastCallByTurn = new Map;
6192
+ });
6193
+
6038
6194
  // packages/ai/src/common/resolve-within-workspace.ts
6039
6195
  import path8 from "path";
6040
6196
  import {
@@ -6133,9 +6289,6 @@ var init_resolve_within_workspace = __esm(() => {
6133
6289
  WORKSPACE = defaultWorkspaceContext.root;
6134
6290
  });
6135
6291
 
6136
- // packages/ai/src/common/output-utils.ts
6137
- var init_output_utils = () => {};
6138
-
6139
6292
  // packages/ai/src/bash/runtime.ts
6140
6293
  import { exec } from "child_process";
6141
6294
  import { promisify } from "util";
@@ -6171,11 +6324,15 @@ var init_runtime5 = __esm(() => {
6171
6324
  import { createTwoFilesPatch } from "diff";
6172
6325
  var init_unified_diff = () => {};
6173
6326
 
6327
+ // packages/ai/src/edit-file/match.ts
6328
+ var init_match = () => {};
6329
+
6174
6330
  // packages/ai/src/edit-file/runtime.ts
6175
6331
  var init_runtime6 = __esm(() => {
6176
6332
  init_runtime5();
6177
6333
  init_resolve_within_workspace();
6178
6334
  init_unified_diff();
6335
+ init_match();
6179
6336
  init_schema2();
6180
6337
  });
6181
6338
 
@@ -6413,6 +6570,8 @@ var init_runtime16 = __esm(() => {
6413
6570
  // packages/ai/src/runtime-registry.ts
6414
6571
  var init_runtime_registry = __esm(() => {
6415
6572
  init_agent_tools();
6573
+ init_coerce_tool_input();
6574
+ init_tool_call_repeat_guard();
6416
6575
  init_runtime4();
6417
6576
  init_runtime6();
6418
6577
  init_runtime7();
@@ -6722,6 +6881,24 @@ var init_overflow_recovery = __esm(() => {
6722
6881
  overflowRecoveryRounds = new Map;
6723
6882
  });
6724
6883
 
6884
+ // apps/server/src/lib/learned-context-limit.ts
6885
+ function noteLearnedContextLimit(modelId, limitTokens) {
6886
+ if (!modelId || typeof limitTokens !== "number" || !Number.isFinite(limitTokens) || limitTokens <= 0) {
6887
+ return;
6888
+ }
6889
+ const existing = learnedLimits.get(modelId);
6890
+ if (existing === undefined || limitTokens < existing) {
6891
+ learnedLimits.set(modelId, Math.floor(limitTokens));
6892
+ }
6893
+ }
6894
+ function getLearnedContextLimit(modelId) {
6895
+ return learnedLimits.get(modelId) ?? null;
6896
+ }
6897
+ var learnedLimits;
6898
+ var init_learned_context_limit = __esm(() => {
6899
+ learnedLimits = new Map;
6900
+ });
6901
+
6725
6902
  // apps/server/src/lib/sse-heartbeat.ts
6726
6903
  function endsWithEventBoundary(chunk) {
6727
6904
  const length = chunk.length;
@@ -6947,6 +7124,16 @@ function getAnthropicProviderOptions(modelId) {
6947
7124
  }
6948
7125
  };
6949
7126
  }
7127
+ function getOpenRouterProviderOptions() {
7128
+ return {
7129
+ openrouter: {
7130
+ provider: {
7131
+ allow_fallbacks: true,
7132
+ require_parameters: true
7133
+ }
7134
+ }
7135
+ };
7136
+ }
6950
7137
  function resolveHeadroomRouting(config5, realBaseUrl) {
6951
7138
  const headroom = config5.headroom;
6952
7139
  if (!headroom?.enabled) {
@@ -7011,12 +7198,8 @@ function resolveOpenAITransportModel({
7011
7198
  if (isOpenRouter) {
7012
7199
  const referer = getEnvValue(env, "OPENROUTER_HTTP_REFERER");
7013
7200
  const title = getEnvValue(env, "OPENROUTER_APP_TITLE", "LIGHTCODE_APP_TITLE");
7014
- if (referer) {
7015
- headers["HTTP-Referer"] = referer;
7016
- }
7017
- if (title) {
7018
- headers["X-OpenRouter-Title"] = title;
7019
- }
7201
+ headers["HTTP-Referer"] = referer ?? "https://github.com/kmugalkhod/lightcode";
7202
+ headers["X-Title"] = title ?? "Lightcode";
7020
7203
  }
7021
7204
  const provider = createOpenAICompatible({
7022
7205
  name: config5.provider,
@@ -7033,6 +7216,7 @@ function resolveOpenAITransportModel({
7033
7216
  headroomRouted,
7034
7217
  model: withXmlToolCallSupport(provider(configuredModel)),
7035
7218
  needsToolCallDiscipline: true,
7219
+ providerOptions: isOpenRouter ? getOpenRouterProviderOptions() : undefined,
7036
7220
  contextWindow: capabilities?.contextLength ?? getModelContextWindow(config5.provider, configuredModel),
7037
7221
  missingCredentialHints: apiKey ? [] : isOpenCodeZen ? ["Set OPENCODE_API_KEY to call OpenCode Zen models."] : isOpenRouter ? ["Set OPENROUTER_API_KEY to call OpenRouter models."] : [
7038
7222
  "Set LIGHTCODE_OPENAI_COMPATIBLE_API_KEY or OPENAI_API_KEY if your OpenAI-compatible endpoint requires authentication."
@@ -7323,6 +7507,26 @@ var init_session_auto_title = __esm(() => {
7323
7507
  import { readFile as readFile2, readdir } from "fs/promises";
7324
7508
  import { platform as platform3 } from "os";
7325
7509
  import path11 from "path";
7510
+ function formatAvailableSkills(skills) {
7511
+ if (skills.length === 0) {
7512
+ return "";
7513
+ }
7514
+ const shown = skills.slice(0, maxListedSkills);
7515
+ const lines = shown.map((skill) => `- ${skill.name}${skill.description ? `: ${skill.description}` : ""}`);
7516
+ const more = skills.length > shown.length ? `
7517
+ \u2026 and ${skills.length - shown.length} more` : "";
7518
+ return `Available skills (load by name with the skill tool):
7519
+ ${lines.join(`
7520
+ `)}${more}`;
7521
+ }
7522
+ function buildAvailableSkills(cwd) {
7523
+ try {
7524
+ return formatAvailableSkills(listSkills({ cwd }));
7525
+ } catch (error) {
7526
+ logger7.warn("workspace_skills_failed", { cwd, error: getErrorMessage(error) });
7527
+ return "";
7528
+ }
7529
+ }
7326
7530
  function describePlatform() {
7327
7531
  const map = {
7328
7532
  darwin: "macOS",
@@ -7398,6 +7602,7 @@ async function buildWorkspaceContext({
7398
7602
  buildGitSummary(cwd),
7399
7603
  buildProjectDocs(cwd)
7400
7604
  ]);
7605
+ const skills = buildAvailableSkills(cwd);
7401
7606
  const today = new Date().toISOString().slice(0, 10);
7402
7607
  const parts = [
7403
7608
  "<environment>",
@@ -7413,6 +7618,8 @@ async function buildWorkspaceContext({
7413
7618
  docs ? `
7414
7619
  Project instructions:
7415
7620
  ${docs}` : "",
7621
+ skills ? `
7622
+ ${skills}` : "",
7416
7623
  "</environment>"
7417
7624
  ];
7418
7625
  const block = parts.join(`
@@ -7421,7 +7628,7 @@ ${docs}` : "",
7421
7628
  \u2026 (environment truncated)
7422
7629
  </environment>` : block;
7423
7630
  }
7424
- var logger7, projectDocNames, maxListedEntries = 40, maxDocChars = 1500, maxBlockChars = 8000;
7631
+ var logger7, projectDocNames, maxListedEntries = 40, maxListedSkills = 20, maxDocChars = 1500, maxBlockChars = 8000;
7425
7632
  var init_workspace_context = __esm(() => {
7426
7633
  init_runtime17();
7427
7634
  init_src();
@@ -7460,7 +7667,8 @@ function messagesHaveToolActivity(messages) {
7460
7667
  function shouldUseFastChatPath({
7461
7668
  messages,
7462
7669
  mode,
7463
- allowedTools
7670
+ allowedTools,
7671
+ availableSkillNames = []
7464
7672
  }) {
7465
7673
  if (allowedTools && allowedTools.length > 0) {
7466
7674
  return false;
@@ -7471,7 +7679,8 @@ function shouldUseFastChatPath({
7471
7679
  return selectCodingAgentIntentTools({
7472
7680
  mode,
7473
7681
  prompt: "",
7474
- messages
7682
+ messages,
7683
+ availableSkillNames
7475
7684
  }).length === 0;
7476
7685
  }
7477
7686
  async function countPendingChatInteractions(sessionId) {
@@ -7553,6 +7762,8 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7553
7762
  }
7554
7763
  const contextConfig = lightcodeConfigResult.config.context;
7555
7764
  const cacheActive = resolvedProviderModel.provider === "anthropic";
7765
+ const reservedOutputTokens = resolveMaxOutputTokens(lightcodeConfigResult.config.maxOutputTokens, lightcodeConfigDefaults.maxOutputTokens, resolvedProviderModel.maxCompletionTokens);
7766
+ const endpointLimitTokens = getLearnedContextLimit(chatModelId);
7556
7767
  const pendingInteractionCount = await countPendingChatInteractions(sessionId);
7557
7768
  let contextState = await loadSessionContextStateSafe(sessionId);
7558
7769
  let view = buildProviderView({
@@ -7560,44 +7771,56 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7560
7771
  contextState,
7561
7772
  config: contextConfig,
7562
7773
  modelContextWindow: resolvedProviderModel.contextWindow,
7774
+ reservedOutputTokens,
7775
+ endpointLimitTokens,
7563
7776
  pendingInteractionCount,
7564
7777
  cacheActive
7565
7778
  });
7566
- if (view.needsCompaction && contextConfig.autoCompact) {
7567
- try {
7568
- const estimatedTokensBefore = view.estimate.tokens;
7569
- const compaction = await compactSessionContext({
7570
- sessionId,
7571
- coveredMessages: view.coveredMessages,
7572
- previousState: contextState,
7573
- model: resolvedProviderModel.model,
7574
- modelId: chatModelId,
7575
- cwd,
7576
- config: contextConfig,
7577
- estimatedTokens: estimatedTokensBefore
7578
- });
7579
- contextState = compaction.state;
7580
- view = buildProviderView({
7581
- messages: validatedMessages,
7582
- contextState,
7583
- config: contextConfig,
7584
- modelContextWindow: resolvedProviderModel.contextWindow,
7585
- pendingInteractionCount,
7586
- cacheActive
7587
- });
7588
- logger8.info("context_auto_compacted", {
7589
- sessionId,
7590
- tier: compaction.state.tier,
7591
- usedFallback: compaction.usedFallback,
7592
- coveredMessageCount: compaction.state.coveredMessageCount,
7593
- estimatedTokensBefore,
7594
- estimatedTokensAfter: view.estimate.tokens
7595
- });
7596
- } catch (error) {
7597
- logger8.error("context_compaction_failed", {
7598
- sessionId,
7599
- error: getErrorMessage(error)
7600
- });
7779
+ if (contextConfig.autoCompact) {
7780
+ let compactionRound = 0;
7781
+ while (view.needsCompaction && compactionRound < MAX_COMPACTION_ROUNDS) {
7782
+ compactionRound += 1;
7783
+ try {
7784
+ const estimatedTokensBefore = view.estimate.tokens;
7785
+ const compaction = await compactSessionContext({
7786
+ sessionId,
7787
+ coveredMessages: view.coveredMessages,
7788
+ previousState: contextState,
7789
+ model: resolvedProviderModel.model,
7790
+ modelId: chatModelId,
7791
+ cwd,
7792
+ config: contextConfig,
7793
+ estimatedTokens: estimatedTokensBefore
7794
+ });
7795
+ contextState = compaction.state;
7796
+ view = buildProviderView({
7797
+ messages: validatedMessages,
7798
+ contextState,
7799
+ config: contextConfig,
7800
+ modelContextWindow: resolvedProviderModel.contextWindow,
7801
+ reservedOutputTokens,
7802
+ endpointLimitTokens,
7803
+ pendingInteractionCount,
7804
+ cacheActive
7805
+ });
7806
+ logger8.info("context_auto_compacted", {
7807
+ sessionId,
7808
+ round: compactionRound,
7809
+ tier: compaction.state.tier,
7810
+ usedFallback: compaction.usedFallback,
7811
+ coveredMessageCount: compaction.state.coveredMessageCount,
7812
+ estimatedTokensBefore,
7813
+ estimatedTokensAfter: view.estimate.tokens,
7814
+ stillNeedsCompaction: view.needsCompaction
7815
+ });
7816
+ } catch (error) {
7817
+ logger8.error("context_compaction_failed", {
7818
+ sessionId,
7819
+ round: compactionRound,
7820
+ error: getErrorMessage(error)
7821
+ });
7822
+ break;
7823
+ }
7601
7824
  }
7602
7825
  }
7603
7826
  const overflowPreserve = getOverflowPreserveRecentMessages(sessionId);
@@ -7612,6 +7835,8 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7612
7835
  contextState,
7613
7836
  config: overflowConfig,
7614
7837
  modelContextWindow: resolvedProviderModel.contextWindow,
7838
+ reservedOutputTokens,
7839
+ endpointLimitTokens,
7615
7840
  pendingInteractionCount: 0,
7616
7841
  cacheActive
7617
7842
  });
@@ -7633,6 +7858,8 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7633
7858
  contextState,
7634
7859
  config: overflowConfig,
7635
7860
  modelContextWindow: resolvedProviderModel.contextWindow,
7861
+ reservedOutputTokens,
7862
+ endpointLimitTokens,
7636
7863
  pendingInteractionCount: 0,
7637
7864
  cacheActive
7638
7865
  });
@@ -7651,7 +7878,24 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7651
7878
  });
7652
7879
  }
7653
7880
  }
7654
- const providerMessages = normalizeProviderMessages(view.providerMessages);
7881
+ const fitted = fitMessagesToBudget(view.providerMessages, {
7882
+ inputBudgetTokens: view.inputBudgetTokens,
7883
+ preserveRecentMessages: contextConfig.preserveRecentMessages
7884
+ });
7885
+ if (fitted.fitted) {
7886
+ logger8.warn("context_fit_to_budget", {
7887
+ sessionId,
7888
+ inputBudgetTokens: view.inputBudgetTokens,
7889
+ contextWindow: view.contextWindow,
7890
+ estimatedTokensBefore: estimateStructuralTokens(view.providerMessages),
7891
+ estimatedTokensAfter: fitted.estimatedTokens,
7892
+ elidedToolOutputs: fitted.elidedToolOutputs,
7893
+ droppedMessages: fitted.droppedMessages,
7894
+ truncatedTextParts: fitted.truncatedTextParts,
7895
+ withinBudget: fitted.withinBudget
7896
+ });
7897
+ }
7898
+ const providerMessages = normalizeProviderMessages(fitted.messages);
7655
7899
  let overflowNotedThisRequest = false;
7656
7900
  const noteOverflowForSession = () => {
7657
7901
  if (overflowNotedThisRequest) {
@@ -7745,15 +7989,22 @@ async function streamSessionChat(c, sessionIdentifier, messagesPayload, cwd, mod
7745
7989
  totalTokens: part.totalUsage.totalTokens
7746
7990
  },
7747
7991
  modelId: chatModelId,
7748
- finishReason: part.finishReason
7992
+ finishReason: part.finishReason,
7993
+ context: {
7994
+ inputTokens: view.estimate.tokens,
7995
+ contextWindow: view.contextWindow,
7996
+ compactedMessages: contextState?.coveredMessageCount ?? 0
7997
+ }
7749
7998
  };
7750
7999
  };
7751
8000
  const environmentContext = await buildWorkspaceContext({ cwd });
8001
+ const availableSkillNames = listSkills({ cwd }).map((skill) => skill.name);
7752
8002
  try {
7753
8003
  if (shouldUseFastChatPath({
7754
8004
  messages: providerMessages,
7755
8005
  mode,
7756
- allowedTools
8006
+ allowedTools,
8007
+ availableSkillNames
7757
8008
  })) {
7758
8009
  logger8.info("chat_fast_path", {
7759
8010
  sessionId,
@@ -7793,6 +8044,7 @@ ${environmentContext}`,
7793
8044
  onError: (error) => {
7794
8045
  const classified = classifyChatError(error);
7795
8046
  if (classified.kind === "context_overflow") {
8047
+ noteLearnedContextLimit(chatModelId, classified.contextLimitTokens);
7796
8048
  noteOverflowForSession();
7797
8049
  }
7798
8050
  incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), { sessionId, statusCode: classified.statusCode });
@@ -7810,6 +8062,7 @@ ${environmentContext}`,
7810
8062
  const mapStreamError = (error) => {
7811
8063
  const classified = classifyChatError(error);
7812
8064
  if (classified.kind === "context_overflow") {
8065
+ noteLearnedContextLimit(chatModelId, classified.contextLimitTokens);
7813
8066
  noteOverflowForSession();
7814
8067
  }
7815
8068
  incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), {
@@ -7864,6 +8117,7 @@ ${environmentContext}`,
7864
8117
  } catch (error) {
7865
8118
  const classified = classifyChatError(error);
7866
8119
  if (classified.kind === "context_overflow") {
8120
+ noteLearnedContextLimit(chatModelId, classified.contextLimitTokens);
7867
8121
  noteContextOverflow(sessionId);
7868
8122
  }
7869
8123
  incrementChatFailureCounter(chatFailureClassForErrorKind(classified.kind), {
@@ -7894,7 +8148,7 @@ ${environmentContext}`,
7894
8148
  }, 500);
7895
8149
  }
7896
8150
  }
7897
- var logger8, recoverableDisconnectMessage = "Connection interrupted. Please retry or regenerate your last message.", providerBillingOrQuotaMessage, fastChatSystemPrompt, fastChatMaxOutputTokens = 512, fastChatRecentMessageCount = 6;
8151
+ var logger8, recoverableDisconnectMessage = "Connection interrupted. Please retry or regenerate your last message.", providerBillingOrQuotaMessage, fastChatSystemPrompt, fastChatMaxOutputTokens = 512, fastChatRecentMessageCount = 6, MAX_COMPACTION_ROUNDS = 3;
7898
8152
  var init_chat_stream = __esm(() => {
7899
8153
  init_src2();
7900
8154
  init_src();
@@ -7903,6 +8157,7 @@ var init_chat_stream = __esm(() => {
7903
8157
  init_chat_store();
7904
8158
  init_context_compaction();
7905
8159
  init_overflow_recovery();
8160
+ init_learned_context_limit();
7906
8161
  init_sse_heartbeat();
7907
8162
  init_context_state_store();
7908
8163
  init_session_auto_title();