zidane 5.3.2 → 5.4.1

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 (47) hide show
  1. package/README.md +16 -1
  2. package/dist/{agent-BXRCCHeq.d.ts → agent-DHQAsdj6.d.ts} +47 -15
  3. package/dist/agent-DHQAsdj6.d.ts.map +1 -0
  4. package/dist/chat.d.ts +62 -5
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +2 -2
  7. package/dist/{index-CT5_p-3P.d.ts → index-CHSaLab5.d.ts} +2 -2
  8. package/dist/{index-CT5_p-3P.d.ts.map → index-CHSaLab5.d.ts.map} +1 -1
  9. package/dist/{index-BPk8-Slm.d.ts → index-CrqFoaQA.d.ts} +12 -11
  10. package/dist/index-CrqFoaQA.d.ts.map +1 -0
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +4 -4
  13. package/dist/{login-DrBZ15G7.js → login-8c5C0FYq.js} +2 -2
  14. package/dist/{login-DrBZ15G7.js.map → login-8c5C0FYq.js.map} +1 -1
  15. package/dist/mcp.d.ts +1 -1
  16. package/dist/{presets-0_IRJAYF.js → presets-Ck4VusTo.js} +2 -2
  17. package/dist/{presets-0_IRJAYF.js.map → presets-Ck4VusTo.js.map} +1 -1
  18. package/dist/presets.d.ts +2 -2
  19. package/dist/presets.js +1 -1
  20. package/dist/providers.d.ts +1 -1
  21. package/dist/session/sqlite.d.ts +1 -1
  22. package/dist/session.d.ts +1 -1
  23. package/dist/skills.d.ts +2 -2
  24. package/dist/{tools-CCsL5SCO.js → tools-PQH1Ge4M.js} +373 -84
  25. package/dist/tools-PQH1Ge4M.js.map +1 -0
  26. package/dist/tools.d.ts +2 -2
  27. package/dist/tools.js +1 -1
  28. package/dist/{transcript-anchors-DSk8LlWt.d.ts → transcript-anchors-ByB2MSCB.d.ts} +19 -4
  29. package/dist/transcript-anchors-ByB2MSCB.d.ts.map +1 -0
  30. package/dist/tui.d.ts +3 -3
  31. package/dist/tui.d.ts.map +1 -1
  32. package/dist/tui.js +52 -23
  33. package/dist/tui.js.map +1 -1
  34. package/dist/{turn-operations-CutZin8X.js → turn-operations-Bqs4YbbH.js} +129 -4
  35. package/dist/turn-operations-Bqs4YbbH.js.map +1 -0
  36. package/dist/types-IcokUOyC.js.map +1 -1
  37. package/dist/types.d.ts +3 -3
  38. package/docs/ARCHITECTURE.md +23 -11
  39. package/docs/CHAT.md +21 -3
  40. package/docs/SKILL.md +34 -3
  41. package/docs/TUI.md +2 -2
  42. package/package.json +1 -1
  43. package/dist/agent-BXRCCHeq.d.ts.map +0 -1
  44. package/dist/index-BPk8-Slm.d.ts.map +0 -1
  45. package/dist/tools-CCsL5SCO.js.map +0 -1
  46. package/dist/transcript-anchors-DSk8LlWt.d.ts.map +0 -1
  47. package/dist/turn-operations-CutZin8X.js.map +0 -1
@@ -805,20 +805,13 @@ const IMAGE_OMITTED_MARKER = "[image omitted — model does not support vision]"
805
805
  const INTERRUPT_MESSAGE_FOR_TOOL_USE = "[Request interrupted by user for tool use]";
806
806
  /**
807
807
  * Canonical tool_result text emitted when a tool call is skipped because a
808
- * sibling sequential call errored or a steering message arrived between
809
- * iterations of {@link executeToolsSequential}. Distinguished from
810
- * {@link INTERRUPT_MESSAGE_FOR_TOOL_USE} so consumers can distinguish "user
808
+ * steering message arrived between dispatches inside
809
+ * {@link executeToolBatch}. Distinguished from
810
+ * {@link INTERRUPT_MESSAGE_FOR_TOOL_USE} so consumers can split "user
811
811
  * cancelled" from "framework superseded".
812
812
  */
813
813
  const TOOL_USE_SKIPPED_MESSAGE = "[Tool use skipped — superseded by user message]";
814
814
  /**
815
- * Canonical tool_result text emitted when the loop catches a sequential
816
- * sibling that threw and synthesizes follow-up results for the remaining
817
- * queued calls. Distinct from {@link TOOL_USE_SKIPPED_MESSAGE} so telemetry
818
- * can split "skipped by user steering" from "skipped after error".
819
- */
820
- const TOOL_USE_AFTER_ERROR_MESSAGE = "[Tool use skipped — previous tool call in batch threw]";
821
- /**
822
815
  * Compute the effective thinking budget for a given run-relative turn, given
823
816
  * the configured decay schedule. Pure helper — exported for tests and so
824
817
  * downstream tooling can preview decay curves without spinning up the loop.
@@ -1078,6 +1071,8 @@ function applyStaleReadElision(messages) {
1078
1071
  messages,
1079
1072
  elidedPaths: []
1080
1073
  };
1074
+ const pathsWithFreshRead = /* @__PURE__ */ new Set();
1075
+ for (const [callId, info] of readCallInfo) if (!staleCallIds.has(callId)) pathsWithFreshRead.add(info.path);
1081
1076
  let changed = false;
