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.
- package/README.md +16 -1
- package/dist/{agent-BXRCCHeq.d.ts → agent-DHQAsdj6.d.ts} +47 -15
- package/dist/agent-DHQAsdj6.d.ts.map +1 -0
- package/dist/chat.d.ts +62 -5
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/{index-CT5_p-3P.d.ts → index-CHSaLab5.d.ts} +2 -2
- package/dist/{index-CT5_p-3P.d.ts.map → index-CHSaLab5.d.ts.map} +1 -1
- package/dist/{index-BPk8-Slm.d.ts → index-CrqFoaQA.d.ts} +12 -11
- package/dist/index-CrqFoaQA.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/{login-DrBZ15G7.js → login-8c5C0FYq.js} +2 -2
- package/dist/{login-DrBZ15G7.js.map → login-8c5C0FYq.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/{presets-0_IRJAYF.js → presets-Ck4VusTo.js} +2 -2
- package/dist/{presets-0_IRJAYF.js.map → presets-Ck4VusTo.js.map} +1 -1
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/skills.d.ts +2 -2
- package/dist/{tools-CCsL5SCO.js → tools-PQH1Ge4M.js} +373 -84
- package/dist/tools-PQH1Ge4M.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-DSk8LlWt.d.ts → transcript-anchors-ByB2MSCB.d.ts} +19 -4
- package/dist/transcript-anchors-ByB2MSCB.d.ts.map +1 -0
- package/dist/tui.d.ts +3 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +52 -23
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-CutZin8X.js → turn-operations-Bqs4YbbH.js} +129 -4
- package/dist/turn-operations-Bqs4YbbH.js.map +1 -0
- package/dist/types-IcokUOyC.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/docs/ARCHITECTURE.md +23 -11
- package/docs/CHAT.md +21 -3
- package/docs/SKILL.md +34 -3
- package/docs/TUI.md +2 -2
- package/package.json +1 -1
- package/dist/agent-BXRCCHeq.d.ts.map +0 -1
- package/dist/index-BPk8-Slm.d.ts.map +0 -1
- package/dist/tools-CCsL5SCO.js.map +0 -1
- package/dist/transcript-anchors-DSk8LlWt.d.ts.map +0 -1
- 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
|
-
*
|
|
809
|
-
*
|
|
810
|
-
* {@link INTERRUPT_MESSAGE_FOR_TOOL_USE} so consumers can
|
|
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 =
|
|
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
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
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
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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")
|
|
4139
|
-
|
|
4140
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
4150
|
-
const
|
|
4151
|
-
if (
|
|
4152
|
-
|
|
4153
|
-
const
|
|
4154
|
-
if (
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
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
|
-
|
|
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,
|
|
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-
|
|
5241
|
+
//# sourceMappingURL=tools-PQH1Ge4M.js.map
|