@ljoukov/llm 0.1.3 → 2.1.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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/llm.ts
2
- import { Buffer as Buffer2 } from "buffer";
2
+ import { Buffer as Buffer3 } from "buffer";
3
3
  import { AsyncLocalStorage } from "async_hooks";
4
4
  import { randomBytes } from "crypto";
5
5
  import {
@@ -125,12 +125,23 @@ var OPENAI_GPT_52_PRICING = {
125
125
  cachedRate: 0.175 / 1e6,
126
126
  outputRate: 14 / 1e6
127
127
  };
128
+ var OPENAI_GPT_53_CODEX_PRICING = {
129
+ inputRate: 1.25 / 1e6,
130
+ cachedRate: 0.125 / 1e6,
131
+ outputRate: 10 / 1e6
132
+ };
128
133
  var OPENAI_GPT_51_CODEX_MINI_PRICING = {
129
134
  inputRate: 0.25 / 1e6,
130
135
  cachedRate: 0.025 / 1e6,
131
136
  outputRate: 2 / 1e6
132
137
  };
133
138
  function getOpenAiPricing(modelId) {
139
+ if (modelId.includes("gpt-5.3-codex")) {
140
+ return OPENAI_GPT_53_CODEX_PRICING;
141
+ }
142
+ if (modelId.includes("gpt-5-codex")) {
143
+ return OPENAI_GPT_53_CODEX_PRICING;
144
+ }
134
145
  if (modelId.includes("gpt-5.2")) {
135
146
  return OPENAI_GPT_52_PRICING;
136
147
  }
@@ -212,7 +223,7 @@ import os from "os";
212
223
  import { TextDecoder } from "util";
213
224
 
214
225
  // src/openai/chatgpt-auth.ts
215
- import { Buffer } from "buffer";
226
+ import { Buffer as Buffer2 } from "buffer";
216
227
  import { z } from "zod";
217
228
 
218
229
  // src/utils/env.ts
@@ -330,7 +341,7 @@ function encodeChatGptAuthJson(profile) {
330
341
  return JSON.stringify(payload);
331
342
  }
332
343
  function encodeChatGptAuthJsonB64(profile) {
333
- return Buffer.from(encodeChatGptAuthJson(profile)).toString("base64url");
344
+ return Buffer2.from(encodeChatGptAuthJson(profile)).toString("base64url");
334
345
  }
335
346
  async function exchangeChatGptOauthCode({
336
347
  code,
@@ -464,7 +475,7 @@ function loadAuthProfileFromEnv() {
464
475
  }
465
476
  const rawB64 = process.env[CHATGPT_AUTH_JSON_B64_ENV];
466
477
  if (rawB64 && rawB64.trim().length > 0) {
467
- const decoded = Buffer.from(rawB64.trim(), "base64url").toString("utf8");
478
+ const decoded = Buffer2.from(rawB64.trim(), "base64url").toString("utf8");
468
479
  return normalizeAuthProfile(AuthInputSchema.parse(JSON.parse(decoded)));
469
480
  }
470
481
  const access = process.env[CHATGPT_ACCESS_ENV] ?? process.env[CHATGPT_ACCESS_TOKEN_ENV] ?? void 0;
@@ -488,7 +499,7 @@ function decodeJwtPayload(token) {
488
499
  }
489
500
  const payloadB64 = segments[1] ?? "";
490
501
  try {
491
- const decoded = Buffer.from(payloadB64, "base64url").toString("utf8");
502
+ const decoded = Buffer2.from(payloadB64, "base64url").toString("utf8");
492
503
  return JSON.parse(decoded);
493
504
  } catch {
494
505
  return null;
@@ -1475,9 +1486,9 @@ function sanitisePartForLogging(part) {
1475
1486
  case "inlineData": {
1476
1487
  let omittedBytes;
1477
1488
  try {
1478
- omittedBytes = Buffer2.from(part.data, "base64").byteLength;
1489
+ omittedBytes = Buffer3.from(part.data, "base64").byteLength;
1479
1490
  } catch {
1480
- omittedBytes = Buffer2.byteLength(part.data, "utf8");
1491
+ omittedBytes = Buffer3.byteLength(part.data, "utf8");
1481
1492
  }
1482
1493
  return {
1483
1494
  type: "inlineData",
@@ -1518,10 +1529,13 @@ function convertGooglePartsToLlmParts(parts) {
1518
1529
  function assertLlmRole(value) {
1519
1530
  switch (value) {
1520
1531
  case "user":
1521
- case "model":
1532
+ case "assistant":
1522
1533
  case "system":
1534
+ case "developer":
1523
1535
  case "tool":
1524
1536
  return value;
1537
+ case "model":
1538
+ return "assistant";
1525
1539
  default:
1526
1540
  throw new Error(`Unsupported LLM role: ${String(value)}`);
1527
1541
  }
@@ -1551,8 +1565,9 @@ function toGeminiPart(part) {
1551
1565
  }
1552
1566
  }
1553
1567
  function convertLlmContentToGeminiContent(content) {
1568
+ const role = content.role === "assistant" ? "model" : "user";
1554
1569
  return {
1555
- role: content.role,
1570
+ role,
1556
1571
  parts: content.parts.map(toGeminiPart)
1557
1572
  };
1558
1573
  }
@@ -1723,28 +1738,350 @@ function parseJsonFromLlmText(rawText) {
1723
1738
  const repairedText = escapeNewlinesInStrings(cleanedText);
1724
1739
  return JSON.parse(repairedText);
1725
1740
  }
1726
- function resolveTextContents(input) {
1727
- if ("contents" in input) {
1728
- return input.contents;
1741
+ function parsePartialJsonFromLlmText(rawText) {
1742
+ const jsonStart = extractJsonStartText(rawText);
1743
+ if (!jsonStart) {
1744
+ return null;
1745
+ }
1746
+ try {
1747
+ return parsePartialJson(jsonStart);
1748
+ } catch {
1749
+ return null;
1750
+ }
1751
+ }
1752
+ function extractJsonStartText(rawText) {
1753
+ let text = rawText.trimStart();
1754
+ if (text.startsWith("```")) {
1755
+ text = text.replace(/^```[a-zA-Z0-9_-]*\s*\n?/, "");
1756
+ }
1757
+ const objIndex = text.indexOf("{");
1758
+ const arrIndex = text.indexOf("[");
1759
+ let start = -1;
1760
+ if (objIndex !== -1 && arrIndex !== -1) {
1761
+ start = Math.min(objIndex, arrIndex);
1762
+ } else {
1763
+ start = objIndex !== -1 ? objIndex : arrIndex;
1764
+ }
1765
+ if (start === -1) {
1766
+ return null;
1767
+ }
1768
+ return text.slice(start);
1769
+ }
1770
+ function parsePartialJson(text) {
1771
+ let i = 0;
1772
+ const len = text.length;
1773
+ const isWhitespace = (char) => char === " " || char === "\n" || char === "\r" || char === " ";
1774
+ const skipWhitespace = () => {
1775
+ while (i < len && isWhitespace(text[i] ?? "")) {
1776
+ i += 1;
1777
+ }
1778
+ };
1779
+ const parseString = () => {
1780
+ if (text[i] !== '"') {
1781
+ return null;
1782
+ }
1783
+ i += 1;
1784
+ let value = "";
1785
+ while (i < len) {
1786
+ const ch = text[i] ?? "";
1787
+ if (ch === '"') {
1788
+ i += 1;
1789
+ return { value, complete: true };
1790
+ }
1791
+ if (ch === "\\") {
1792
+ if (i + 1 >= len) {
1793
+ return { value, complete: false };
1794
+ }
1795
+ const esc = text[i + 1] ?? "";
1796
+ switch (esc) {
1797
+ case '"':
1798
+ case "\\":
1799
+ case "/":
1800
+ value += esc;
1801
+ i += 2;
1802
+ continue;
1803
+ case "b":
1804
+ value += "\b";
1805
+ i += 2;
1806
+ continue;
1807
+ case "f":
1808
+ value += "\f";
1809
+ i += 2;
1810
+ continue;
1811
+ case "n":
1812
+ value += "\n";
1813
+ i += 2;
1814
+ continue;
1815
+ case "r":
1816
+ value += "\r";
1817
+ i += 2;
1818
+ continue;
1819
+ case "t":
1820
+ value += " ";
1821
+ i += 2;
1822
+ continue;
1823
+ case "u": {
1824
+ if (i + 5 >= len) {
1825
+ return { value, complete: false };
1826
+ }
1827
+ const hex = text.slice(i + 2, i + 6);
1828
+ if (!/^[0-9a-fA-F]{4}$/u.test(hex)) {
1829
+ value += "u";
1830
+ i += 2;
1831
+ continue;
1832
+ }
1833
+ value += String.fromCharCode(Number.parseInt(hex, 16));
1834
+ i += 6;
1835
+ continue;
1836
+ }
1837
+ default:
1838
+ value += esc;
1839
+ i += 2;
1840
+ continue;
1841
+ }
1842
+ }
1843
+ value += ch;
1844
+ i += 1;
1845
+ }
1846
+ return { value, complete: false };
1847
+ };
1848
+ const parseNumber = () => {
1849
+ const start = i;
1850
+ while (i < len) {
1851
+ const ch = text[i] ?? "";
1852
+ if (isWhitespace(ch) || ch === "," || ch === "}" || ch === "]") {
1853
+ break;
1854
+ }
1855
+ i += 1;
1856
+ }
1857
+ const raw = text.slice(start, i);
1858
+ if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/u.test(raw)) {
1859
+ i = start;
1860
+ return null;
1861
+ }
1862
+ return { value: Number(raw), complete: true };
1863
+ };
1864
+ const parseLiteral = () => {
1865
+ if (text.startsWith("true", i)) {
1866
+ i += 4;
1867
+ return { value: true, complete: true };
1868
+ }
1869
+ if (text.startsWith("false", i)) {
1870
+ i += 5;
1871
+ return { value: false, complete: true };
1872
+ }
1873
+ if (text.startsWith("null", i)) {
1874
+ i += 4;
1875
+ return { value: null, complete: true };
1876
+ }
1877
+ return null;
1878
+ };
1879
+ skipWhitespace();
1880
+ const first = text[i];
1881
+ if (first !== "{" && first !== "[") {
1882
+ return null;
1883
+ }
1884
+ const root = first === "{" ? {} : [];
1885
+ const stack = first === "{" ? [{ type: "object", value: root, state: "keyOrEnd" }] : [{ type: "array", value: root, state: "valueOrEnd" }];
1886
+ i += 1;
1887
+ while (stack.length > 0) {
1888
+ skipWhitespace();
1889
+ if (i >= len) {
1890
+ break;
1891
+ }
1892
+ const ctx = stack[stack.length - 1];
1893
+ if (!ctx) {
1894
+ break;
1895
+ }
1896
+ const ch = text[i] ?? "";
1897
+ if (ctx.type === "object") {
1898
+ if (ctx.state === "keyOrEnd") {
1899
+ if (ch === "}") {
1900
+ i += 1;
1901
+ stack.pop();
1902
+ continue;
1903
+ }
1904
+ if (ch === ",") {
1905
+ i += 1;
1906
+ continue;
1907
+ }
1908
+ if (ch !== '"') {
1909
+ break;
1910
+ }
1911
+ const key = parseString();
1912
+ if (!key) {
1913
+ break;
1914
+ }
1915
+ if (!key.complete) {
1916
+ break;
1917
+ }
1918
+ ctx.key = key.value;
1919
+ ctx.state = "colon";
1920
+ continue;
1921
+ }
1922
+ if (ctx.state === "colon") {
1923
+ if (ch === ":") {
1924
+ i += 1;
1925
+ ctx.state = "value";
1926
+ continue;
1927
+ }
1928
+ break;
1929
+ }
1930
+ if (ctx.state === "value") {
1931
+ if (ch === "}") {
1932
+ i += 1;
1933
+ ctx.key = void 0;
1934
+ stack.pop();
1935
+ continue;
1936
+ }
1937
+ if (ch === ",") {
1938
+ i += 1;
1939
+ ctx.key = void 0;
1940
+ ctx.state = "keyOrEnd";
1941
+ continue;
1942
+ }
1943
+ const key = ctx.key;
1944
+ if (!key) {
1945
+ break;
1946
+ }
1947
+ if (ch === "{" || ch === "[") {
1948
+ const container = ch === "{" ? {} : [];
1949
+ ctx.value[key] = container;
1950
+ ctx.key = void 0;
1951
+ ctx.state = "commaOrEnd";
1952
+ stack.push(
1953
+ ch === "{" ? { type: "object", value: container, state: "keyOrEnd" } : { type: "array", value: container, state: "valueOrEnd" }
1954
+ );
1955
+ i += 1;
1956
+ continue;
1957
+ }
1958
+ let primitive = null;
1959
+ if (ch === '"') {
1960
+ primitive = parseString();
1961
+ } else if (ch === "-" || ch >= "0" && ch <= "9") {
1962
+ primitive = parseNumber();
1963
+ } else {
1964
+ primitive = parseLiteral();
1965
+ }
1966
+ if (!primitive) {
1967
+ break;
1968
+ }
1969
+ ctx.value[key] = primitive.value;
1970
+ ctx.key = void 0;
1971
+ ctx.state = "commaOrEnd";
1972
+ if (!primitive.complete) {
1973
+ break;
1974
+ }
1975
+ continue;
1976
+ }
1977
+ if (ctx.state === "commaOrEnd") {
1978
+ if (ch === ",") {
1979
+ i += 1;
1980
+ ctx.state = "keyOrEnd";
1981
+ continue;
1982
+ }
1983
+ if (ch === "}") {
1984
+ i += 1;
1985
+ stack.pop();
1986
+ continue;
1987
+ }
1988
+ break;
1989
+ }
1990
+ } else {
1991
+ if (ctx.state === "valueOrEnd") {
1992
+ if (ch === "]") {
1993
+ i += 1;
1994
+ stack.pop();
1995
+ continue;
1996
+ }
1997
+ if (ch === ",") {
1998
+ i += 1;
1999
+ continue;
2000
+ }
2001
+ if (ch === "{" || ch === "[") {
2002
+ const container = ch === "{" ? {} : [];
2003
+ ctx.value.push(container);
2004
+ ctx.state = "commaOrEnd";
2005
+ stack.push(
2006
+ ch === "{" ? { type: "object", value: container, state: "keyOrEnd" } : { type: "array", value: container, state: "valueOrEnd" }
2007
+ );
2008
+ i += 1;
2009
+ continue;
2010
+ }
2011
+ let primitive = null;
2012
+ if (ch === '"') {
2013
+ primitive = parseString();
2014
+ } else if (ch === "-" || ch >= "0" && ch <= "9") {
2015
+ primitive = parseNumber();
2016
+ } else {
2017
+ primitive = parseLiteral();
2018
+ }
2019
+ if (!primitive) {
2020
+ break;
2021
+ }
2022
+ ctx.value.push(primitive.value);
2023
+ ctx.state = "commaOrEnd";
2024
+ if (!primitive.complete) {
2025
+ break;
2026
+ }
2027
+ continue;
2028
+ }
2029
+ if (ctx.state === "commaOrEnd") {
2030
+ if (ch === ",") {
2031
+ i += 1;
2032
+ ctx.state = "valueOrEnd";
2033
+ continue;
2034
+ }
2035
+ if (ch === "]") {
2036
+ i += 1;
2037
+ stack.pop();
2038
+ continue;
2039
+ }
2040
+ break;
2041
+ }
2042
+ }
1729
2043
  }
2044
+ return root;
2045
+ }
2046
+ function resolveTextContents(input) {
1730
2047
  const contents = [];
1731
- if (input.systemPrompt) {
2048
+ if (input.instructions) {
2049
+ const instructions = input.instructions.trim();
2050
+ if (instructions.length > 0) {
2051
+ contents.push({
2052
+ role: "system",
2053
+ parts: [{ type: "text", text: instructions }]
2054
+ });
2055
+ }
2056
+ }
2057
+ if (typeof input.input === "string") {
1732
2058
  contents.push({
1733
- role: "system",
1734
- parts: [{ type: "text", text: input.systemPrompt }]
2059
+ role: "user",
2060
+ parts: [{ type: "text", text: input.input }]
2061
+ });
2062
+ return contents;
2063
+ }
2064
+ for (const message of input.input) {
2065
+ const parts = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content;
2066
+ contents.push({
2067
+ role: message.role,
2068
+ parts: parts.map(
2069
+ (part) => part.type === "text" ? {
2070
+ type: "text",
2071
+ text: part.text,
2072
+ thought: "thought" in part && part.thought === true ? true : void 0
2073
+ } : { type: "inlineData", data: part.data, mimeType: part.mimeType }
2074
+ )
1735
2075
  });
1736
2076
  }
1737
- contents.push({
1738
- role: "user",
1739
- parts: [{ type: "text", text: input.prompt }]
1740
- });
1741
2077
  return contents;
1742
2078
  }
1743
2079
  function toOpenAiInput(contents) {
1744
2080
  const OPENAI_ROLE_FROM_LLM = {
1745
2081
  user: "user",
1746
- model: "assistant",
2082
+ assistant: "assistant",
1747
2083
  system: "system",
2084
+ developer: "developer",
1748
2085
  tool: "assistant"
1749
2086
  };
1750
2087
  return contents.map((content) => {
@@ -1783,7 +2120,7 @@ function toChatGptInput(contents) {
1783
2120
  const instructionsParts = [];
1784
2121
  const input = [];
1785
2122
  for (const content of contents) {
1786
- if (content.role === "system") {
2123
+ if (content.role === "system" || content.role === "developer") {
1787
2124
  for (const part of content.parts) {
1788
2125
  if (part.type === "text") {
1789
2126
  instructionsParts.push(part.text);
@@ -1791,7 +2128,7 @@ function toChatGptInput(contents) {
1791
2128
  }
1792
2129
  continue;
1793
2130
  }
1794
- const isAssistant = content.role === "model";
2131
+ const isAssistant = content.role === "assistant" || content.role === "tool";
1795
2132
  const parts = [];
1796
2133
  for (const part of content.parts) {
1797
2134
  if (part.type === "text") {
@@ -2063,8 +2400,8 @@ function parseOpenAiToolArguments(raw) {
2063
2400
  function formatZodIssues(issues) {
2064
2401
  const messages = [];
2065
2402
  for (const issue of issues) {
2066
- const path2 = issue.path.length > 0 ? issue.path.map(String).join(".") : "input";
2067
- messages.push(`${path2}: ${issue.message}`);
2403
+ const path5 = issue.path.length > 0 ? issue.path.map(String).join(".") : "input";
2404
+ messages.push(`${path5}: ${issue.message}`);
2068
2405
  }
2069
2406
  return messages.join("; ");
2070
2407
  }
@@ -2246,9 +2583,9 @@ function resolveGeminiThinkingConfig(modelId) {
2246
2583
  }
2247
2584
  function decodeInlineDataBuffer(base64) {
2248
2585
  try {
2249
- return Buffer2.from(base64, "base64");
2586
+ return Buffer3.from(base64, "base64");
2250
2587
  } catch {
2251
- return Buffer2.from(base64, "base64url");
2588
+ return Buffer3.from(base64, "base64url");
2252
2589
  }
2253
2590
  }
2254
2591
  function extractImages(content) {
@@ -2511,7 +2848,7 @@ async function runTextCall(params) {
2511
2848
  });
2512
2849
  }
2513
2850
  const mergedParts = mergeConsecutiveTextParts(responseParts);
2514
- const content = mergedParts.length > 0 ? { role: responseRole ?? "model", parts: mergedParts } : void 0;
2851
+ const content = mergedParts.length > 0 ? { role: responseRole ?? "assistant", parts: mergedParts } : void 0;
2515
2852
  const { text, thoughts } = extractTextByChannel(content);
2516
2853
  const costUsd = estimateCallCostUsd({
2517
2854
  modelId: modelVersion,
@@ -2561,8 +2898,7 @@ async function generateText(request) {
2561
2898
  }
2562
2899
  return await call.result;
2563
2900
  }
2564
- async function generateJson(request) {
2565
- const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
2901
+ function buildJsonSchemaConfig(request) {
2566
2902
  const schemaName = (request.openAiSchemaName ?? "llm-response").trim() || "llm-response";
2567
2903
  const providerInfo = resolveProvider(request.model);
2568
2904
  const isOpenAiVariant = providerInfo.provider === "openai" || providerInfo.provider === "chatgpt";
@@ -2574,31 +2910,143 @@ async function generateJson(request) {
2574
2910
  if (isOpenAiVariant && !isJsonSchemaObject(responseJsonSchema)) {
2575
2911
  throw new Error("OpenAI structured outputs require a JSON object schema at the root.");
2576
2912
  }
2577
- const openAiTextFormat = providerInfo.provider === "openai" ? {
2913
+ const openAiTextFormat = isOpenAiVariant ? {
2578
2914
  type: "json_schema",
2579
2915
  name: schemaName,
2580
2916
  strict: true,
2581
2917
  schema: normalizeOpenAiSchema(responseJsonSchema)
2582
2918
  } : void 0;
2919
+ return { providerInfo, responseJsonSchema, openAiTextFormat };
2920
+ }
2921
+ function streamJson(request) {
2922
+ const queue = createAsyncQueue();
2923
+ const abortController = new AbortController();
2924
+ const resolveAbortSignal = () => {
2925
+ if (!request.signal) {
2926
+ return abortController.signal;
2927
+ }
2928
+ if (request.signal.aborted) {
2929
+ abortController.abort(request.signal.reason);
2930
+ } else {
2931
+ request.signal.addEventListener(
2932
+ "abort",
2933
+ () => abortController.abort(request.signal?.reason),
2934
+ {
2935
+ once: true
2936
+ }
2937
+ );
2938
+ }
2939
+ return abortController.signal;
2940
+ };
2941
+ const result = (async () => {
2942
+ const signal = resolveAbortSignal();
2943
+ const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
2944
+ const { providerInfo, responseJsonSchema, openAiTextFormat } = buildJsonSchemaConfig(request);
2945
+ const streamMode = request.streamMode ?? "partial";
2946
+ const failures = [];
2947
+ let openAiTextFormatForAttempt = openAiTextFormat;
2948
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2949
+ let rawText = "";
2950
+ let lastPartial = "";
2951
+ try {
2952
+ const call = streamText({
2953
+ model: request.model,
2954
+ input: request.input,
2955
+ instructions: request.instructions,
2956
+ tools: request.tools,
2957
+ responseMimeType: request.responseMimeType ?? "application/json",
2958
+ responseJsonSchema,
2959
+ openAiReasoningEffort: request.openAiReasoningEffort,
2960
+ ...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
2961
+ signal
2962
+ });
2963
+ try {
2964
+ for await (const event of call.events) {
2965
+ queue.push(event);
2966
+ if (event.type === "delta" && event.channel === "response") {
2967
+ rawText += event.text;
2968
+ if (streamMode === "partial") {
2969
+ const partial = parsePartialJsonFromLlmText(rawText);
2970
+ if (partial !== null) {
2971
+ const serialized = JSON.stringify(partial);
2972
+ if (serialized !== lastPartial) {
2973
+ lastPartial = serialized;
2974
+ queue.push({
2975
+ type: "json",
2976
+ stage: "partial",
2977
+ value: partial
2978
+ });
2979
+ }
2980
+ }
2981
+ }
2982
+ }
2983
+ }
2984
+ } catch (streamError) {
2985
+ await call.result.catch(() => void 0);
2986
+ throw streamError;
2987
+ }
2988
+ const result2 = await call.result;
2989
+ rawText = rawText || result2.text;
2990
+ const cleanedText = normalizeJsonText(rawText);
2991
+ const repairedText = escapeNewlinesInStrings(cleanedText);
2992
+ const payload = JSON.parse(repairedText);
2993
+ const normalized = typeof request.normalizeJson === "function" ? request.normalizeJson(payload) : payload;
2994
+ const parsed = request.schema.parse(normalized);
2995
+ queue.push({ type: "json", stage: "final", value: parsed });
2996
+ queue.close();
2997
+ return { value: parsed, rawText, result: result2 };
2998
+ } catch (error) {
2999
+ const handled = error instanceof Error ? error : new Error(String(error));
3000
+ failures.push({ attempt, rawText, error: handled });
3001
+ if (providerInfo.provider === "chatgpt" && openAiTextFormatForAttempt) {
3002
+ openAiTextFormatForAttempt = void 0;
3003
+ }
3004
+ if (attempt >= maxAttempts) {
3005
+ throw new LlmJsonCallError(`LLM JSON call failed after ${attempt} attempt(s)`, failures);
3006
+ }
3007
+ }
3008
+ }
3009
+ throw new LlmJsonCallError("LLM JSON call failed", failures);
3010
+ })().catch((error) => {
3011
+ const err = error instanceof Error ? error : new Error(String(error));
3012
+ queue.fail(err);
3013
+ throw err;
3014
+ });
3015
+ return {
3016
+ events: queue.iterable,
3017
+ result,
3018
+ abort: () => abortController.abort()
3019
+ };
3020
+ }
3021
+ async function generateJson(request) {
3022
+ const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
3023
+ const { providerInfo, responseJsonSchema, openAiTextFormat } = buildJsonSchemaConfig(request);
3024
+ let openAiTextFormatForAttempt = openAiTextFormat;
2583
3025
  const failures = [];
2584
3026
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2585
3027
  let rawText = "";
2586
3028
  try {
2587
- const contents = resolveTextContents(request);
2588
3029
  const call = streamText({
2589
3030
  model: request.model,
2590
- contents,
3031
+ input: request.input,
3032
+ instructions: request.instructions,
2591
3033
  tools: request.tools,
2592
3034
  responseMimeType: request.responseMimeType ?? "application/json",
2593
3035
  responseJsonSchema,
2594
3036
  openAiReasoningEffort: request.openAiReasoningEffort,
2595
- ...openAiTextFormat ? { openAiTextFormat } : {},
3037
+ ...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
2596
3038
  signal: request.signal
2597
3039
  });
2598
- for await (const event of call.events) {
2599
- if (event.type === "delta" && event.channel === "response") {
2600
- rawText += event.text;
3040
+ try {
3041
+ for await (const event of call.events) {
3042
+ request.onEvent?.(event);
3043
+ if (event.type === "delta" && event.channel === "response") {
3044
+ rawText += event.text;
3045
+ }
2601
3046
  }
3047
+ } catch (streamError) {
3048
+ await call.result.catch(() => void 0);
3049
+ throw streamError;
2602
3050
  }
2603
3051
  const result = await call.result;
2604
3052
  rawText = rawText || result.text;
@@ -2611,6 +3059,9 @@ async function generateJson(request) {
2611
3059
  } catch (error) {
2612
3060
  const handled = error instanceof Error ? error : new Error(String(error));
2613
3061
  failures.push({ attempt, rawText, error: handled });
3062
+ if (providerInfo.provider === "chatgpt" && openAiTextFormatForAttempt) {
3063
+ openAiTextFormatForAttempt = void 0;
3064
+ }
2614
3065
  if (attempt >= maxAttempts) {
2615
3066
  throw new LlmJsonCallError(`LLM JSON call failed after ${attempt} attempt(s)`, failures);
2616
3067
  }
@@ -3196,7 +3647,7 @@ async function gradeGeneratedImage(params) {
3196
3647
  ];
3197
3648
  const { value } = await generateJson({
3198
3649
  model: params.model,
3199
- contents: [{ role: "user", parts }],
3650
+ input: [{ role: "user", content: parts }],
3200
3651
  schema: IMAGE_GRADE_SCHEMA
3201
3652
  });
3202
3653
  return value;
@@ -3273,7 +3724,7 @@ async function generateImages(request) {
3273
3724
  lines.push(`\\nPlease make all ${pending.length} remaining images.`);
3274
3725
  return [{ type: "text", text: lines.join("\\n") }];
3275
3726
  };
3276
- const contents = [{ role: "user", parts: buildInitialPromptParts() }];
3727
+ const inputMessages = [{ role: "user", content: buildInitialPromptParts() }];
3277
3728
  const orderedEntries = [...promptEntries];
3278
3729
  const resolvedImages = /* @__PURE__ */ new Map();
3279
3730
  const removeResolvedEntries = (resolved) => {
@@ -3293,7 +3744,7 @@ async function generateImages(request) {
3293
3744
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
3294
3745
  const result = await generateText({
3295
3746
  model: request.model,
3296
- contents,
3747
+ input: inputMessages,
3297
3748
  responseModalities: ["IMAGE", "TEXT"],
3298
3749
  imageAspectRatio: request.imageAspectRatio,
3299
3750
  imageSize: request.imageSize ?? "2K"
@@ -3340,8 +3791,11 @@ async function generateImages(request) {
3340
3791
  if (promptEntries.length === 0) {
3341
3792
  break;
3342
3793
  }
3343
- contents.push(result.content);
3344
- contents.push({ role: "user", parts: buildContinuationPromptParts(promptEntries) });
3794
+ inputMessages.push({
3795
+ role: "assistant",
3796
+ content: result.content.parts
3797
+ });
3798
+ inputMessages.push({ role: "user", content: buildContinuationPromptParts(promptEntries) });
3345
3799
  }
3346
3800
  const orderedImages = [];
3347
3801
  for (const entry of orderedEntries) {
@@ -3415,11 +3869,1667 @@ function appendMarkdownSourcesSection(value, sources) {
3415
3869
  ## Sources
3416
3870
  ${lines}`;
3417
3871
  }
3872
+
3873
+ // src/tools/filesystemTools.ts
3874
+ import path4 from "path";
3875
+ import { z as z5 } from "zod";
3876
+
3877
+ // src/tools/applyPatch.ts
3878
+ import path3 from "path";
3879
+ import { z as z4 } from "zod";
3880
+
3881
+ // src/tools/filesystem.ts
3882
+ import { promises as fs2 } from "fs";
3883
+ import path2 from "path";
3884
+ var InMemoryAgentFilesystem = class {
3885
+ #files = /* @__PURE__ */ new Map();
3886
+ #dirs = /* @__PURE__ */ new Map();
3887
+ #clock = 0;
3888
+ constructor(initialFiles = {}) {
3889
+ const root = path2.resolve("/");
3890
+ this.#dirs.set(root, { mtimeMs: this.#nextMtime() });
3891
+ for (const [filePath, content] of Object.entries(initialFiles)) {
3892
+ const absolutePath = path2.resolve(filePath);
3893
+ this.#ensureDirSync(path2.dirname(absolutePath));
3894
+ this.#files.set(absolutePath, {
3895
+ content,
3896
+ mtimeMs: this.#nextMtime()
3897
+ });
3898
+ }
3899
+ }
3900
+ async readTextFile(filePath) {
3901
+ const absolutePath = path2.resolve(filePath);
3902
+ const file = this.#files.get(absolutePath);
3903
+ if (!file) {
3904
+ throw createNoSuchFileError("open", absolutePath);
3905
+ }
3906
+ return file.content;
3907
+ }
3908
+ async writeTextFile(filePath, content) {
3909
+ const absolutePath = path2.resolve(filePath);
3910
+ const parentPath = path2.dirname(absolutePath);
3911
+ if (!this.#dirs.has(parentPath)) {
3912
+ throw createNoSuchFileError("open", parentPath);
3913
+ }
3914
+ this.#files.set(absolutePath, { content, mtimeMs: this.#nextMtime() });
3915
+ }
3916
+ async deleteFile(filePath) {
3917
+ const absolutePath = path2.resolve(filePath);
3918
+ if (!this.#files.delete(absolutePath)) {
3919
+ throw createNoSuchFileError("unlink", absolutePath);
3920
+ }
3921
+ }
3922
+ async ensureDir(directoryPath) {
3923
+ this.#ensureDirSync(path2.resolve(directoryPath));
3924
+ }
3925
+ async readDir(directoryPath) {
3926
+ const absolutePath = path2.resolve(directoryPath);
3927
+ const directory = this.#dirs.get(absolutePath);
3928
+ if (!directory) {
3929
+ throw createNoSuchFileError("scandir", absolutePath);
3930
+ }
3931
+ const entries = [];
3932
+ const seenNames = /* @__PURE__ */ new Set();
3933
+ for (const [dirPath, dirRecord] of this.#dirs.entries()) {
3934
+ if (dirPath === absolutePath) {
3935
+ continue;
3936
+ }
3937
+ if (path2.dirname(dirPath) !== absolutePath) {
3938
+ continue;
3939
+ }
3940
+ const name = path2.basename(dirPath);
3941
+ if (seenNames.has(name)) {
3942
+ continue;
3943
+ }
3944
+ seenNames.add(name);
3945
+ entries.push({
3946
+ name,
3947
+ path: dirPath,
3948
+ kind: "directory",
3949
+ mtimeMs: dirRecord.mtimeMs
3950
+ });
3951
+ }
3952
+ for (const [filePath, fileRecord] of this.#files.entries()) {
3953
+ if (path2.dirname(filePath) !== absolutePath) {
3954
+ continue;
3955
+ }
3956
+ const name = path2.basename(filePath);
3957
+ if (seenNames.has(name)) {
3958
+ continue;
3959
+ }
3960
+ seenNames.add(name);
3961
+ entries.push({
3962
+ name,
3963
+ path: filePath,
3964
+ kind: "file",
3965
+ mtimeMs: fileRecord.mtimeMs
3966
+ });
3967
+ }
3968
+ entries.sort((left, right) => left.name.localeCompare(right.name));
3969
+ return entries;
3970
+ }
3971
+ async stat(entryPath) {
3972
+ const absolutePath = path2.resolve(entryPath);
3973
+ const file = this.#files.get(absolutePath);
3974
+ if (file) {
3975
+ return { kind: "file", mtimeMs: file.mtimeMs };
3976
+ }
3977
+ const directory = this.#dirs.get(absolutePath);
3978
+ if (directory) {
3979
+ return { kind: "directory", mtimeMs: directory.mtimeMs };
3980
+ }
3981
+ throw createNoSuchFileError("stat", absolutePath);
3982
+ }
3983
+ snapshot() {
3984
+ const entries = [...this.#files.entries()].sort(([left], [right]) => left.localeCompare(right));
3985
+ return Object.fromEntries(entries.map(([filePath, record]) => [filePath, record.content]));
3986
+ }
3987
+ #ensureDirSync(directoryPath) {
3988
+ const absolutePath = path2.resolve(directoryPath);
3989
+ const parts = [];
3990
+ let cursor = absolutePath;
3991
+ for (; ; ) {
3992
+ if (this.#dirs.has(cursor)) {
3993
+ break;
3994
+ }
3995
+ parts.push(cursor);
3996
+ const parent = path2.dirname(cursor);
3997
+ if (parent === cursor) {
3998
+ break;
3999
+ }
4000
+ cursor = parent;
4001
+ }
4002
+ for (let index = parts.length - 1; index >= 0; index -= 1) {
4003
+ const nextDir = parts[index];
4004
+ if (nextDir === void 0) {
4005
+ continue;
4006
+ }
4007
+ if (!this.#dirs.has(nextDir)) {
4008
+ this.#dirs.set(nextDir, { mtimeMs: this.#nextMtime() });
4009
+ }
4010
+ }
4011
+ }
4012
+ #nextMtime() {
4013
+ this.#clock += 1;
4014
+ return this.#clock;
4015
+ }
4016
+ };
4017
+ function createNodeAgentFilesystem() {
4018
+ return {
4019
+ readTextFile: async (filePath) => fs2.readFile(filePath, "utf8"),
4020
+ writeTextFile: async (filePath, content) => fs2.writeFile(filePath, content, "utf8"),
4021
+ deleteFile: async (filePath) => fs2.unlink(filePath),
4022
+ ensureDir: async (directoryPath) => {
4023
+ await fs2.mkdir(directoryPath, { recursive: true });
4024
+ },
4025
+ readDir: async (directoryPath) => {
4026
+ const entries = await fs2.readdir(directoryPath, { withFileTypes: true });
4027
+ const result = [];
4028
+ for (const entry of entries) {
4029
+ const entryPath = path2.resolve(directoryPath, entry.name);
4030
+ const stats = await fs2.lstat(entryPath);
4031
+ result.push({
4032
+ name: entry.name,
4033
+ path: entryPath,
4034
+ kind: statsToKind(stats),
4035
+ mtimeMs: stats.mtimeMs
4036
+ });
4037
+ }
4038
+ return result;
4039
+ },
4040
+ stat: async (entryPath) => {
4041
+ const stats = await fs2.lstat(entryPath);
4042
+ return {
4043
+ kind: statsToKind(stats),
4044
+ mtimeMs: stats.mtimeMs
4045
+ };
4046
+ }
4047
+ };
4048
+ }
4049
+ function createInMemoryAgentFilesystem(initialFiles = {}) {
4050
+ return new InMemoryAgentFilesystem(initialFiles);
4051
+ }
4052
+ function statsToKind(stats) {
4053
+ if (stats.isSymbolicLink()) {
4054
+ return "symlink";
4055
+ }
4056
+ if (stats.isDirectory()) {
4057
+ return "directory";
4058
+ }
4059
+ if (stats.isFile()) {
4060
+ return "file";
4061
+ }
4062
+ return "other";
4063
+ }
4064
+ function createNoSuchFileError(syscall, filePath) {
4065
+ const error = new Error(
4066
+ `ENOENT: no such file or directory, ${syscall} '${filePath}'`
4067
+ );
4068
+ error.code = "ENOENT";
4069
+ error.syscall = syscall;
4070
+ error.path = filePath;
4071
+ return error;
4072
+ }
4073
+
4074
+ // src/tools/applyPatch.ts
4075
+ var BEGIN_PATCH_LINE = "*** Begin Patch";
4076
+ var END_PATCH_LINE = "*** End Patch";
4077
+ var ADD_FILE_PREFIX = "*** Add File: ";
4078
+ var DELETE_FILE_PREFIX = "*** Delete File: ";
4079
+ var UPDATE_FILE_PREFIX = "*** Update File: ";
4080
+ var MOVE_TO_PREFIX = "*** Move to: ";
4081
+ var END_OF_FILE_LINE = "*** End of File";
4082
+ var DEFAULT_MAX_PATCH_BYTES = 1024 * 1024;
4083
+ var applyPatchToolInputSchema = z4.object({
4084
+ input: z4.string().min(1).describe("The entire apply_patch payload, including Begin/End markers.")
4085
+ });
4086
+ function createApplyPatchTool(options = {}) {
4087
+ return tool({
4088
+ description: options.description ?? "Apply edits using a Codex-style apply_patch payload with Begin/End markers.",
4089
+ inputSchema: applyPatchToolInputSchema,
4090
+ execute: async ({ input }) => applyPatch({
4091
+ patch: input,
4092
+ cwd: options.cwd,
4093
+ fs: options.fs,
4094
+ allowOutsideCwd: options.allowOutsideCwd,
4095
+ checkAccess: options.checkAccess,
4096
+ maxPatchBytes: options.maxPatchBytes
4097
+ })
4098
+ });
4099
+ }
4100
+ async function applyPatch(request) {
4101
+ const cwd = path3.resolve(request.cwd ?? process.cwd());
4102
+ const adapter = request.fs ?? createNodeAgentFilesystem();
4103
+ const allowOutsideCwd = request.allowOutsideCwd === true;
4104
+ const patchBytes = Buffer.byteLength(request.patch, "utf8");
4105
+ const maxPatchBytes = request.maxPatchBytes ?? DEFAULT_MAX_PATCH_BYTES;
4106
+ if (patchBytes > maxPatchBytes) {
4107
+ throw new Error(
4108
+ `apply_patch failed: patch too large (${patchBytes} bytes > ${maxPatchBytes} bytes)`
4109
+ );
4110
+ }
4111
+ const parsed = parsePatchDocument(normalizePatchText(request.patch));
4112
+ const added = [];
4113
+ const modified = [];
4114
+ const deleted = [];
4115
+ for (const operation of parsed.operations) {
4116
+ if (operation.type === "add") {
4117
+ const absolutePath2 = resolvePatchPath(operation.path, cwd, allowOutsideCwd);
4118
+ await runAccessHook(request.checkAccess, {
4119
+ cwd,
4120
+ kind: "add",
4121
+ path: absolutePath2
4122
+ });
4123
+ await adapter.ensureDir(path3.dirname(absolutePath2));
4124
+ await adapter.writeTextFile(absolutePath2, operation.content);
4125
+ added.push(toDisplayPath(absolutePath2, cwd));
4126
+ continue;
4127
+ }
4128
+ if (operation.type === "delete") {
4129
+ const absolutePath2 = resolvePatchPath(operation.path, cwd, allowOutsideCwd);
4130
+ await runAccessHook(request.checkAccess, {
4131
+ cwd,
4132
+ kind: "delete",
4133
+ path: absolutePath2
4134
+ });
4135
+ await adapter.readTextFile(absolutePath2);
4136
+ await adapter.deleteFile(absolutePath2);
4137
+ deleted.push(toDisplayPath(absolutePath2, cwd));
4138
+ continue;
4139
+ }
4140
+ const absolutePath = resolvePatchPath(operation.path, cwd, allowOutsideCwd);
4141
+ await runAccessHook(request.checkAccess, {
4142
+ cwd,
4143
+ kind: "update",
4144
+ path: absolutePath
4145
+ });
4146
+ const current = await adapter.readTextFile(absolutePath);
4147
+ const next = deriveUpdatedContent(current, operation.chunks, toDisplayPath(absolutePath, cwd));
4148
+ if (operation.movePath) {
4149
+ const destinationPath = resolvePatchPath(operation.movePath, cwd, allowOutsideCwd);
4150
+ await runAccessHook(request.checkAccess, {
4151
+ cwd,
4152
+ kind: "move",
4153
+ path: destinationPath,
4154
+ fromPath: absolutePath,
4155
+ toPath: destinationPath
4156
+ });
4157
+ await adapter.ensureDir(path3.dirname(destinationPath));
4158
+ await adapter.writeTextFile(destinationPath, next);
4159
+ await adapter.deleteFile(absolutePath);
4160
+ modified.push(toDisplayPath(destinationPath, cwd));
4161
+ continue;
4162
+ }
4163
+ await adapter.writeTextFile(absolutePath, next);
4164
+ modified.push(toDisplayPath(absolutePath, cwd));
4165
+ }
4166
+ return {
4167
+ success: true,
4168
+ summary: formatSummary(added, modified, deleted),
4169
+ added,
4170
+ modified,
4171
+ deleted
4172
+ };
4173
+ }
4174
+ async function runAccessHook(hook, context) {
4175
+ if (!hook) {
4176
+ return;
4177
+ }
4178
+ await hook(context);
4179
+ }
4180
+ function normalizePatchText(raw) {
4181
+ return raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
4182
+ }
4183
+ function resolvePatchPath(rawPath, cwd, allowOutsideCwd) {
4184
+ const trimmed = rawPath.trim();
4185
+ if (trimmed.length === 0) {
4186
+ throw new Error("apply_patch failed: empty file path");
4187
+ }
4188
+ const absolutePath = path3.isAbsolute(trimmed) ? path3.resolve(trimmed) : path3.resolve(cwd, trimmed);
4189
+ if (!allowOutsideCwd && !isPathInsideCwd(absolutePath, cwd)) {
4190
+ throw new Error(`apply_patch failed: path "${trimmed}" resolves outside cwd "${cwd}"`);
4191
+ }
4192
+ return absolutePath;
4193
+ }
4194
+ function isPathInsideCwd(candidatePath, cwd) {
4195
+ const relative = path3.relative(cwd, candidatePath);
4196
+ return relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
4197
+ }
4198
+ function toDisplayPath(absolutePath, cwd) {
4199
+ const relative = path3.relative(cwd, absolutePath);
4200
+ if (relative === "") {
4201
+ return ".";
4202
+ }
4203
+ if (!relative.startsWith("..") && !path3.isAbsolute(relative)) {
4204
+ return relative;
4205
+ }
4206
+ return absolutePath;
4207
+ }
4208
+ function parsePatchDocument(patch) {
4209
+ const lines = patch.split("\n");
4210
+ if (lines.at(-1) === "") {
4211
+ lines.pop();
4212
+ }
4213
+ if (lines.length < 2) {
4214
+ throw new Error("apply_patch failed: patch must contain Begin/End markers");
4215
+ }
4216
+ if (lines[0] !== BEGIN_PATCH_LINE) {
4217
+ throw new Error(`apply_patch failed: missing "${BEGIN_PATCH_LINE}" header`);
4218
+ }
4219
+ if (lines[lines.length - 1] !== END_PATCH_LINE) {
4220
+ throw new Error(`apply_patch failed: missing "${END_PATCH_LINE}" footer`);
4221
+ }
4222
+ const body = lines.slice(1, -1);
4223
+ if (body.length === 0) {
4224
+ throw new Error("apply_patch failed: patch body is empty");
4225
+ }
4226
+ const operations = [];
4227
+ let index = 0;
4228
+ while (index < body.length) {
4229
+ const line = body[index];
4230
+ if (!line) {
4231
+ throw new Error("apply_patch failed: unexpected empty line between file sections");
4232
+ }
4233
+ if (line.startsWith(ADD_FILE_PREFIX)) {
4234
+ const filePath = extractPatchPath(line, ADD_FILE_PREFIX);
4235
+ index += 1;
4236
+ const contentLines = [];
4237
+ while (index < body.length) {
4238
+ const contentLine = body[index];
4239
+ if (contentLine === void 0 || isPatchSectionHeader(contentLine)) {
4240
+ break;
4241
+ }
4242
+ if (!contentLine.startsWith("+")) {
4243
+ throw new Error(`apply_patch failed: invalid add-file line "${contentLine}"`);
4244
+ }
4245
+ contentLines.push(contentLine.slice(1));
4246
+ index += 1;
4247
+ }
4248
+ if (contentLines.length === 0) {
4249
+ throw new Error(`apply_patch failed: add-file section for "${filePath}" is empty`);
4250
+ }
4251
+ operations.push({
4252
+ type: "add",
4253
+ path: filePath,
4254
+ content: `${contentLines.join("\n")}
4255
+ `
4256
+ });
4257
+ continue;
4258
+ }
4259
+ if (line.startsWith(DELETE_FILE_PREFIX)) {
4260
+ operations.push({
4261
+ type: "delete",
4262
+ path: extractPatchPath(line, DELETE_FILE_PREFIX)
4263
+ });
4264
+ index += 1;
4265
+ continue;
4266
+ }
4267
+ if (line.startsWith(UPDATE_FILE_PREFIX)) {
4268
+ const filePath = extractPatchPath(line, UPDATE_FILE_PREFIX);
4269
+ index += 1;
4270
+ let movePath;
4271
+ const moveHeader = body[index];
4272
+ if (moveHeader?.startsWith(MOVE_TO_PREFIX)) {
4273
+ movePath = extractPatchPath(moveHeader, MOVE_TO_PREFIX);
4274
+ index += 1;
4275
+ }
4276
+ const chunks = [];
4277
+ while (index < body.length) {
4278
+ const hunkHeader = body[index];
4279
+ if (hunkHeader === void 0 || isPatchSectionHeader(hunkHeader)) {
4280
+ break;
4281
+ }
4282
+ if (!(hunkHeader === "@@" || hunkHeader.startsWith("@@ "))) {
4283
+ throw new Error(
4284
+ `apply_patch failed: expected hunk marker in "${filePath}", got "${hunkHeader}"`
4285
+ );
4286
+ }
4287
+ const contextSelector = hunkHeader.length > 2 ? hunkHeader.slice(3) : void 0;
4288
+ index += 1;
4289
+ const oldLines = [];
4290
+ const newLines = [];
4291
+ let sawBodyLine = false;
4292
+ let sawChangeLine = false;
4293
+ let isEndOfFile = false;
4294
+ while (index < body.length) {
4295
+ const chunkLine = body[index];
4296
+ if (chunkLine === void 0) {
4297
+ break;
4298
+ }
4299
+ if (chunkLine === "@@" || chunkLine.startsWith("@@ ") || isPatchSectionHeader(chunkLine)) {
4300
+ break;
4301
+ }
4302
+ if (chunkLine === END_OF_FILE_LINE) {
4303
+ isEndOfFile = true;
4304
+ index += 1;
4305
+ break;
4306
+ }
4307
+ if (chunkLine.length === 0) {
4308
+ throw new Error(`apply_patch failed: invalid empty hunk line in "${filePath}"`);
4309
+ }
4310
+ const prefix = chunkLine[0];
4311
+ const content = chunkLine.slice(1);
4312
+ if (prefix === " ") {
4313
+ oldLines.push(content);
4314
+ newLines.push(content);
4315
+ } else if (prefix === "-") {
4316
+ oldLines.push(content);
4317
+ sawChangeLine = true;
4318
+ } else if (prefix === "+") {
4319
+ newLines.push(content);
4320
+ sawChangeLine = true;
4321
+ } else {
4322
+ throw new Error(
4323
+ `apply_patch failed: unsupported hunk prefix "${prefix}" in "${chunkLine}"`
4324
+ );
4325
+ }
4326
+ sawBodyLine = true;
4327
+ index += 1;
4328
+ }
4329
+ if (!sawBodyLine) {
4330
+ throw new Error(`apply_patch failed: empty hunk body in "${filePath}"`);
4331
+ }
4332
+ if (!sawChangeLine) {
4333
+ throw new Error(
4334
+ `apply_patch failed: hunk in "${filePath}" must include '+' or '-' lines`
4335
+ );
4336
+ }
4337
+ chunks.push({
4338
+ contextSelector,
4339
+ oldLines,
4340
+ newLines,
4341
+ isEndOfFile
4342
+ });
4343
+ }
4344
+ if (chunks.length === 0) {
4345
+ throw new Error(`apply_patch failed: update section for "${filePath}" has no hunks`);
4346
+ }
4347
+ operations.push({
4348
+ type: "update",
4349
+ path: filePath,
4350
+ movePath,
4351
+ chunks
4352
+ });
4353
+ continue;
4354
+ }
4355
+ throw new Error(`apply_patch failed: unrecognized section header "${line}"`);
4356
+ }
4357
+ return { operations };
4358
+ }
4359
+ function extractPatchPath(line, prefix) {
4360
+ const value = line.slice(prefix.length).trim();
4361
+ if (value.length === 0) {
4362
+ throw new Error(`apply_patch failed: missing file path in "${line}"`);
4363
+ }
4364
+ return value;
4365
+ }
4366
+ function isPatchSectionHeader(line) {
4367
+ return line.startsWith(ADD_FILE_PREFIX) || line.startsWith(DELETE_FILE_PREFIX) || line.startsWith(UPDATE_FILE_PREFIX);
4368
+ }
4369
+ function deriveUpdatedContent(originalContent, chunks, displayPath) {
4370
+ const originalLines = splitFileContentIntoLines(originalContent);
4371
+ const replacements = [];
4372
+ let lineIndex = 0;
4373
+ for (const chunk of chunks) {
4374
+ if (chunk.contextSelector !== void 0) {
4375
+ const contextIndex = seekSequence(originalLines, [chunk.contextSelector], lineIndex, false);
4376
+ if (contextIndex === null) {
4377
+ throw new Error(
4378
+ `apply_patch failed: unable to locate context "${chunk.contextSelector}" in ${displayPath}`
4379
+ );
4380
+ }
4381
+ lineIndex = contextIndex + 1;
4382
+ }
4383
+ if (chunk.oldLines.length === 0) {
4384
+ replacements.push({
4385
+ startIndex: originalLines.length,
4386
+ oldLength: 0,
4387
+ newLines: [...chunk.newLines]
4388
+ });
4389
+ continue;
4390
+ }
4391
+ let oldLines = [...chunk.oldLines];
4392
+ let newLines = [...chunk.newLines];
4393
+ let startIndex = seekSequence(originalLines, oldLines, lineIndex, chunk.isEndOfFile);
4394
+ if (startIndex === null && oldLines.at(-1) === "") {
4395
+ oldLines = oldLines.slice(0, -1);
4396
+ if (newLines.at(-1) === "") {
4397
+ newLines = newLines.slice(0, -1);
4398
+ }
4399
+ startIndex = seekSequence(originalLines, oldLines, lineIndex, chunk.isEndOfFile);
4400
+ }
4401
+ if (startIndex === null) {
4402
+ throw new Error(
4403
+ `apply_patch failed: failed to match hunk in ${displayPath}:
4404
+ ${chunk.oldLines.join("\n")}`
4405
+ );
4406
+ }
4407
+ replacements.push({
4408
+ startIndex,
4409
+ oldLength: oldLines.length,
4410
+ newLines
4411
+ });
4412
+ lineIndex = startIndex + oldLines.length;
4413
+ }
4414
+ replacements.sort((left, right) => left.startIndex - right.startIndex);
4415
+ const nextLines = applyReplacements(originalLines, replacements);
4416
+ if (nextLines.length > 0 && nextLines[nextLines.length - 1] !== "") {
4417
+ nextLines.push("");
4418
+ }
4419
+ return nextLines.join("\n");
4420
+ }
4421
+ function splitFileContentIntoLines(content) {
4422
+ const lines = content.split("\n");
4423
+ if (lines.at(-1) === "") {
4424
+ lines.pop();
4425
+ }
4426
+ return lines;
4427
+ }
4428
+ function seekSequence(sourceLines, targetLines, startIndex, isEndOfFile) {
4429
+ if (targetLines.length === 0) {
4430
+ return Math.min(Math.max(startIndex, 0), sourceLines.length);
4431
+ }
4432
+ const from = Math.max(startIndex, 0);
4433
+ const maxStart = sourceLines.length - targetLines.length;
4434
+ if (maxStart < from) {
4435
+ return null;
4436
+ }
4437
+ const matchesAt = (candidateIndex) => {
4438
+ for (let offset = 0; offset < targetLines.length; offset += 1) {
4439
+ if (sourceLines[candidateIndex + offset] !== targetLines[offset]) {
4440
+ return false;
4441
+ }
4442
+ }
4443
+ return true;
4444
+ };
4445
+ if (isEndOfFile) {
4446
+ return matchesAt(maxStart) ? maxStart : null;
4447
+ }
4448
+ for (let candidate = from; candidate <= maxStart; candidate += 1) {
4449
+ if (matchesAt(candidate)) {
4450
+ return candidate;
4451
+ }
4452
+ }
4453
+ return null;
4454
+ }
4455
+ function applyReplacements(lines, replacements) {
4456
+ const result = [...lines];
4457
+ for (let index = replacements.length - 1; index >= 0; index -= 1) {
4458
+ const replacement = replacements[index];
4459
+ if (replacement === void 0) {
4460
+ continue;
4461
+ }
4462
+ result.splice(replacement.startIndex, replacement.oldLength, ...replacement.newLines);
4463
+ }
4464
+ return result;
4465
+ }
4466
+ function formatSummary(added, modified, deleted) {
4467
+ const lines = ["Success. Updated the following files:"];
4468
+ for (const filePath of added) {
4469
+ lines.push(`A ${filePath}`);
4470
+ }
4471
+ for (const filePath of modified) {
4472
+ lines.push(`M ${filePath}`);
4473
+ }
4474
+ for (const filePath of deleted) {
4475
+ lines.push(`D ${filePath}`);
4476
+ }
4477
+ return `${lines.join("\n")}
4478
+ `;
4479
+ }
4480
+
4481
+ // src/tools/filesystemTools.ts
4482
+ var DEFAULT_READ_FILE_LINE_LIMIT = 2e3;
4483
+ var DEFAULT_LIST_DIR_LIMIT = 25;
4484
+ var DEFAULT_LIST_DIR_DEPTH = 2;
4485
+ var DEFAULT_GREP_LIMIT = 100;
4486
+ var MAX_GREP_LIMIT = 2e3;
4487
+ var DEFAULT_MAX_LINE_LENGTH = 500;
4488
+ var DEFAULT_GREP_MAX_SCANNED_FILES = 2e4;
4489
+ var DEFAULT_TAB_WIDTH = 4;
4490
+ var codexReadFileInputSchema = z5.object({
4491
+ file_path: z5.string().min(1).describe("Absolute path to the file"),
4492
+ offset: z5.number().int().min(1).optional().describe("The line number to start reading from. Must be 1 or greater."),
4493
+ limit: z5.number().int().min(1).optional().describe("The maximum number of lines to return."),
4494
+ mode: z5.enum(["slice", "indentation"]).optional().describe('Optional mode selector: "slice" (default) or "indentation".'),
4495
+ indentation: z5.object({
4496
+ anchor_line: z5.number().int().min(1).optional(),
4497
+ max_levels: z5.number().int().min(0).optional(),
4498
+ include_siblings: z5.boolean().optional(),
4499
+ include_header: z5.boolean().optional(),
4500
+ max_lines: z5.number().int().min(1).optional()
4501
+ }).optional()
4502
+ });
4503
+ var codexListDirInputSchema = z5.object({
4504
+ dir_path: z5.string().min(1).describe("Absolute path to the directory to list."),
4505
+ offset: z5.number().int().min(1).optional().describe("The entry number to start listing from. Must be 1 or greater."),
4506
+ limit: z5.number().int().min(1).optional().describe("The maximum number of entries to return."),
4507
+ depth: z5.number().int().min(1).optional().describe("The maximum directory depth to traverse. Must be 1 or greater.")
4508
+ });
4509
+ var codexGrepFilesInputSchema = z5.object({
4510
+ pattern: z5.string().min(1).describe("Regular expression pattern to search for."),
4511
+ include: z5.string().optional().describe('Optional glob limiting searched files (for example "*.rs").'),
4512
+ path: z5.string().optional().describe("Directory or file path to search. Defaults to cwd."),
4513
+ limit: z5.number().int().min(1).optional().describe("Maximum number of file paths to return (defaults to 100).")
4514
+ });
4515
+ var applyPatchInputSchema = z5.object({
4516
+ input: z5.string().min(1)
4517
+ });
4518
+ var geminiReadFileInputSchema = z5.object({
4519
+ file_path: z5.string().min(1),
4520
+ offset: z5.number().int().min(0).optional(),
4521
+ limit: z5.number().int().min(1).optional()
4522
+ });
4523
+ var geminiWriteFileInputSchema = z5.object({
4524
+ file_path: z5.string().min(1),
4525
+ content: z5.string()
4526
+ });
4527
+ var geminiReplaceInputSchema = z5.object({
4528
+ file_path: z5.string().min(1),
4529
+ instruction: z5.string().min(1),
4530
+ old_string: z5.string(),
4531
+ new_string: z5.string(),
4532
+ expected_replacements: z5.number().int().min(1).optional()
4533
+ });
4534
+ var geminiListDirectoryInputSchema = z5.object({
4535
+ dir_path: z5.string().min(1),
4536
+ ignore: z5.array(z5.string()).optional(),
4537
+ file_filtering_options: z5.object({
4538
+ respect_git_ignore: z5.boolean().optional(),
4539
+ respect_gemini_ignore: z5.boolean().optional()
4540
+ }).optional()
4541
+ });
4542
+ var geminiGrepSearchInputSchema = z5.object({
4543
+ pattern: z5.string().min(1),
4544
+ dir_path: z5.string().optional(),
4545
+ include: z5.string().optional(),
4546
+ exclude_pattern: z5.string().optional(),
4547
+ names_only: z5.boolean().optional(),
4548
+ max_matches_per_file: z5.number().int().min(1).optional(),
4549
+ total_max_matches: z5.number().int().min(1).optional()
4550
+ });
4551
+ var geminiGlobInputSchema = z5.object({
4552
+ pattern: z5.string().min(1),
4553
+ dir_path: z5.string().optional(),
4554
+ case_sensitive: z5.boolean().optional(),
4555
+ respect_git_ignore: z5.boolean().optional(),
4556
+ respect_gemini_ignore: z5.boolean().optional()
4557
+ });
4558
+ function resolveFilesystemToolProfile(model, profile = "auto") {
4559
+ if (profile !== "auto") {
4560
+ return profile;
4561
+ }
4562
+ if (isCodexModel(model)) {
4563
+ return "codex";
4564
+ }
4565
+ if (isGeminiModel(model)) {
4566
+ return "gemini";
4567
+ }
4568
+ return "model-agnostic";
4569
+ }
4570
+ function createFilesystemToolSetForModel(model, profileOrOptions = "auto", maybeOptions) {
4571
+ if (typeof profileOrOptions === "string") {
4572
+ const resolvedProfile2 = resolveFilesystemToolProfile(model, profileOrOptions);
4573
+ if (resolvedProfile2 === "codex") {
4574
+ return createCodexFilesystemToolSet(maybeOptions);
4575
+ }
4576
+ if (resolvedProfile2 === "gemini") {
4577
+ return createGeminiFilesystemToolSet(maybeOptions);
4578
+ }
4579
+ return createModelAgnosticFilesystemToolSet(maybeOptions);
4580
+ }
4581
+ const resolvedProfile = resolveFilesystemToolProfile(model, "auto");
4582
+ if (resolvedProfile === "codex") {
4583
+ return createCodexFilesystemToolSet(profileOrOptions);
4584
+ }
4585
+ if (resolvedProfile === "gemini") {
4586
+ return createGeminiFilesystemToolSet(profileOrOptions);
4587
+ }
4588
+ return createModelAgnosticFilesystemToolSet(profileOrOptions);
4589
+ }
4590
+ function createCodexFilesystemToolSet(options = {}) {
4591
+ return {
4592
+ apply_patch: createCodexApplyPatchTool(options),
4593
+ read_file: createCodexReadFileTool(options),
4594
+ list_dir: createListDirTool(options),
4595
+ grep_files: createGrepFilesTool(options)
4596
+ };
4597
+ }
4598
+ function createGeminiFilesystemToolSet(options = {}) {
4599
+ return {
4600
+ read_file: createReadFileTool(options),
4601
+ write_file: createWriteFileTool(options),
4602
+ replace: createReplaceTool(options),
4603
+ list_directory: createListDirectoryTool(options),
4604
+ grep_search: createGrepSearchTool(options),
4605
+ glob: createGlobTool(options)
4606
+ };
4607
+ }
4608
+ function createModelAgnosticFilesystemToolSet(options = {}) {
4609
+ return createGeminiFilesystemToolSet(options);
4610
+ }
4611
+ function createCodexApplyPatchTool(options = {}) {
4612
+ return tool({
4613
+ description: "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.",
4614
+ inputSchema: applyPatchInputSchema,
4615
+ execute: async ({ input }) => {
4616
+ const runtime = resolveRuntime(options);
4617
+ const result = await applyPatch({
4618
+ patch: input,
4619
+ cwd: runtime.cwd,
4620
+ fs: runtime.filesystem,
4621
+ allowOutsideCwd: runtime.allowOutsideCwd,
4622
+ checkAccess: runtime.checkAccess ? async (context) => {
4623
+ await runtime.checkAccess?.({
4624
+ cwd: runtime.cwd,
4625
+ tool: "apply_patch",
4626
+ action: mapApplyPatchAction(context.kind),
4627
+ path: context.path,
4628
+ fromPath: context.fromPath,
4629
+ toPath: context.toPath
4630
+ });
4631
+ } : void 0,
4632
+ maxPatchBytes: options.applyPatch?.maxPatchBytes
4633
+ });
4634
+ return result.summary;
4635
+ }
4636
+ });
4637
+ }
4638
+ function createCodexReadFileTool(options = {}) {
4639
+ return tool({
4640
+ description: "Reads a local file with 1-indexed line numbers, supporting slice and indentation-aware block modes.",
4641
+ inputSchema: codexReadFileInputSchema,
4642
+ execute: async (input) => readFileCodex(input, options)
4643
+ });
4644
+ }
4645
+ function createListDirTool(options = {}) {
4646
+ return tool({
4647
+ description: "Lists entries in a local directory with 1-indexed entry numbers and simple type labels.",
4648
+ inputSchema: codexListDirInputSchema,
4649
+ execute: async (input) => listDirectoryCodex(input, options)
4650
+ });
4651
+ }
4652
+ function createGrepFilesTool(options = {}) {
4653
+ return tool({
4654
+ description: "Finds files whose contents match the pattern and lists them by modification time.",
4655
+ inputSchema: codexGrepFilesInputSchema,
4656
+ execute: async (input) => grepFilesCodex(input, options)
4657
+ });
4658
+ }
4659
+ function createReadFileTool(options = {}) {
4660
+ return tool({
4661
+ description: "Reads and returns content of a specified file.",
4662
+ inputSchema: geminiReadFileInputSchema,
4663
+ execute: async (input) => readFileGemini(input, options)
4664
+ });
4665
+ }
4666
+ function createWriteFileTool(options = {}) {
4667
+ return tool({
4668
+ description: "Writes content to a specified file in the local filesystem.",
4669
+ inputSchema: geminiWriteFileInputSchema,
4670
+ execute: async (input) => writeFileGemini(input, options)
4671
+ });
4672
+ }
4673
+ function createReplaceTool(options = {}) {
4674
+ return tool({
4675
+ description: "Replaces exact literal text within a file.",
4676
+ inputSchema: geminiReplaceInputSchema,
4677
+ execute: async (input) => replaceFileContentGemini(input, options)
4678
+ });
4679
+ }
4680
+ function createListDirectoryTool(options = {}) {
4681
+ return tool({
4682
+ description: "Lists files and subdirectories directly within a specified directory path.",
4683
+ inputSchema: geminiListDirectoryInputSchema,
4684
+ execute: async (input) => listDirectoryGemini(input, options)
4685
+ });
4686
+ }
4687
+ function createGrepSearchTool(options = {}) {
4688
+ return tool({
4689
+ description: "Searches for a regex pattern within file contents.",
4690
+ inputSchema: geminiGrepSearchInputSchema,
4691
+ execute: async (input) => grepSearchGemini(input, options)
4692
+ });
4693
+ }
4694
+ function createGlobTool(options = {}) {
4695
+ return tool({
4696
+ description: "Finds files matching glob patterns, sorted by modification time (newest first).",
4697
+ inputSchema: geminiGlobInputSchema,
4698
+ execute: async (input) => globFilesGemini(input, options)
4699
+ });
4700
+ }
4701
+ async function readFileCodex(input, options) {
4702
+ const runtime = resolveRuntime(options);
4703
+ if (!path4.isAbsolute(input.file_path)) {
4704
+ throw new Error("file_path must be an absolute path");
4705
+ }
4706
+ const filePath = resolvePathWithPolicy(input.file_path, runtime.cwd, runtime.allowOutsideCwd);
4707
+ await runAccessHook2(runtime, {
4708
+ cwd: runtime.cwd,
4709
+ tool: "read_file",
4710
+ action: "read",
4711
+ path: filePath
4712
+ });
4713
+ const content = await runtime.filesystem.readTextFile(filePath);
4714
+ const lines = splitLines(content);
4715
+ const offset = input.offset ?? 1;
4716
+ const limit = input.limit ?? DEFAULT_READ_FILE_LINE_LIMIT;
4717
+ const mode = input.mode ?? "slice";
4718
+ if (offset > lines.length) {
4719
+ throw new Error("offset exceeds file length");
4720
+ }
4721
+ if (mode === "slice") {
4722
+ const output = [];
4723
+ const lastLine = Math.min(lines.length, offset + limit - 1);
4724
+ for (let lineNumber = offset; lineNumber <= lastLine; lineNumber += 1) {
4725
+ const line = lines[lineNumber - 1] ?? "";
4726
+ output.push(`L${lineNumber}: ${truncateAtCodePointBoundary(line, runtime.maxLineLength)}`);
4727
+ }
4728
+ return output.join("\n");
4729
+ }
4730
+ const indentation = input.indentation ?? {};
4731
+ const anchorLine = indentation.anchor_line ?? offset;
4732
+ if (anchorLine < 1 || anchorLine > lines.length) {
4733
+ throw new Error("anchor_line exceeds file length");
4734
+ }
4735
+ const records = lines.map((line, index) => ({
4736
+ number: index + 1,
4737
+ raw: line,
4738
+ display: truncateAtCodePointBoundary(line, runtime.maxLineLength),
4739
+ indent: measureIndent(line, DEFAULT_TAB_WIDTH)
4740
+ }));
4741
+ const selected = readWithIndentationMode({
4742
+ records,
4743
+ anchorLine,
4744
+ limit,
4745
+ maxLevels: indentation.max_levels ?? 0,
4746
+ includeSiblings: indentation.include_siblings ?? false,
4747
+ includeHeader: indentation.include_header ?? true,
4748
+ maxLines: indentation.max_lines
4749
+ });
4750
+ return selected.map((record) => `L${record.number}: ${record.display}`).join("\n");
4751
+ }
4752
+ async function listDirectoryCodex(input, options) {
4753
+ const runtime = resolveRuntime(options);
4754
+ if (!path4.isAbsolute(input.dir_path)) {
4755
+ throw new Error("dir_path must be an absolute path");
4756
+ }
4757
+ const dirPath = resolvePathWithPolicy(input.dir_path, runtime.cwd, runtime.allowOutsideCwd);
4758
+ await runAccessHook2(runtime, {
4759
+ cwd: runtime.cwd,
4760
+ tool: "list_dir",
4761
+ action: "list",
4762
+ path: dirPath
4763
+ });
4764
+ const stats = await runtime.filesystem.stat(dirPath);
4765
+ if (stats.kind !== "directory") {
4766
+ throw new Error(`failed to read directory: "${dirPath}" is not a directory`);
4767
+ }
4768
+ const offset = input.offset ?? 1;
4769
+ const limit = input.limit ?? DEFAULT_LIST_DIR_LIMIT;
4770
+ const depth = input.depth ?? DEFAULT_LIST_DIR_DEPTH;
4771
+ const entries = await collectDirectoryEntries(
4772
+ runtime.filesystem,
4773
+ dirPath,
4774
+ depth,
4775
+ runtime.maxLineLength
4776
+ );
4777
+ if (offset > entries.length) {
4778
+ throw new Error("offset exceeds directory entry count");
4779
+ }
4780
+ const startIndex = offset - 1;
4781
+ const remaining = entries.length - startIndex;
4782
+ const cappedLimit = Math.min(limit, remaining);
4783
+ const selected = entries.slice(startIndex, startIndex + cappedLimit);
4784
+ const output = [`Absolute path: ${dirPath}`];
4785
+ for (const entry of selected) {
4786
+ output.push(formatListEntry(entry));
4787
+ }
4788
+ if (startIndex + cappedLimit < entries.length) {
4789
+ output.push(`More than ${cappedLimit} entries found`);
4790
+ }
4791
+ return output.join("\n");
4792
+ }
4793
+ async function grepFilesCodex(input, options) {
4794
+ const runtime = resolveRuntime(options);
4795
+ const pattern = input.pattern.trim();
4796
+ if (pattern.length === 0) {
4797
+ throw new Error("pattern must not be empty");
4798
+ }
4799
+ const regex = compileRegex(pattern);
4800
+ const searchPath = resolvePathWithPolicy(
4801
+ input.path ?? runtime.cwd,
4802
+ runtime.cwd,
4803
+ runtime.allowOutsideCwd
4804
+ );
4805
+ await runAccessHook2(runtime, {
4806
+ cwd: runtime.cwd,
4807
+ tool: "grep_files",
4808
+ action: "search",
4809
+ path: searchPath,
4810
+ pattern,
4811
+ include: input.include?.trim()
4812
+ });
4813
+ const searchPathInfo = await runtime.filesystem.stat(searchPath);
4814
+ const filesToScan = await collectSearchFiles({
4815
+ filesystem: runtime.filesystem,
4816
+ searchPath,
4817
+ rootKind: searchPathInfo.kind,
4818
+ maxScannedFiles: runtime.grepMaxScannedFiles
4819
+ });
4820
+ const includeMatcher = input.include ? createGlobMatcher(input.include) : null;
4821
+ const matches = [];
4822
+ for (const filePath of filesToScan) {
4823
+ const relativePath = toDisplayPath2(filePath, runtime.cwd);
4824
+ if (includeMatcher && !includeMatcher(relativePath)) {
4825
+ continue;
4826
+ }
4827
+ const fileContent = await runtime.filesystem.readTextFile(filePath);
4828
+ if (!regex.test(fileContent)) {
4829
+ continue;
4830
+ }
4831
+ const stats = await runtime.filesystem.stat(filePath);
4832
+ matches.push({ filePath: normalizeSlashes(relativePath), mtimeMs: stats.mtimeMs });
4833
+ }
4834
+ if (matches.length === 0) {
4835
+ return "No matches found.";
4836
+ }
4837
+ matches.sort((left, right) => right.mtimeMs - left.mtimeMs);
4838
+ const limit = Math.min(input.limit ?? DEFAULT_GREP_LIMIT, MAX_GREP_LIMIT);
4839
+ return matches.slice(0, limit).map((match) => match.filePath).join("\n");
4840
+ }
4841
+ async function readFileGemini(input, options) {
4842
+ const runtime = resolveRuntime(options);
4843
+ const filePath = resolvePathWithPolicy(input.file_path, runtime.cwd, runtime.allowOutsideCwd);
4844
+ await runAccessHook2(runtime, {
4845
+ cwd: runtime.cwd,
4846
+ tool: "read_file",
4847
+ action: "read",
4848
+ path: filePath
4849
+ });
4850
+ const content = await runtime.filesystem.readTextFile(filePath);
4851
+ if (input.offset === void 0 && input.limit === void 0) {
4852
+ return content;
4853
+ }
4854
+ const lines = splitLines(content);
4855
+ const offset = Math.max(0, input.offset ?? 0);
4856
+ const limit = input.limit ?? DEFAULT_READ_FILE_LINE_LIMIT;
4857
+ if (offset >= lines.length) {
4858
+ return "";
4859
+ }
4860
+ const end = Math.min(lines.length, offset + limit);
4861
+ return lines.slice(offset, end).join("\n");
4862
+ }
4863
+ async function writeFileGemini(input, options) {
4864
+ const runtime = resolveRuntime(options);
4865
+ const filePath = resolvePathWithPolicy(input.file_path, runtime.cwd, runtime.allowOutsideCwd);
4866
+ await runAccessHook2(runtime, {
4867
+ cwd: runtime.cwd,
4868
+ tool: "write_file",
4869
+ action: "write",
4870
+ path: filePath
4871
+ });
4872
+ await runtime.filesystem.ensureDir(path4.dirname(filePath));
4873
+ await runtime.filesystem.writeTextFile(filePath, input.content);
4874
+ return `Successfully wrote file: ${toDisplayPath2(filePath, runtime.cwd)}`;
4875
+ }
4876
+ async function replaceFileContentGemini(input, options) {
4877
+ const runtime = resolveRuntime(options);
4878
+ const filePath = resolvePathWithPolicy(input.file_path, runtime.cwd, runtime.allowOutsideCwd);
4879
+ await runAccessHook2(runtime, {
4880
+ cwd: runtime.cwd,
4881
+ tool: "replace",
4882
+ action: "write",
4883
+ path: filePath
4884
+ });
4885
+ const expectedReplacements = input.expected_replacements ?? 1;
4886
+ const oldValue = input.old_string;
4887
+ const newValue = input.new_string;
4888
+ let originalContent = "";
4889
+ try {
4890
+ originalContent = await runtime.filesystem.readTextFile(filePath);
4891
+ } catch (error) {
4892
+ if (isNoEntError(error) && oldValue.length === 0) {
4893
+ await runtime.filesystem.ensureDir(path4.dirname(filePath));
4894
+ await runtime.filesystem.writeTextFile(filePath, newValue);
4895
+ return `Successfully wrote new file: ${toDisplayPath2(filePath, runtime.cwd)}`;
4896
+ }
4897
+ throw error;
4898
+ }
4899
+ if (oldValue === newValue) {
4900
+ throw new Error("No changes to apply. old_string and new_string are identical.");
4901
+ }
4902
+ const occurrences = countOccurrences(originalContent, oldValue);
4903
+ if (occurrences === 0) {
4904
+ throw new Error("Failed to edit, could not find old_string in file.");
4905
+ }
4906
+ if (occurrences !== expectedReplacements) {
4907
+ throw new Error(
4908
+ `Failed to edit, expected ${expectedReplacements} occurrence(s) but found ${occurrences}.`
4909
+ );
4910
+ }
4911
+ const updatedContent = safeReplaceAll(originalContent, oldValue, newValue);
4912
+ await runtime.filesystem.writeTextFile(filePath, updatedContent);
4913
+ return `Successfully replaced ${occurrences} occurrence(s) in ${toDisplayPath2(filePath, runtime.cwd)}.`;
4914
+ }
4915
+ async function listDirectoryGemini(input, options) {
4916
+ const runtime = resolveRuntime(options);
4917
+ const dirPath = resolvePathWithPolicy(input.dir_path, runtime.cwd, runtime.allowOutsideCwd);
4918
+ await runAccessHook2(runtime, {
4919
+ cwd: runtime.cwd,
4920
+ tool: "list_directory",
4921
+ action: "list",
4922
+ path: dirPath
4923
+ });
4924
+ const stats = await runtime.filesystem.stat(dirPath);
4925
+ if (stats.kind !== "directory") {
4926
+ throw new Error(`Path is not a directory: ${dirPath}`);
4927
+ }
4928
+ const entries = await runtime.filesystem.readDir(dirPath);
4929
+ const ignoreMatchers = (input.ignore ?? []).map((pattern) => createGlobMatcher(pattern));
4930
+ const filtered = entries.filter((entry) => {
4931
+ if (ignoreMatchers.length === 0) {
4932
+ return true;
4933
+ }
4934
+ return !ignoreMatchers.some((matches) => matches(entry.name));
4935
+ }).sort((left, right) => left.name.localeCompare(right.name));
4936
+ if (filtered.length === 0) {
4937
+ return `Directory ${toDisplayPath2(dirPath, runtime.cwd)} is empty.`;
4938
+ }
4939
+ return filtered.map((entry) => {
4940
+ const label = entry.kind === "directory" ? `${entry.name}/` : entry.name;
4941
+ return label;
4942
+ }).join("\n");
4943
+ }
4944
+ async function grepSearchGemini(input, options) {
4945
+ const runtime = resolveRuntime(options);
4946
+ const pattern = input.pattern.trim();
4947
+ if (pattern.length === 0) {
4948
+ throw new Error("pattern must not be empty");
4949
+ }
4950
+ const include = input.include?.trim();
4951
+ const searchPath = resolvePathWithPolicy(
4952
+ input.dir_path ?? runtime.cwd,
4953
+ runtime.cwd,
4954
+ runtime.allowOutsideCwd
4955
+ );
4956
+ await runAccessHook2(runtime, {
4957
+ cwd: runtime.cwd,
4958
+ tool: "grep_search",
4959
+ action: "search",
4960
+ path: searchPath,
4961
+ pattern,
4962
+ include
4963
+ });
4964
+ const searchPathInfo = await runtime.filesystem.stat(searchPath);
4965
+ const filesToScan = await collectSearchFiles({
4966
+ filesystem: runtime.filesystem,
4967
+ searchPath,
4968
+ rootKind: searchPathInfo.kind,
4969
+ maxScannedFiles: runtime.grepMaxScannedFiles
4970
+ });
4971
+ const matcher = include ? createGlobMatcher(include) : null;
4972
+ const patternRegex = compileRegex(pattern);
4973
+ const excludeRegex = input.exclude_pattern ? compileRegex(input.exclude_pattern) : null;
4974
+ const totalMaxMatches = input.total_max_matches ?? DEFAULT_GREP_LIMIT;
4975
+ const perFileMaxMatches = input.max_matches_per_file ?? Number.POSITIVE_INFINITY;
4976
+ const matches = [];
4977
+ const fileMatches = /* @__PURE__ */ new Set();
4978
+ for (const filePath of filesToScan) {
4979
+ const relativePath = normalizeSlashes(toDisplayPath2(filePath, runtime.cwd));
4980
+ if (matcher && !matcher(relativePath)) {
4981
+ continue;
4982
+ }
4983
+ const content = await runtime.filesystem.readTextFile(filePath);
4984
+ const lines = splitLines(content);
4985
+ let fileMatchCount = 0;
4986
+ for (let index = 0; index < lines.length; index += 1) {
4987
+ const line = lines[index] ?? "";
4988
+ if (!patternRegex.test(line)) {
4989
+ continue;
4990
+ }
4991
+ if (excludeRegex?.test(line)) {
4992
+ continue;
4993
+ }
4994
+ if (fileMatches.has(relativePath) === false) {
4995
+ fileMatches.add(relativePath);
4996
+ }
4997
+ if (input.names_only) {
4998
+ continue;
4999
+ }
5000
+ matches.push({
5001
+ filePath: relativePath,
5002
+ mtimeMs: 0,
5003
+ lineNumber: index + 1,
5004
+ line
5005
+ });
5006
+ fileMatchCount += 1;
5007
+ if (fileMatchCount >= perFileMaxMatches || matches.length >= totalMaxMatches) {
5008
+ break;
5009
+ }
5010
+ }
5011
+ if (input.names_only && fileMatches.size >= totalMaxMatches) {
5012
+ break;
5013
+ }
5014
+ if (!input.names_only && matches.length >= totalMaxMatches) {
5015
+ break;
5016
+ }
5017
+ }
5018
+ if (input.names_only) {
5019
+ if (fileMatches.size === 0) {
5020
+ return "No matches found.";
5021
+ }
5022
+ return [...fileMatches].slice(0, totalMaxMatches).join("\n");
5023
+ }
5024
+ if (matches.length === 0) {
5025
+ return "No matches found.";
5026
+ }
5027
+ return matches.slice(0, totalMaxMatches).map((match) => `${match.filePath}:${match.lineNumber}:${match.line ?? ""}`).join("\n");
5028
+ }
5029
+ async function globFilesGemini(input, options) {
5030
+ const runtime = resolveRuntime(options);
5031
+ const dirPath = resolvePathWithPolicy(
5032
+ input.dir_path ?? runtime.cwd,
5033
+ runtime.cwd,
5034
+ runtime.allowOutsideCwd
5035
+ );
5036
+ await runAccessHook2(runtime, {
5037
+ cwd: runtime.cwd,
5038
+ tool: "glob",
5039
+ action: "search",
5040
+ path: dirPath,
5041
+ pattern: input.pattern
5042
+ });
5043
+ const dirStats = await runtime.filesystem.stat(dirPath);
5044
+ if (dirStats.kind !== "directory") {
5045
+ throw new Error(`Path is not a directory: ${dirPath}`);
5046
+ }
5047
+ const matcher = createGlobMatcher(input.pattern, input.case_sensitive === true);
5048
+ const files = await collectSearchFiles({
5049
+ filesystem: runtime.filesystem,
5050
+ searchPath: dirPath,
5051
+ rootKind: "directory",
5052
+ maxScannedFiles: runtime.grepMaxScannedFiles
5053
+ });
5054
+ const matched = [];
5055
+ for (const filePath of files) {
5056
+ const relativePath = normalizeSlashes(path4.relative(dirPath, filePath));
5057
+ if (!matcher(relativePath)) {
5058
+ continue;
5059
+ }
5060
+ const fileStats = await runtime.filesystem.stat(filePath);
5061
+ matched.push({
5062
+ filePath,
5063
+ mtimeMs: fileStats.mtimeMs
5064
+ });
5065
+ }
5066
+ if (matched.length === 0) {
5067
+ return "No files found.";
5068
+ }
5069
+ matched.sort((left, right) => right.mtimeMs - left.mtimeMs);
5070
+ return matched.map((entry) => normalizeSlashes(path4.resolve(entry.filePath))).join("\n");
5071
+ }
5072
+ function resolveRuntime(options) {
5073
+ return {
5074
+ cwd: path4.resolve(options.cwd ?? process.cwd()),
5075
+ filesystem: options.fs ?? createNodeAgentFilesystem(),
5076
+ allowOutsideCwd: options.allowOutsideCwd === true,
5077
+ checkAccess: options.checkAccess,
5078
+ maxLineLength: options.maxLineLength ?? DEFAULT_MAX_LINE_LENGTH,
5079
+ grepMaxScannedFiles: options.grepMaxScannedFiles ?? DEFAULT_GREP_MAX_SCANNED_FILES
5080
+ };
5081
+ }
5082
+ async function runAccessHook2(runtime, context) {
5083
+ if (!runtime.checkAccess) {
5084
+ return;
5085
+ }
5086
+ await runtime.checkAccess(context);
5087
+ }
5088
+ function isCodexModel(model) {
5089
+ const normalized = model.startsWith("chatgpt-") ? model.slice("chatgpt-".length) : model;
5090
+ return normalized.includes("codex");
5091
+ }
5092
+ function isGeminiModel(model) {
5093
+ return model.startsWith("gemini-");
5094
+ }
5095
+ function mapApplyPatchAction(action) {
5096
+ if (action === "add" || action === "update") {
5097
+ return "write";
5098
+ }
5099
+ if (action === "delete") {
5100
+ return "delete";
5101
+ }
5102
+ return "move";
5103
+ }
5104
+ function resolvePathWithPolicy(inputPath, cwd, allowOutsideCwd) {
5105
+ const absolutePath = path4.isAbsolute(inputPath) ? path4.resolve(inputPath) : path4.resolve(cwd, inputPath);
5106
+ if (!allowOutsideCwd && !isPathInsideCwd2(absolutePath, cwd)) {
5107
+ throw new Error(`path "${inputPath}" resolves outside cwd "${cwd}"`);
5108
+ }
5109
+ return absolutePath;
5110
+ }
5111
+ function isPathInsideCwd2(candidatePath, cwd) {
5112
+ const relative = path4.relative(cwd, candidatePath);
5113
+ return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
5114
+ }
5115
+ function toDisplayPath2(absolutePath, cwd) {
5116
+ const relative = path4.relative(cwd, absolutePath);
5117
+ if (relative === "") {
5118
+ return ".";
5119
+ }
5120
+ if (!relative.startsWith("..") && !path4.isAbsolute(relative)) {
5121
+ return relative;
5122
+ }
5123
+ return absolutePath;
5124
+ }
5125
+ function splitLines(content) {
5126
+ const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
5127
+ const lines = normalized.split("\n");
5128
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
5129
+ lines.pop();
5130
+ }
5131
+ return lines;
5132
+ }
5133
+ function truncateAtCodePointBoundary(value, maxLength) {
5134
+ if (value.length <= maxLength) {
5135
+ return value;
5136
+ }
5137
+ return Array.from(value).slice(0, maxLength).join("");
5138
+ }
5139
+ function measureIndent(line, tabWidth) {
5140
+ let count = 0;
5141
+ for (const char of line) {
5142
+ if (char === " ") {
5143
+ count += 1;
5144
+ continue;
5145
+ }
5146
+ if (char === " ") {
5147
+ count += tabWidth;
5148
+ continue;
5149
+ }
5150
+ break;
5151
+ }
5152
+ return count;
5153
+ }
5154
+ function computeEffectiveIndents(records) {
5155
+ const effective = [];
5156
+ let previous = 0;
5157
+ for (const record of records) {
5158
+ if (record.raw.trim().length === 0) {
5159
+ effective.push(previous);
5160
+ } else {
5161
+ previous = record.indent;
5162
+ effective.push(previous);
5163
+ }
5164
+ }
5165
+ return effective;
5166
+ }
5167
+ function trimBoundaryBlankLines(records) {
5168
+ while (records.length > 0 && records[0]?.raw.trim().length === 0) {
5169
+ records.shift();
5170
+ }
5171
+ while (records.length > 0 && records[records.length - 1]?.raw.trim().length === 0) {
5172
+ records.pop();
5173
+ }
5174
+ }
5175
+ function isCommentLine(line) {
5176
+ const trimmed = line.trim();
5177
+ return trimmed.startsWith("#") || trimmed.startsWith("//") || trimmed.startsWith("--");
5178
+ }
5179
+ function readWithIndentationMode(params) {
5180
+ const { records, anchorLine, limit, maxLevels, includeSiblings, includeHeader, maxLines } = params;
5181
+ const anchorIndex = anchorLine - 1;
5182
+ const effectiveIndents = computeEffectiveIndents(records);
5183
+ const anchorIndent = effectiveIndents[anchorIndex] ?? 0;
5184
+ const minIndent = maxLevels === 0 ? 0 : Math.max(anchorIndent - maxLevels * DEFAULT_TAB_WIDTH, 0);
5185
+ const guardLimit = maxLines ?? limit;
5186
+ const finalLimit = Math.min(limit, guardLimit, records.length);
5187
+ if (finalLimit <= 1) {
5188
+ return [records[anchorIndex]].filter((entry) => Boolean(entry));
5189
+ }
5190
+ let upper = anchorIndex - 1;
5191
+ let lower = anchorIndex + 1;
5192
+ let upperMinIndentHits = 0;
5193
+ let lowerMinIndentHits = 0;
5194
+ const output = [records[anchorIndex]].filter(
5195
+ (entry) => Boolean(entry)
5196
+ );
5197
+ while (output.length < finalLimit) {
5198
+ let progressed = 0;
5199
+ if (upper >= 0) {
5200
+ const candidate = records[upper];
5201
+ const candidateIndent = effectiveIndents[upper] ?? 0;
5202
+ if (candidate && candidateIndent >= minIndent) {
5203
+ output.unshift(candidate);
5204
+ progressed += 1;
5205
+ upper -= 1;
5206
+ if (candidateIndent === minIndent && !includeSiblings) {
5207
+ const allowHeaderComment = includeHeader && isCommentLine(candidate.raw);
5208
+ const canTakeLine = allowHeaderComment || upperMinIndentHits === 0;
5209
+ if (canTakeLine) {
5210
+ upperMinIndentHits += 1;
5211
+ } else {
5212
+ output.shift();
5213
+ progressed -= 1;
5214
+ upper = -1;
5215
+ }
5216
+ }
5217
+ if (output.length >= finalLimit) {
5218
+ break;
5219
+ }
5220
+ } else {
5221
+ upper = -1;
5222
+ }
5223
+ }
5224
+ if (lower < records.length) {
5225
+ const candidate = records[lower];
5226
+ const candidateIndent = effectiveIndents[lower] ?? 0;
5227
+ if (candidate && candidateIndent >= minIndent) {
5228
+ output.push(candidate);
5229
+ progressed += 1;
5230
+ lower += 1;
5231
+ if (candidateIndent === minIndent && !includeSiblings) {
5232
+ if (lowerMinIndentHits > 0) {
5233
+ output.pop();
5234
+ progressed -= 1;
5235
+ lower = records.length;
5236
+ }
5237
+ lowerMinIndentHits += 1;
5238
+ }
5239
+ } else {
5240
+ lower = records.length;
5241
+ }
5242
+ }
5243
+ if (progressed === 0) {
5244
+ break;
5245
+ }
5246
+ }
5247
+ trimBoundaryBlankLines(output);
5248
+ return output;
5249
+ }
5250
+ async function collectDirectoryEntries(filesystem, rootPath, depth, maxLineLength) {
5251
+ const queue = [
5252
+ { path: rootPath, relativePrefix: "", remainingDepth: depth }
5253
+ ];
5254
+ const records = [];
5255
+ while (queue.length > 0) {
5256
+ const next = queue.shift();
5257
+ if (!next) {
5258
+ break;
5259
+ }
5260
+ const entries = await filesystem.readDir(next.path);
5261
+ const nextEntries = [...entries].map((entry) => {
5262
+ const relativePath = next.relativePrefix ? `${next.relativePrefix}/${entry.name}` : entry.name;
5263
+ return {
5264
+ entry,
5265
+ relativePath,
5266
+ depth: next.relativePrefix.length === 0 ? 0 : next.relativePrefix.split("/").length,
5267
+ sortName: normalizeSlashes(relativePath)
5268
+ };
5269
+ }).sort((left, right) => left.sortName.localeCompare(right.sortName));
5270
+ for (const item of nextEntries) {
5271
+ if (item.entry.kind === "directory" && next.remainingDepth > 1) {
5272
+ queue.push({
5273
+ path: item.entry.path,
5274
+ relativePrefix: item.relativePath,
5275
+ remainingDepth: next.remainingDepth - 1
5276
+ });
5277
+ }
5278
+ records.push({
5279
+ name: item.sortName,
5280
+ displayName: truncateAtCodePointBoundary(item.entry.name, maxLineLength),
5281
+ depth: item.depth,
5282
+ kind: item.entry.kind
5283
+ });
5284
+ }
5285
+ }
5286
+ records.sort((left, right) => left.name.localeCompare(right.name));
5287
+ return records;
5288
+ }
5289
+ function formatListEntry(entry) {
5290
+ const indent = " ".repeat(entry.depth * 2);
5291
+ let name = entry.displayName;
5292
+ if (entry.kind === "directory") {
5293
+ name += "/";
5294
+ } else if (entry.kind === "symlink") {
5295
+ name += "@";
5296
+ } else if (entry.kind === "other") {
5297
+ name += "?";
5298
+ }
5299
+ return `${indent}${name}`;
5300
+ }
5301
+ async function collectSearchFiles(params) {
5302
+ const { filesystem, searchPath, rootKind, maxScannedFiles } = params;
5303
+ if (rootKind === "file") {
5304
+ return [searchPath];
5305
+ }
5306
+ const queue = [searchPath];
5307
+ const files = [];
5308
+ while (queue.length > 0) {
5309
+ const current = queue.shift();
5310
+ if (!current) {
5311
+ break;
5312
+ }
5313
+ const entries = await filesystem.readDir(current);
5314
+ for (const entry of entries) {
5315
+ if (entry.kind === "directory") {
5316
+ queue.push(entry.path);
5317
+ continue;
5318
+ }
5319
+ if (entry.kind !== "file") {
5320
+ continue;
5321
+ }
5322
+ files.push(entry.path);
5323
+ if (files.length >= maxScannedFiles) {
5324
+ return files;
5325
+ }
5326
+ }
5327
+ }
5328
+ return files;
5329
+ }
5330
+ function compileRegex(pattern) {
5331
+ try {
5332
+ return new RegExp(pattern, "m");
5333
+ } catch (error) {
5334
+ const message = error instanceof Error ? error.message : String(error);
5335
+ throw new Error(`invalid regex pattern: ${message}`);
5336
+ }
5337
+ }
5338
+ function createGlobMatcher(pattern, caseSensitive = false) {
5339
+ const expanded = expandBracePatterns(normalizeSlashes(pattern.trim()));
5340
+ const flags = caseSensitive ? "" : "i";
5341
+ const compiled = expanded.map((entry) => ({
5342
+ regex: globToRegex(entry, flags),
5343
+ applyToBasename: !entry.includes("/")
5344
+ }));
5345
+ return (candidatePath) => {
5346
+ const normalizedPath = normalizeSlashes(candidatePath);
5347
+ const basename = path4.posix.basename(normalizedPath);
5348
+ return compiled.some(
5349
+ (entry) => entry.regex.test(entry.applyToBasename ? basename : normalizedPath)
5350
+ );
5351
+ };
5352
+ }
5353
+ function globToRegex(globPattern, flags) {
5354
+ let source = "^";
5355
+ for (let index = 0; index < globPattern.length; index += 1) {
5356
+ const char = globPattern[index];
5357
+ const nextChar = globPattern[index + 1];
5358
+ if (char === void 0) {
5359
+ continue;
5360
+ }
5361
+ if (char === "*" && nextChar === "*") {
5362
+ source += ".*";
5363
+ index += 1;
5364
+ continue;
5365
+ }
5366
+ if (char === "*") {
5367
+ source += "[^/]*";
5368
+ continue;
5369
+ }
5370
+ if (char === "?") {
5371
+ source += "[^/]";
5372
+ continue;
5373
+ }
5374
+ source += escapeRegexCharacter(char);
5375
+ }
5376
+ source += "$";
5377
+ return new RegExp(source, flags);
5378
+ }
5379
+ function expandBracePatterns(pattern) {
5380
+ const start = pattern.indexOf("{");
5381
+ if (start === -1) {
5382
+ return [pattern];
5383
+ }
5384
+ let depth = 0;
5385
+ let end = -1;
5386
+ for (let index = start; index < pattern.length; index += 1) {
5387
+ const char = pattern[index];
5388
+ if (char === "{") {
5389
+ depth += 1;
5390
+ continue;
5391
+ }
5392
+ if (char === "}") {
5393
+ depth -= 1;
5394
+ if (depth === 0) {
5395
+ end = index;
5396
+ break;
5397
+ }
5398
+ }
5399
+ }
5400
+ if (end === -1) {
5401
+ return [pattern];
5402
+ }
5403
+ const prefix = pattern.slice(0, start);
5404
+ const suffix = pattern.slice(end + 1);
5405
+ const body = pattern.slice(start + 1, end);
5406
+ const variants = splitTopLevel(body, ",");
5407
+ const expanded = [];
5408
+ for (const variant of variants) {
5409
+ expanded.push(...expandBracePatterns(`${prefix}${variant}${suffix}`));
5410
+ }
5411
+ return expanded;
5412
+ }
5413
+ function splitTopLevel(value, separator) {
5414
+ const parts = [];
5415
+ let depth = 0;
5416
+ let current = "";
5417
+ for (const char of value) {
5418
+ if (char === "{") {
5419
+ depth += 1;
5420
+ current += char;
5421
+ continue;
5422
+ }
5423
+ if (char === "}") {
5424
+ depth = Math.max(0, depth - 1);
5425
+ current += char;
5426
+ continue;
5427
+ }
5428
+ if (char === separator && depth === 0) {
5429
+ parts.push(current);
5430
+ current = "";
5431
+ continue;
5432
+ }
5433
+ current += char;
5434
+ }
5435
+ parts.push(current);
5436
+ return parts;
5437
+ }
5438
+ function escapeRegexCharacter(char) {
5439
+ return /[.*+?^${}()|[\]\\]/u.test(char) ? `\\${char}` : char;
5440
+ }
5441
+ function normalizeSlashes(value) {
5442
+ return value.replaceAll("\\", "/");
5443
+ }
5444
+ function countOccurrences(text, search) {
5445
+ if (search.length === 0) {
5446
+ return 0;
5447
+ }
5448
+ return text.split(search).length - 1;
5449
+ }
5450
+ function safeReplaceAll(text, search, replacement) {
5451
+ if (search.length === 0) {
5452
+ return text;
5453
+ }
5454
+ return text.split(search).join(replacement);
5455
+ }
5456
+ function isNoEntError(error) {
5457
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
5458
+ }
5459
+
5460
+ // src/agent.ts
5461
+ async function runAgentLoop(request) {
5462
+ const { tools: customTools, filesystemTool, filesystem_tool, ...toolLoopRequest } = request;
5463
+ const filesystemSelection = filesystemTool ?? filesystem_tool;
5464
+ const filesystemTools = resolveFilesystemTools(request.model, filesystemSelection);
5465
+ const mergedTools = mergeToolSets(filesystemTools, customTools ?? {});
5466
+ if (Object.keys(mergedTools).length === 0) {
5467
+ throw new Error(
5468
+ "runAgentLoop requires at least one tool. Provide `tools` or enable `filesystemTool`."
5469
+ );
5470
+ }
5471
+ return runToolLoop({
5472
+ ...toolLoopRequest,
5473
+ tools: mergedTools
5474
+ });
5475
+ }
5476
+ function resolveFilesystemTools(model, selection) {
5477
+ if (selection === void 0 || selection === false) {
5478
+ return {};
5479
+ }
5480
+ if (selection === true) {
5481
+ return createFilesystemToolSetForModel(model, "auto");
5482
+ }
5483
+ if (typeof selection === "string") {
5484
+ return createFilesystemToolSetForModel(model, selection);
5485
+ }
5486
+ if (selection.enabled === false) {
5487
+ return {};
5488
+ }
5489
+ if (selection.options && selection.profile !== void 0) {
5490
+ return createFilesystemToolSetForModel(model, selection.profile, selection.options);
5491
+ }
5492
+ if (selection.options) {
5493
+ return createFilesystemToolSetForModel(model, selection.options);
5494
+ }
5495
+ return createFilesystemToolSetForModel(model, selection.profile ?? "auto");
5496
+ }
5497
+ function mergeToolSets(base, extra) {
5498
+ const merged = { ...base };
5499
+ for (const [toolName, toolSpec] of Object.entries(extra)) {
5500
+ if (Object.hasOwn(merged, toolName)) {
5501
+ throw new Error(
5502
+ `Duplicate tool name "${toolName}" in runAgentLoop. Rename the custom tool or disable that filesystem tool.`
5503
+ );
5504
+ }
5505
+ merged[toolName] = toolSpec;
5506
+ }
5507
+ return merged;
5508
+ }
3418
5509
  export {
5510
+ InMemoryAgentFilesystem,
3419
5511
  LlmJsonCallError,
3420
5512
  appendMarkdownSourcesSection,
5513
+ applyPatch,
3421
5514
  configureGemini,
3422
5515
  convertGooglePartsToLlmParts,
5516
+ createApplyPatchTool,
5517
+ createCodexApplyPatchTool,
5518
+ createCodexFilesystemToolSet,
5519
+ createCodexReadFileTool,
5520
+ createFilesystemToolSetForModel,
5521
+ createGeminiFilesystemToolSet,
5522
+ createGlobTool,
5523
+ createGrepFilesTool,
5524
+ createGrepSearchTool,
5525
+ createInMemoryAgentFilesystem,
5526
+ createListDirTool,
5527
+ createListDirectoryTool,
5528
+ createModelAgnosticFilesystemToolSet,
5529
+ createNodeAgentFilesystem,
5530
+ createReadFileTool,
5531
+ createReplaceTool,
5532
+ createWriteFileTool,
3423
5533
  encodeChatGptAuthJson,
3424
5534
  encodeChatGptAuthJsonB64,
3425
5535
  estimateCallCostUsd,
@@ -3435,8 +5545,11 @@ export {
3435
5545
  loadLocalEnv,
3436
5546
  parseJsonFromLlmText,
3437
5547
  refreshChatGptOauthToken,
5548
+ resolveFilesystemToolProfile,
5549
+ runAgentLoop,
3438
5550
  runToolLoop,
3439
5551
  sanitisePartForLogging,
5552
+ streamJson,
3440
5553
  streamText,
3441
5554
  stripCodexCitationMarkers,
3442
5555
  toGeminiJsonSchema,