1082
1077
  const out = messages.slice();
1083
1078
  for (let i = 0; i < out.length; i++) {
@@ -1101,7 +1096,7 @@ function applyStaleReadElision(messages) {
1101
1096
  }
1102
1097
  return {
1103
1098
  messages: changed ? out : messages,
1104
- elidedPaths: [...elidedPathSet]
1099
+ elidedPaths: [...elidedPathSet].filter((p) => !pathsWithFreshRead.has(p))
1105
1100
  };
1106
1101
  }
1107
1102
  /**
@@ -1518,7 +1513,7 @@ async function executeTurn(ctx, turn) {
1518
1513
  usage: result.usage
1519
1514
  };
1520
1515
  }
1521
- const toolResults = ctx.toolExecution === "parallel" ? await executeToolsParallel(ctx, canonicalToolCalls, turnId) : await executeToolsSequential(ctx, canonicalToolCalls, turnId);
1516
+ const toolResults = await executeToolBatch(ctx, canonicalToolCalls, turnId);
1522
1517
  const toolResultMsg = ctx.provider.toolResultsMessage(toolResults);
1523
1518
  const toolResultsTurn = {
1524
1519
  id: await ctx.generateTurnId(),
@@ -1859,57 +1854,170 @@ async function emitToolResult(ctx, params) {
1859
1854
  isError
1860
1855
  };
1861
1856
  }
1862
- async function executeToolsSequential(ctx, toolCalls, turnId) {
1863
- const results = [];
1864
- for (let i = 0; i < toolCalls.length; i++) {
1865
- const call = toolCalls[i];
1866
- if (ctx.signal.aborted) {
1867
- for (let j = i; j < toolCalls.length; j++) results.push({
1868
- id: toolCalls[j].id,
1869
- content: INTERRUPT_MESSAGE_FOR_TOOL_USE,
1870
- isError: true
1871
- });
1872
- return results;
1873
- }
1874
- if (ctx.steeringQueue.length > 0) {
1875
- for (let j = i; j < toolCalls.length; j++) results.push({
1876
- id: toolCalls[j].id,
1877
- content: TOOL_USE_SKIPPED_MESSAGE,
1878
- isError: true
1879
- });
1880
- return results;
1881
- }
1882
- try {
1883
- const { result } = await executeSingleTool(ctx, call, turnId);
1884
- results.push(result);
1885
- } catch (err) {
1886
- results.push({
1887
- id: call.id,
1888
- content: `Error: ${errorMessage(err)}`,
1889
- isError: true
1890
- });
1891
- for (let j = i + 1; j < toolCalls.length; j++) results.push({
1892
- id: toolCalls[j].id,
1893
- content: TOOL_USE_AFTER_ERROR_MESSAGE,
1894
- isError: true
1895
- });
1896
- return results;
1897
- }
1857
+ /** Default cap on in-flight tools per turn. Mirrors Claude Code's `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY`. */
1858
+ const DEFAULT_MAX_CONCURRENT_TOOLS = 10;
1859
+ /** Canonical name of the shell tool — referenced for cascade-cancel semantics. */
1860
+ const SHELL_TOOL_NAME = "shell";
1861
+ /** Reason surfaced on `siblingAbort.signal` when a shell error cancels its fleet. */
1862
+ const SHELL_CASCADE_REASON = "sibling-shell-error";
1863
+ /**
1864
+ * Canonical `tool_result.content` text emitted to siblings that were
1865
+ * cancelled by a `shell` error in the same batch. Distinct from
1866
+ * {@link INTERRUPT_MESSAGE_FOR_TOOL_USE} (user-issued abort) and
1867
+ * {@link TOOL_USE_SKIPPED_MESSAGE} (steered) so consumers can split
1868
+ * the three causes by string-match.
1869
+ */
1870
+ const SHELL_CASCADE_CANCEL_MESSAGE = "Cancelled: a sibling `shell` call in the same batch errored; re-run independently if still needed.";
1871
+ /**
1872
+ * Resolve a tool's concurrency-safety verdict for a specific call.
1873
+ *
1874
+ * - Missing toolDef (unknown tool) → `false`. `executeSingleTool` handles
1875
+ * the unknown-tool path itself; barriering it keeps the unknown-tool
1876
+ * error from racing with siblings.
1877
+ * - Static `true` / `false` → use as-is.
1878
+ * - Function invoke; any throw is treated as `false` (fail-closed) so a
1879
+ * buggy predicate can't accidentally widen the safety window.
1880
+ *
1881
+ * Pure / sync — pre-computed once per call before dispatch begins, so the
1882
+ * scheduler's hot path stays branch-light.
1883
+ */
1884
+ function resolveConcurrencySafe(def, input) {
1885
+ if (!def) return false;
1886
+ const flag = def.isConcurrencySafe;
1887
+ if (flag === void 0) return false;
1888
+ if (typeof flag === "boolean") return flag;
1889
+ try {
1890
+ return flag(input) === true;
1891
+ } catch {
1892
+ return false;
1898
1893
  }
1899
- return results;
1900
1894
  }
1901
- async function executeToolsParallel(ctx, toolCalls, turnId) {
1902
- const executions = toolCalls.map((call) => executeSingleTool(ctx, call, turnId));
1903
- return (await Promise.allSettled(executions)).map((s, i) => {
1904
- if (s.status === "fulfilled") return s.value.result;
1905
- const reason = s.reason;
1906
- const isAbort = ctx.signal.aborted || reason instanceof Error && reason.name === "AbortError";
1907
- return {
1908
- id: toolCalls[i].id,
1909
- content: isAbort ? INTERRUPT_MESSAGE_FOR_TOOL_USE : `Error: ${reason instanceof Error ? reason.message : String(reason)}`,
1895
+ /**
1896
+ * Unified per-turn tool dispatcher.
1897
+ *
1898
+ * Walks `toolCalls` in submission order. For each call:
1899
+ *
1900
+ * - **Concurrency-safe + fleet is all safe + room under the cap** → fires
1901
+ * asynchronously and the loop advances to the next call. The fleet runs
1902
+ * in parallel up to `behavior.maxConcurrentTools` (default {@link
1903
+ * DEFAULT_MAX_CONCURRENT_TOOLS}).
1904
+ * - **Unsafe** (or the in-flight fleet contains anything unsafe) → acts
1905
+ * as a barrier: waits for the fleet to drain, then runs alone, then
1906
+ * unblocks the queue.
1907
+ *
1908
+ * Results are written into a fixed `results[index]` array on completion
1909
+ * and yielded back in submission order, so the model sees deterministic
1910
+ * adjacency regardless of which call finished first.
1911
+ *
1912
+ * **Failure modes:**
1913
+ *
1914
+ * - **Hook throws / tool body throws** — captured per-call into a
1915
+ * `tool_result` so the assistant turn's `tool_use` IDs always have
1916
+ * matching `tool_result` IDs (providers reject orphan IDs).
1917
+ * - **Parent abort** mid-batch — drains the in-flight fleet (their
1918
+ * `AbortError` becomes `INTERRUPT_MESSAGE_FOR_TOOL_USE`), then synthesizes
1919
+ * interrupt results for any unstarted calls so the turn closes cleanly.
1920
+ * - **Steering queue populated** between dispatches — same drain + a
1921
+ * `TOOL_USE_SKIPPED_MESSAGE` result for unstarted calls. The outer loop
1922
+ * picks up the steer at the next checkpoint.
1923
+ * - **Shell error in a fleet** — `siblingAbort.abort('sibling-shell-error')`
1924
+ * tears down concurrently-running siblings. Mirrors the convention that
1925
+ * shell commands often chain (`mkdir foo && cd foo`); one failing
1926
+ * sibling commonly invalidates the rest. Non-shell errors are isolated.
1927
+ *
1928
+ * A child `AbortController` (`siblingAbort`) forwards the parent abort
1929
+ * AND carries the shell-cascade signal — siblings see one signal source.
1930
+ */
1931
+ async function executeToolBatch(ctx, toolCalls, turnId) {
1932
+ if (toolCalls.length === 0) return [];
1933
+ const N = toolCalls.length;
1934
+ const maxConcurrent = Math.max(1, ctx.maxConcurrentTools ?? DEFAULT_MAX_CONCURRENT_TOOLS);
1935
+ const results = Array.from({ length: N });
1936
+ const safe = Array.from({ length: N });
1937
+ for (let i = 0; i < N; i++) safe[i] = resolveConcurrencySafe(ctx.tools[toolCalls[i].name], toolCalls[i].input);
1938
+ const siblingAbort = new AbortController();
1939
+ let parentAbortListener;
1940
+ if (ctx.signal.aborted) siblingAbort.abort(ctx.signal.reason ?? "parent-aborted");
1941
+ else {
1942
+ parentAbortListener = () => siblingAbort.abort(ctx.signal.reason ?? "parent-aborted");
1943
+ ctx.signal.addEventListener("abort", parentAbortListener, { once: true });
1944
+ }
1945
+ const childCtx = {
1946
+ ...ctx,
1947
+ signal: siblingAbort.signal
1948
+ };
1949
+ /** Indices currently in flight. Tracked for fleet-safety + cap checks. */
1950
+ const inFlight = /* @__PURE__ */ new Map();
1951
+ /**
1952
+ * Distinguish a shell-cascade kill from a user-issued abort so the
1953
+ * model sees actionable text. When BOTH the parent signal and the
1954
+ * sibling signal are aborted, the parent wins — user-issued aborts
1955
+ * take precedence (the model is being interrupted by the human, not
1956
+ * by a sibling's failure).
1957
+ */
1958
+ const cancelMessage = () => {
1959
+ if (ctx.signal.aborted) return INTERRUPT_MESSAGE_FOR_TOOL_USE;
1960
+ if (siblingAbort.signal.reason === SHELL_CASCADE_REASON) return SHELL_CASCADE_CANCEL_MESSAGE;
1961
+ return INTERRUPT_MESSAGE_FOR_TOOL_USE;
1962
+ };
1963
+ const dispatch = (index) => {
1964
+ const call = toolCalls[index];
1965
+ return executeSingleTool(childCtx, call, turnId).then(({ result }) => {
1966
+ results[index] = result;
1967
+ if (result.isError && call.name === SHELL_TOOL_NAME && !siblingAbort.signal.aborted) siblingAbort.abort(SHELL_CASCADE_REASON);
1968
+ }, (err) => {
1969
+ const isAbort = siblingAbort.signal.aborted || ctx.signal.aborted || err instanceof Error && err.name === "AbortError";
1970
+ results[index] = {
1971
+ id: call.id,
1972
+ content: isAbort ? cancelMessage() : `Error: ${errorMessage(err)}`,
1973
+ isError: true
1974
+ };
1975
+ }).finally(() => {
1976
+ inFlight.delete(index);
1977
+ });
1978
+ };
1979
+ const drain = async () => {
1980
+ if (inFlight.size > 0) await Promise.all([...inFlight.values()]);
1981
+ };
1982
+ /** Whether every in-flight call is concurrency-safe. */
1983
+ const fleetAllSafe = () => {
1984
+ for (const idx of inFlight.keys()) if (!safe[idx]) return false;
1985
+ return true;
1986
+ };
1987
+ /**
1988
+ * Fill all unstarted slots (`results[j]` still undefined) with the
1989
+ * canonical text + `isError: true`. Used at every short-circuit
1990
+ * branch (abort / steer) so the assistant turn's `tool_use` IDs
1991
+ * always have matching `tool_result` IDs — providers reject orphan
1992
+ * IDs loudly.
1993
+ */
1994
+ const fillUnstarted = (from, content) => {
1995
+ for (let j = from; j < N; j++) if (!results[j]) results[j] = {
1996
+ id: toolCalls[j].id,
1997
+ content,
1910
1998
  isError: true
1911
1999
  };
1912
- });
2000
+ };
2001
+ try {
2002
+ for (let i = 0; i < N; i++) {
2003
+ if (!safe[i] || !fleetAllSafe() || inFlight.size >= maxConcurrent) await drain();
2004
+ if (ctx.signal.aborted) {
2005
+ await drain();
2006
+ fillUnstarted(i, INTERRUPT_MESSAGE_FOR_TOOL_USE);
2007
+ return results;
2008
+ }
2009
+ if (ctx.steeringQueue.length > 0) {
2010
+ await drain();
2011
+ fillUnstarted(i, TOOL_USE_SKIPPED_MESSAGE);
2012
+ return results;
2013
+ }
2014
+ inFlight.set(i, dispatch(i));
2015
+ }
2016
+ await drain();
2017
+ return results;
2018
+ } finally {
2019
+ if (parentAbortListener) ctx.signal.removeEventListener("abort", parentAbortListener);
2020
+ }
1913
2021
  }
1914
2022
  //#endregion
1915
2023
  //#region src/prompt.ts
@@ -2129,6 +2237,7 @@ function looksBinary(text, sniffBytes = SNIFF_BYTES) {
2129
2237
  function createSkillsReadTool(options) {
2130
2238
  const byName = new Map(options.catalog.map((s) => [s.name, s]));
2131
2239
  return {
2240
+ isConcurrencySafe: true,
2132
2241
  spec: {
2133
2242
  name: "skills_read",
2134
2243
  description: "Read a bundled resource file from an active skill. The skill must have been activated via skills_use first. Path is relative to the skill's directory (e.g. \"references/REFERENCE.md\", \"assets/template.txt\").",
@@ -2392,6 +2501,7 @@ function createToolSearchTool(options) {
2392
2501
  }
2393
2502
  const maxLimit = Math.max(options.catalog.length, 1);
2394
2503
  return {
2504
+ isConcurrencySafe: true,
2395
2505
  spec: {
2396
2506
  name: "tool_search",
2397
2507
  description: "Discover and load schemas for additional tools listed in <searchable_tools>. Tools listed there are advertised by name + description only — their input schemas are not loaded into context until you surface them through this tool. Pass `query` for a substring search, `names` to load specific tools, or `server` to load every tool from one MCP server. Returned tools become callable for the rest of this run.",
@@ -2607,7 +2717,7 @@ async function synthesizeMissingToolResults(turns, syntheticTurnId, runId, provi
2607
2717
  }
2608
2718
  function resolveBehavior(agentBehavior, runBehavior) {
2609
2719
  return {
2610
- toolExecution: runBehavior?.toolExecution ?? agentBehavior?.toolExecution ?? "parallel",
2720
+ maxConcurrentTools: runBehavior?.maxConcurrentTools ?? agentBehavior?.maxConcurrentTools,
2611
2721
  maxTurns: runBehavior?.maxTurns ?? agentBehavior?.maxTurns,
2612
2722
  maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens,
2613
2723
  thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
@@ -2917,7 +3027,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2917
3027
  const thinking = options.thinking ?? "off";
2918
3028
  const model = options.model ?? provider.meta.defaultModel;
2919
3029
  const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
2920
- const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, persistThreshold, persistExcludeTools, persistDir, strictToolPairing } = resolvedBehavior;
3030
+ const { maxConcurrentTools, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, persistThreshold, persistExcludeTools, persistDir, strictToolPairing } = resolvedBehavior;
2921
3031
  let system = options.system || agentSystem || "You are a helpful assistant.";
2922
3032
  if (skillsCatalog) system = `${system}\n\n${skillsCatalog}`;
2923
3033
  const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? {
@@ -3080,7 +3190,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
3080
3190
  model,
3081
3191
  system,
3082
3192
  thinking,
3083
- toolExecution,
3193
+ ...maxConcurrentTools !== void 0 ? { maxConcurrentTools } : {},
3084
3194
  signal: abortController.signal,
3085
3195
  execution: executionContext,
3086
3196
  handle: executionHandle,
@@ -3662,7 +3772,7 @@ const edit = {
3662
3772
  if (readState) {
3663
3773
  const absKey = readStateKey(ctx.handle.cwd, target);
3664
3774
  const prior = readState.get(absKey);
3665
- if (!prior) return `Edit error: ${target} has not been read in this session. Call read_file first so the edit applies against the current contents. (Reads inside a \`spawn\` subagent with \`persist: false\` and without \`shareReadState: true\` do NOT propagate to the parent — re-read in the calling context.)`;
3775
+ if (!prior) return `Edit error: ${target} has not been read in this session. Call read_file first so the edit applies against the current contents.`;
3666
3776
  if (prior.contentHash !== hashContent(original)) return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
3667
3777
  }
3668
3778
  }
@@ -3760,6 +3870,7 @@ async function globViaShell(pattern, ctx, limit) {
3760
3870
  return result.stdout.split("\n").filter((line) => line.length > 0);
3761
3871
  }
3762
3872
  const glob = {
3873
+ isConcurrencySafe: true,
3763
3874
  spec: {
3764
3875
  name: "glob",
3765
3876
  description: "Match files by glob pattern (supports **, *, ?). Relative to the execution context cwd. By default each row is `<path>\\t<size-bytes>\\t<mtime-iso>`; set `metadata: false` for a plain newline-separated list of paths. Always sorted.",
@@ -3825,6 +3936,7 @@ const glob = {
3825
3936
  const DEFAULT_HEAD_LIMIT = 250;
3826
3937
  const DEFAULT_OUTPUT_MODE = "files_with_matches";
3827
3938
  const grep = {
3939
+ isConcurrencySafe: true,
3828
3940
  spec: {
3829
3941
  name: "grep",
3830
3942
  description: "Search file contents by regex. Returns matching paths (default), match content, or per-file counts. Backed by ripgrep when available with a Bun.Glob fallback for in-process runs.",
@@ -4060,6 +4172,7 @@ function createInteractionTool(options) {
4060
4172
  //#endregion
4061
4173
  //#region src/tools/list-files.ts
4062
4174
  const listFiles = {
4175
+ isConcurrencySafe: true,
4063
4176
  spec: {
4064
4177
  name: "list_files",
4065
4178
  description: "List files and directories at the given path (relative to project root).",
@@ -4082,10 +4195,33 @@ const listFiles = {
4082
4195
  };
4083
4196
  //#endregion
4084
4197
  //#region src/tools/multi-edit.ts
4198
+ /**
4199
+ * Inline annotation builder — kept local to avoid importing from
4200
+ * `chat/edit-approval.ts` (that's a renderer-side module; tools live
4201
+ * one layer below). Line shape matches `parseEditOutcomesFromResult`'s
4202
+ * regex so the round-trip is lossless.
4203
+ *
4204
+ * Newlines in `reason` are folded to spaces because the parser is line-
4205
+ * scoped (`body.split('\n')`); a multi-line reason would split into a
4206
+ * "trailing prose" line and trip the malformed-block guard, losing every
4207
+ * outcome below it. Static reasons in this file are single-line; the
4208
+ * sanitize is a guard against a pathological `target` (file path
4209
+ * containing a newline) leaking into `old_string not found in <target>`.
4210
+ */
4211
+ function annotationFor(outcomes) {
4212
+ const lines = ["<edit-outcomes>"];
4213
+ for (let i = 0; i < outcomes.length; i++) {
4214
+ const o = outcomes[i];
4215
+ const reason = o.reason ? `: ${o.reason.replace(/\r?\n/g, " ")}` : "";
4216
+ lines.push(`#${i + 1} ${o.kind}${reason}`);
4217
+ }
4218
+ lines.push("</edit-outcomes>");
4219
+ return lines.join("\n");
4220
+ }
4085
4221
  const multiEdit = {
4086
4222
  spec: {
4087
4223
  name: "multi_edit",
4088
- description: "Apply a sequential list of edits to a file. Each edit operates on the result of the previous edit. Prefer this over multiple `edit` calls when several non-overlapping changes are needed in the same file. The batch is atomic if any edit fails to apply, none are written. Each step tolerates `read_file` line-number prefixes (`<N>\\t…`, `<N>|…`, or `<N>→…`) in `old_string` / `new_string`.",
4224
+ description: "Apply a sequential list of edits to a file. Each edit operates on the result of the previous APPLIED edit. Prefer this over multiple `edit` calls when several non-overlapping changes are needed in the same file. Edits run **best-effort**: a per-step failure (`old_string` not found, ambiguous match without `replace_all`, identical strings) is reported in the result but does NOT block the remaining steps. The file is written iff at least one step applied. The result lists per-hunk outcomes (`applied` / `failed`) so the model can re-issue just the failures without resending the whole batch. Each step tolerates `read_file` line-number prefixes (`<N>\\t…`, `<N>|…`, or `<N>→…`) in `old_string` / `new_string`.",
4089
4225
  inputSchema: {
4090
4226
  type: "object",
4091
4227
  properties: {
@@ -4095,7 +4231,7 @@ const multiEdit = {
4095
4231
  },
4096
4232
  edits: {
4097
4233
  type: "array",
4098
- description: "List of edits applied in order; each operates on the previous edit's output.",
4234
+ description: "List of edits applied in order; each operates on the previous applied edit's output.",
4099
4235
  items: {
4100
4236
  type: "object",
4101
4237
  properties: {
@@ -4125,40 +4261,88 @@ const multiEdit = {
4125
4261
  if (readState) {
4126
4262
  const absKey = readStateKey(ctx.handle.cwd, target);
4127
4263
  const prior = readState.get(absKey);
4128
- if (!prior) return `multi_edit error: ${target} has not been read in this session. Call read_file first so the edits apply against the current contents. (Reads inside a \`spawn\` subagent with \`persist: false\` and without \`shareReadState: true\` do NOT propagate to the parent — re-read in the calling context.)`;
4264
+ if (!prior) return `multi_edit error: ${target} has not been read in this session. Call read_file first so the edits apply against the current contents.`;
4129
4265
  if (prior.contentHash !== hashContent(current)) return `multi_edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
4130
4266
  }
4131
4267
  }
4268
+ const outcomes = [];
4132
4269
  let totalReplacements = 0;
4133
4270
  for (let i = 0; i < steps.length; i++) {
4134
4271
  const step = steps[i];
4135
4272
  const find = step.old_string;
4136
4273
  const replacement = step.new_string;
4137
4274
  const replaceAll = step.replace_all === true;
4138
- if (typeof find !== "string" || typeof replacement !== "string") return `multi_edit error: edit #${i + 1} is missing old_string or new_string.`;
4139
- if (find.length === 0) return `multi_edit error: edit #${i + 1} has empty old_string. Use write_file to fully replace a file.`;
4140
- if (find === replacement) return `multi_edit error: edit #${i + 1} old_string and new_string are identical.`;
4275
+ if (typeof find !== "string" || typeof replacement !== "string") {
4276
+ outcomes.push({
4277
+ kind: "failed",
4278
+ reason: "missing old_string or new_string"
4279
+ });
4280
+ continue;
4281
+ }
4282
+ if (find.length === 0) {
4283
+ outcomes.push({
4284
+ kind: "failed",
4285
+ reason: "empty old_string (use write_file to fully replace a file)"
4286
+ });
4287
+ continue;
4288
+ }
4289
+ if (find === replacement) {
4290
+ outcomes.push({
4291
+ kind: "failed",
4292
+ reason: "old_string and new_string are identical"
4293
+ });
4294
+ continue;
4295
+ }
4141
4296
  const match = resolveOldString(current, find);
4142
- if (!match) return `multi_edit error: edit #${i + 1} old_string not found in ${target}.`;
4297
+ if (!match) {
4298
+ outcomes.push({
4299
+ kind: "failed",
4300
+ reason: `old_string not found in ${target}`
4301
+ });
4302
+ continue;
4303
+ }
4143
4304
  const { actual, occurrences, via } = match;
4144
- if (occurrences > 1 && !replaceAll) return `multi_edit error: edit #${i + 1} old_string appears ${occurrences} times. Pass replace_all=true on this edit or expand old_string for uniqueness.`;
4305
+ if (occurrences > 1 && !replaceAll) {
4306
+ outcomes.push({
4307
+ kind: "failed",
4308
+ reason: `old_string appears ${occurrences} times — pass replace_all=true on this edit or expand old_string for uniqueness`
4309
+ });
4310
+ continue;
4311
+ }
4145
4312
  const styledReplacement = styleReplacementForVia(replacement, via, actual);
4146
4313
  current = replaceAll ? current.split(actual).join(styledReplacement) : current.replace(actual, styledReplacement);
4147
4314
  totalReplacements += occurrences;
4315
+ outcomes.push({ kind: "applied" });
4148
4316
  }
4149
- await ctx.execution.writeFile(ctx.handle, target, current);
4150
- const readState = resolveReadStateMap(ctx);
4151
- if (readState) {
4152
- const absKey = readStateKey(ctx.handle.cwd, target);
4153
- const prior = readState.get(absKey);
4154
- if (prior) readState.set(absKey, {
4155
- ...prior,
4156
- contentHash: hashContent(current),
4157
- mtimeMs: Date.now()
4158
- });
4317
+ const appliedCount = outcomes.reduce((n, o) => o.kind === "applied" ? n + 1 : n, 0);
4318
+ const failedCount = outcomes.length - appliedCount;
4319
+ if (appliedCount > 0) {
4320
+ await ctx.execution.writeFile(ctx.handle, target, current);
4321
+ const readState = resolveReadStateMap(ctx);
4322
+ if (readState) {
4323
+ const absKey = readStateKey(ctx.handle.cwd, target);
4324
+ const prior = readState.get(absKey);
4325
+ if (prior) readState.set(absKey, {
4326
+ ...prior,
4327
+ contentHash: hashContent(current),
4328
+ mtimeMs: Date.now()
4329
+ });
4330
+ }
4159
4331
  }
4160
4332
  const n = steps.length;
4161
- return `Edited ${target}: applied ${n} edit${n === 1 ? "" : "s"} (${totalReplacements} replacement${totalReplacements === 1 ? "" : "s"}).`;
4333
+ let header;
4334
+ if (appliedCount === n) header = `Edited ${target}: applied ${n} edit${n === 1 ? "" : "s"} (${totalReplacements} replacement${totalReplacements === 1 ? "" : "s"}).`;
4335
+ else if (appliedCount > 0) header = `Edited ${target}: applied ${appliedCount} of ${n} edits (${totalReplacements} replacement${totalReplacements === 1 ? "" : "s"}).`;
4336
+ else header = `multi_edit error: no edits applied to ${target} (${n} attempted).`;
4337
+ const failureLines = [];
4338
+ for (let i = 0; i < outcomes.length; i++) {
4339
+ const o = outcomes[i];
4340
+ if (o.kind === "failed") failureLines.push(`edit #${i + 1} failed: ${o.reason}`);
4341
+ }
4342
+ const parts = [header];
4343
+ if (failureLines.length > 0) parts.push(failureLines.join("\n"));
4344
+ if (failedCount > 0) parts.push(annotationFor(outcomes));
4345
+ return parts.join("\n\n");
4162
4346
  }
4163
4347
  };
4164
4348
  //#endregion
@@ -4244,6 +4428,7 @@ const DEFAULT_BYTE_CAP = 262144;
4244
4428
  */
4245
4429
  const DEFAULT_IMAGE_BYTE_CAP = 5 * 1024 * 1024;
4246
4430
  const readFile$1 = {
4431
+ isConcurrencySafe: true,
4247
4432
  spec: {
4248
4433
  name: "read_file",
4249
4434
  description: "Read a file by path. Returns lines [offset..offset+limit). Default offset=1, limit=2000. Each line is prefixed with its 1-indexed line number followed by a tab (e.g. `42\\tconst foo = bar`); the prefix is metadata, not part of the file. Mirrors Claude Code's `cat -n`-style compact output for token efficiency. A trailing footer explains how to read the rest when truncated. Binary files return a short marker rather than mojibake.",
@@ -4438,7 +4623,110 @@ function extractTrailingCommand(command) {
4438
4623
  * context's own default (30 s for in-process).
4439
4624
  */
4440
4625
  const DEFAULT_MAX_OUTPUT_BYTES = 32768;
4626
+ /**
4627
+ * Best-effort read-only allow-list for the leading command token. Members
4628
+ * are commands whose stock behavior cannot mutate the workspace under any
4629
+ * argument combination — `ls`, `cat`, `pwd`, etc. Commands that *can*
4630
+ * mutate depending on flags (`find -delete`, `git tag <name>`, `tar -x`)
4631
+ * are intentionally excluded; the input-aware {@link isReadOnlyShellCommand}
4632
+ * predicate falls back to the conservative "not safe" answer for them, so
4633
+ * the scheduler barriers them.
4634
+ */
4635
+ const SHELL_READ_ONLY_COMMANDS = new Set([
4636
+ "ls",
4637
+ "cat",
4638
+ "head",
4639
+ "tail",
4640
+ "wc",
4641
+ "pwd",
4642
+ "whoami",
4643
+ "id",
4644
+ "date",
4645
+ "uname",
4646
+ "hostname",
4647
+ "tty",
4648
+ "echo",
4649
+ "printf",
4650
+ "env",
4651
+ "printenv",
4652
+ "which",
4653
+ "type",
4654
+ "command",
4655
+ "file",
4656
+ "stat",
4657
+ "grep",
4658
+ "rg",
4659
+ "ag",
4660
+ "true",
4661
+ "false",
4662
+ "test"
4663
+ ]);
4664
+ /**
4665
+ * `git` subcommands that are pure reads regardless of arguments. Excludes
4666
+ * `branch`/`tag`/`remote` (which can mutate when given a name) and
4667
+ * `config` (which writes when given a value).
4668
+ */
4669
+ const GIT_READ_ONLY_SUBCOMMANDS = new Set([
4670
+ "status",
4671
+ "log",
4672
+ "diff",
4673
+ "show",
4674
+ "blame",
4675
+ "rev-parse",
4676
+ "ls-files",
4677
+ "ls-tree",
4678
+ "cat-file",
4679
+ "reflog",
4680
+ "shortlog",
4681
+ "describe",
4682
+ "rev-list",
4683
+ "name-rev",
4684
+ "whatchanged",
4685
+ "merge-base",
4686
+ "symbolic-ref"
4687
+ ]);
4688
+ /**
4689
+ * Conservative read-only verdict for a shell command — used to opt a
4690
+ * `shell` invocation into the scheduler's concurrent fleet. Returns
4691
+ * `false` (fail-closed) on anything ambiguous so the scheduler barriers
4692
+ * it. Specifically:
4693
+ *
4694
+ * - Rejects compound commands (`;`, `&&`, `||`, `|`) and redirects (`>`,
4695
+ * `>>`, `<`) — even a pipe to a read-only sink is treated as too
4696
+ * complex to analyze.
4697
+ * - Rejects subshell / process substitution (`$(...)`, `` `...` ``,
4698
+ * `<(...)`, `>(...)`).
4699
+ * - Skips leading `VAR=value` env assignments to find the real
4700
+ * command token.
4701
+ * - Strips a possible absolute path on the command (`/usr/bin/ls` → `ls`).
4702
+ * - Allows the command iff its base name is in
4703
+ * {@link SHELL_READ_ONLY_COMMANDS} OR it's `git <subcmd>` where
4704
+ * `<subcmd>` is in {@link GIT_READ_ONLY_SUBCOMMANDS}.
4705
+ *
4706
+ * Cheap (no spawned process; regex + token scan). Safe to call from the
4707
+ * hot scheduler path.
4708
+ */
4709
+ function isReadOnlyShellCommand(command) {
4710
+ if (typeof command !== "string") return false;
4711
+ const trimmed = command.trim();
4712
+ if (trimmed === "") return false;
4713
+ if (/[<>;&|`\n]/.test(trimmed)) return false;
4714
+ if (trimmed.includes("$(") || trimmed.includes("<(") || trimmed.includes(">(")) return false;
4715
+ const tokens = trimmed.split(/\s+/);
4716
+ let i = 0;
4717
+ while (i < tokens.length && /^[A-Z_]\w*=/i.test(tokens[i])) i++;
4718
+ const head = tokens[i];
4719
+ if (!head) return false;
4720
+ const base = head.split("/").pop() ?? head;
4721
+ if (SHELL_READ_ONLY_COMMANDS.has(base)) return true;
4722
+ if (base === "git") {
4723
+ const sub = tokens[i + 1];
4724
+ return typeof sub === "string" && GIT_READ_ONLY_SUBCOMMANDS.has(sub);
4725
+ }
4726
+ return false;
4727
+ }
4441
4728
  const shell = {
4729
+ isConcurrencySafe: (input) => isReadOnlyShellCommand(input.command),
4442
4730
  spec: {
4443
4731
  name: "shell",
4444
4732
  description: "Execute a shell command in the project root and return its combined stdout/stderr. Output is tail-priority truncated at 32 KiB by default; errors and exit-code summaries live in the tail. By default each call appends a `(exit N, Nms)` footer and surfaces non-empty stderr in a separate section even on success — set `metadata: false` to return only stdout. Set maxOutputBytes=0 to disable truncation.",
@@ -4715,6 +5003,7 @@ function createSpawnTool(options = {}) {
4715
5003
  get totalChildStats() {
4716
5004
  return { ...localStats };
4717
5005
  },
5006
+ isConcurrencySafe: true,
4718
5007
  spec: {
4719
5008
  name: "spawn",
4720
5009
  description: "Spawn a sub-agent for a self-contained task that benefits from isolation (separate context window, separate retries) — for example, a deep research dive or a long codegen pass on a specific file. The sub-agent runs independently with its own tool access and returns its final response. Do NOT spawn for sequential steps you could do yourself.",
@@ -4947,6 +5236,6 @@ const writeFile$1 = {
4947
5236
  }
4948
5237
  };
4949
5238
  //#endregion
4950
- export { readStateKey as A, PERSISTENCE_PREVIEW_BYTES as C, resolvePersistDir as D, maybePersistToolResult as E, getReadState as O, PERSISTED_STUB_PREFIX as S, cleanupPersistedSession as T, createSkillsReadTool as _, multiEdit as a, TOOL_USE_SKIPPED_MESSAGE as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, resolveReadStateMap as j, hashContent as k, glob as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shell as r, createInteractionTool as s, writeFile$1 as t, edit as u, INTERRUPT_MESSAGE_FOR_TOOL_USE as v, buildPersistedStub as w, validateToolArgs as x, TOOL_USE_AFTER_ERROR_MESSAGE as y };
5239
+ export { readStateKey as A, PERSISTENCE_PREVIEW_BYTES as C, resolvePersistDir as D, maybePersistToolResult as E, getReadState as O, PERSISTED_STUB_PREFIX as S, cleanupPersistedSession as T, createSkillsReadTool as _, multiEdit as a, TOOL_USE_SKIPPED_MESSAGE as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, resolveReadStateMap as j, hashContent as k, glob as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shell as r, createInteractionTool as s, writeFile$1 as t, edit as u, INTERRUPT_MESSAGE_FOR_TOOL_USE as v, buildPersistedStub as w, validateToolArgs as x, SHELL_CASCADE_CANCEL_MESSAGE as y };
4951
5240
 
4952
- //# sourceMappingURL=tools-CCsL5SCO.js.map
5241
+ //# sourceMappingURL=tools-PQH1Ge4M.js.map