zidane 5.3.1 → 5.4.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/README.md +18 -1
- package/dist/{agent-bKs7MRT2.d.ts → agent-DHQAsdj6.d.ts} +174 -15
- package/dist/agent-DHQAsdj6.d.ts.map +1 -0
- package/dist/chat.d.ts +4 -4
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +1 -1
- package/dist/{index-CTmNaIDb.d.ts → index-CHSaLab5.d.ts} +2 -2
- package/dist/{index-CTmNaIDb.d.ts.map → index-CHSaLab5.d.ts.map} +1 -1
- package/dist/{index-BlMvPh9X.d.ts → index-CrqFoaQA.d.ts} +36 -11
- package/dist/index-CrqFoaQA.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- package/dist/{login-CNS9_8Ue.js → login-bK0EP8La.js} +3 -3
- package/dist/{login-CNS9_8Ue.js.map → login-bK0EP8La.js.map} +1 -1
- package/dist/{mcp-ZsSFo4Dp.js → mcp-DhmmJfxK.js} +15 -2
- package/dist/mcp-DhmmJfxK.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{presets-h5i3kpOP.js → presets-M8f6lDnW.js} +2 -2
- package/dist/{presets-h5i3kpOP.js.map → presets-M8f6lDnW.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-CWEDS2ZT.js → tools-DKdyPoUf.js} +425 -165
- package/dist/tools-DKdyPoUf.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +2 -2
- package/dist/{transcript-anchors-DOUqyvXR.d.ts → transcript-anchors-Fgh_rZ04.d.ts} +3 -3
- package/dist/{transcript-anchors-DOUqyvXR.d.ts.map → transcript-anchors-Fgh_rZ04.d.ts.map} +1 -1
- package/dist/tui.d.ts +2 -2
- package/dist/tui.js +4 -4
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-D9HvatsR.js → turn-operations-DDokWR8p.js} +5 -4
- package/dist/turn-operations-DDokWR8p.js.map +1 -0
- package/dist/types-IcokUOyC.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/docs/ARCHITECTURE.md +18 -7
- package/docs/SKILL.md +56 -3
- package/package.json +1 -1
- package/dist/agent-bKs7MRT2.d.ts.map +0 -1
- package/dist/index-BlMvPh9X.d.ts.map +0 -1
- package/dist/mcp-ZsSFo4Dp.js.map +0 -1
- package/dist/tools-CWEDS2ZT.js.map +0 -1
- package/dist/turn-operations-D9HvatsR.js.map +0 -1
|
@@ -2,7 +2,7 @@ import { n as createProcessContext } from "./contexts-BwiHIr2w.js";
|
|
|
2
2
|
import { a as AgentToolPairingError, l as toTypedError, r as AgentProviderError, s as errorMessage, t as AgentAbortedError } from "./errors-Byb0F8B9.js";
|
|
3
3
|
import { t as toolOutputByteLength } from "./types-IcokUOyC.js";
|
|
4
4
|
import { a as detectTurnInterruption, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureToolResultPairing, s as filterUnresolvedToolUses } from "./messages-D0xT979U.js";
|
|
5
|
-
import { t as connectMcpServers } from "./mcp-
|
|
5
|
+
import { t as connectMcpServers } from "./mcp-DhmmJfxK.js";
|
|
6
6
|
import { _ as validateResourcePath, b as createSkillActivationState, d as escapeXml, n as resolveSkills, p as installAllowedToolsGate, t as interpolateShellCommands, u as buildCatalog } from "./interpolate-ERgZUxgg.js";
|
|
7
7
|
import { n as formatTokenUsage, t as flattenTurns } from "./stats-DgOvY7wd.js";
|
|
8
8
|
import { createHooks } from "hookable";
|
|
@@ -107,8 +107,14 @@ function rewriteMessagesToWire(messages, maps) {
|
|
|
107
107
|
const STATE = /* @__PURE__ */ new WeakMap();
|
|
108
108
|
/**
|
|
109
109
|
* Get or lazily create the per-session read-state map. Returns `undefined`
|
|
110
|
-
* when no session is provided
|
|
111
|
-
*
|
|
110
|
+
* when no session is provided.
|
|
111
|
+
*
|
|
112
|
+
* Most tool callers should prefer {@link resolveReadStateMap}, which
|
|
113
|
+
* additionally honors an explicit `ctx.readState` (the path
|
|
114
|
+
* `spawn`'s `shareReadState: true` uses to forward the parent's map
|
|
115
|
+
* into a sessionless child). Use this helper directly only when the
|
|
116
|
+
* Session-keyed map is exactly what you want and you don't need to
|
|
117
|
+
* accept an override.
|
|
112
118
|
*/
|
|
113
119
|
function getReadState(session) {
|
|
114
120
|
if (!session) return void 0;
|
|
@@ -119,19 +125,38 @@ function getReadState(session) {
|
|
|
119
125
|
}
|
|
120
126
|
return map;
|
|
121
127
|
}
|
|
122
|
-
const TOOL_DEDUP_STATE = /* @__PURE__ */ new WeakMap();
|
|
123
128
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
129
|
+
* Resolve the active read-state map from a tool context. An explicit
|
|
130
|
+
* `ctx.readState` wins (used by `spawn`'s `shareReadState` opt-in to
|
|
131
|
+
* forward the parent's map into a sessionless child); otherwise the
|
|
132
|
+
* usual `Session`-keyed lookup applies.
|
|
133
|
+
*
|
|
134
|
+
* Tools should call this helper instead of `getReadState(ctx.session)`
|
|
135
|
+
* directly so the `shareReadState` plumbing is honored uniformly.
|
|
126
136
|
*/
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
function resolveReadStateMap(ctx) {
|
|
138
|
+
return ctx.readState ?? getReadState(ctx.session);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Canonical read-state key for a `(cwd, path)` pair.
|
|
142
|
+
*
|
|
143
|
+
* `ctx.execution.readFile(handle, path)` resolves the model-provided
|
|
144
|
+
* path against `handle.cwd` before touching disk — so `src/App.tsx`,
|
|
145
|
+
* `./src/App.tsx`, and `/abs/cwd/src/App.tsx` all read the **same**
|
|
146
|
+
* bytes. The read-state map MUST mirror that resolution: without it,
|
|
147
|
+
* a model that reads `src/App.tsx` and then edits `./src/App.tsx`
|
|
148
|
+
* trips the `requireReadBeforeEdit` gate for a file it demonstrably
|
|
149
|
+
* already saw (the gate's "has not been read in this session" message
|
|
150
|
+
* fires on a stale key).
|
|
151
|
+
*
|
|
152
|
+
* `node:path`'s `resolve(cwd, path)` short-circuits when `path` is
|
|
153
|
+
* already absolute, so absolute and relative shapes converge on the
|
|
154
|
+
* same canonical form. We don't `realpath()` symlinks — the file IS
|
|
155
|
+
* the path the model addressed, not the realpath behind it; the host's
|
|
156
|
+
* execution context decides how to dereference.
|
|
157
|
+
*/
|
|
158
|
+
function readStateKey(cwd, path) {
|
|
159
|
+
return resolve(cwd, path);
|
|
135
160
|
}
|
|
136
161
|
/**
|
|
137
162
|
* FNV-1a 32-bit hash, hex-encoded. Fast, non-cryptographic — we only need
|
|
@@ -147,6 +172,20 @@ function hashContent(text) {
|
|
|
147
172
|
}
|
|
148
173
|
return h.toString(16).padStart(8, "0");
|
|
149
174
|
}
|
|
175
|
+
const TOOL_DEDUP_STATE = /* @__PURE__ */ new WeakMap();
|
|
176
|
+
/**
|
|
177
|
+
* Get or lazily create the per-session tool-dedup map. Returns `undefined`
|
|
178
|
+
* when no session is provided — middleware should treat that as "no dedup".
|
|
179
|
+
*/
|
|
180
|
+
function getToolDedupState(session) {
|
|
181
|
+
if (!session) return void 0;
|
|
182
|
+
let map = TOOL_DEDUP_STATE.get(session);
|
|
183
|
+
if (!map) {
|
|
184
|
+
map = /* @__PURE__ */ new Map();
|
|
185
|
+
TOOL_DEDUP_STATE.set(session, map);
|
|
186
|
+
}
|
|
187
|
+
return map;
|
|
188
|
+
}
|
|
150
189
|
//#endregion
|
|
151
190
|
//#region src/dedup-tools.ts
|
|
152
191
|
/**
|
|
@@ -766,20 +805,13 @@ const IMAGE_OMITTED_MARKER = "[image omitted — model does not support vision]"
|
|
|
766
805
|
const INTERRUPT_MESSAGE_FOR_TOOL_USE = "[Request interrupted by user for tool use]";
|
|
767
806
|
/**
|
|
768
807
|
* Canonical tool_result text emitted when a tool call is skipped because a
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
* {@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
|
|
772
811
|
* cancelled" from "framework superseded".
|
|
773
812
|
*/
|
|
774
813
|
const TOOL_USE_SKIPPED_MESSAGE = "[Tool use skipped — superseded by user message]";
|
|
775
814
|
/**
|
|
776
|
-
* Canonical tool_result text emitted when the loop catches a sequential
|
|
777
|
-
* sibling that threw and synthesizes follow-up results for the remaining
|
|
778
|
-
* queued calls. Distinct from {@link TOOL_USE_SKIPPED_MESSAGE} so telemetry
|
|
779
|
-
* can split "skipped by user steering" from "skipped after error".
|
|
780
|
-
*/
|
|
781
|
-
const TOOL_USE_AFTER_ERROR_MESSAGE = "[Tool use skipped — previous tool call in batch threw]";
|
|
782
|
-
/**
|
|
783
815
|
* Compute the effective thinking budget for a given run-relative turn, given
|
|
784
816
|
* the configured decay schedule. Pure helper — exported for tests and so
|
|
785
817
|
* downstream tooling can preview decay curves without spinning up the loop.
|
|
@@ -1067,19 +1099,21 @@ function applyStaleReadElision(messages) {
|
|
|
1067
1099
|
}
|
|
1068
1100
|
/**
|
|
1069
1101
|
* Drop read-state entries for paths whose reads got elided. Keys are
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1102
|
+
* canonical absolute paths produced by `readStateKey(cwd, path)` (see
|
|
1103
|
+
* `src/tools/read-state.ts`); the loop has `ctx.handle.cwd` in scope,
|
|
1104
|
+
* so we re-derive each elided path's canonical key and delete by
|
|
1105
|
+
* direct lookup. No suffix matching, no cross-cwd ambiguity.
|
|
1106
|
+
*
|
|
1107
|
+
* Resolves the map via `resolveReadStateMap` so a child agent running
|
|
1108
|
+
* with a parent's shared map (via `shareReadState`) invalidates the
|
|
1109
|
+
* shared entries too — otherwise a child's `read → edit → re-read`
|
|
1110
|
+
* would dedup-hit on the now-stale parent entry.
|
|
1076
1111
|
*/
|
|
1077
|
-
function invalidateReadStateForElidedPaths(
|
|
1078
|
-
if (
|
|
1079
|
-
const readState =
|
|
1112
|
+
function invalidateReadStateForElidedPaths(ctx, cwd, elidedPaths) {
|
|
1113
|
+
if (elidedPaths.length === 0) return;
|
|
1114
|
+
const readState = resolveReadStateMap(ctx);
|
|
1080
1115
|
if (!readState || readState.size === 0) return;
|
|
1081
|
-
const
|
|
1082
|
-
for (const key of [...readState.keys()]) if (suffixes.some((s) => key.endsWith(s))) readState.delete(key);
|
|
1116
|
+
for (const p of elidedPaths) readState.delete(readStateKey(cwd, p));
|
|
1083
1117
|
}
|
|
1084
1118
|
/**
|
|
1085
1119
|
* Run {@link ensureToolResultPairing} with the loop's hook + strict-mode
|
|
@@ -1265,7 +1299,7 @@ async function executeTurn(ctx, turn) {
|
|
|
1265
1299
|
if (ctx.elideStaleReads === true) {
|
|
1266
1300
|
const elision = applyStaleReadElision(canonicalMessages);
|
|
1267
1301
|
canonicalMessages = elision.messages;
|
|
1268
|
-
invalidateReadStateForElidedPaths(ctx.
|
|
1302
|
+
invalidateReadStateForElidedPaths(ctx, ctx.handle.cwd, elision.elidedPaths);
|
|
1269
1303
|
}
|
|
1270
1304
|
const wireMessages = rewriteMessagesToWire(canonicalMessages, ctx.aliasMaps);
|
|
1271
1305
|
let sanitizedMessages = sanitizeStoredToolResults(ctx.provider, wireMessages);
|
|
@@ -1477,7 +1511,7 @@ async function executeTurn(ctx, turn) {
|
|
|
1477
1511
|
usage: result.usage
|
|
1478
1512
|
};
|
|
1479
1513
|
}
|
|
1480
|
-
const toolResults =
|
|
1514
|
+
const toolResults = await executeToolBatch(ctx, canonicalToolCalls, turnId);
|
|
1481
1515
|
const toolResultMsg = ctx.provider.toolResultsMessage(toolResults);
|
|
1482
1516
|
const toolResultsTurn = {
|
|
1483
1517
|
id: await ctx.generateTurnId(),
|
|
@@ -1539,17 +1573,35 @@ function stripImagesForNonVision(provider, output) {
|
|
|
1539
1573
|
if (provider.meta.capabilities?.vision !== false) return output;
|
|
1540
1574
|
return output.map((b) => b.type === "image" ? IMAGE_OMITTED_MARKER : b.text).join("\n");
|
|
1541
1575
|
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Build the per-call base for every `tool:*` hook ctx (and the
|
|
1578
|
+
* matching shape for `mcp:tool:*`). Centralized so the `runId` /
|
|
1579
|
+
* `parentRunId` / `depth` identity fields land uniformly on every
|
|
1580
|
+
* event the loop fires — without one helper they drift across ~14
|
|
1581
|
+
* inline construction sites. The returned object IS the
|
|
1582
|
+
* {@link ToolHookContext} canonical shape; specialized hook payloads
|
|
1583
|
+
* (`gateCtx`, `transformCtx`, etc.) spread it and append their own
|
|
1584
|
+
* fields.
|
|
1585
|
+
*/
|
|
1586
|
+
function buildToolHookBase(ctx, turnId, callId, name, displayName, input) {
|
|
1587
|
+
return {
|
|
1588
|
+
turnId,
|
|
1589
|
+
callId,
|
|
1590
|
+
name,
|
|
1591
|
+
displayName,
|
|
1592
|
+
input,
|
|
1593
|
+
...ctx.runId !== void 0 ? { runId: ctx.runId } : {},
|
|
1594
|
+
...ctx.parentRunId !== void 0 ? { parentRunId: ctx.parentRunId } : {},
|
|
1595
|
+
...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1542
1598
|
async function executeSingleTool(ctx, call, turnId) {
|
|
1543
1599
|
const toolDef = ctx.tools[call.name];
|
|
1544
1600
|
const callId = call.id;
|
|
1545
1601
|
const displayName = toWireName(call.name, ctx.aliasMaps);
|
|
1546
1602
|
const runToolCounts = Object.freeze({ ...ctx.runToolCounts });
|
|
1547
1603
|
const gateCtx = {
|
|
1548
|
-
turnId,
|
|
1549
|
-
callId,
|
|
1550
|
-
name: call.name,
|
|
1551
|
-
displayName,
|
|
1552
|
-
input: call.input,
|
|
1604
|
+
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, call.input),
|
|
1553
1605
|
block: false,
|
|
1554
1606
|
reason: "Tool execution was blocked",
|
|
1555
1607
|
runToolCounts
|
|
@@ -1602,11 +1654,7 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1602
1654
|
let effectiveInput = gateCtx.input;
|
|
1603
1655
|
if (!toolDef) {
|
|
1604
1656
|
const unknownCtx = {
|
|
1605
|
-
turnId,
|
|
1606
|
-
callId,
|
|
1607
|
-
name: call.name,
|
|
1608
|
-
displayName,
|
|
1609
|
-
input: effectiveInput,
|
|
1657
|
+
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
|
|
1610
1658
|
suppressError: false
|
|
1611
1659
|
};
|
|
1612
1660
|
await ctx.hooks.callHook("tool:unknown", unknownCtx);
|
|
@@ -1615,11 +1663,7 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1615
1663
|
if (!unknownCtx.suppressError) {
|
|
1616
1664
|
const err = /* @__PURE__ */ new Error(`Unknown tool: ${call.name}`);
|
|
1617
1665
|
await ctx.hooks.callHook("tool:error", {
|
|
1618
|
-
turnId,
|
|
1619
|
-
callId,
|
|
1620
|
-
name: call.name,
|
|
1621
|
-
displayName,
|
|
1622
|
-
input: effectiveInput,
|
|
1666
|
+
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
|
|
1623
1667
|
error: err
|
|
1624
1668
|
});
|
|
1625
1669
|
}
|
|
@@ -1641,11 +1685,7 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1641
1685
|
const validation = validateToolArgs(effectiveInput, toolDef.spec.inputSchema);
|
|
1642
1686
|
if (!validation.valid) {
|
|
1643
1687
|
await ctx.hooks.callHook("validation:reject", {
|
|
1644
|
-
turnId,
|
|
1645
|
-
callId,
|
|
1646
|
-
name: call.name,
|
|
1647
|
-
displayName,
|
|
1648
|
-
input: effectiveInput,
|
|
1688
|
+
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
|
|
1649
1689
|
reason: validation.error ?? "invalid input",
|
|
1650
1690
|
schema: toolDef.spec.inputSchema
|
|
1651
1691
|
});
|
|
@@ -1667,11 +1707,7 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1667
1707
|
effectiveInput = validation.coercedInput ?? effectiveInput;
|
|
1668
1708
|
const coercions = validation.coercions && validation.coercions.length > 0 ? validation.coercions : void 0;
|
|
1669
1709
|
if (coercions) await ctx.hooks.callHook("validation:coerce", {
|
|
1670
|
-
turnId,
|
|
1671
|
-
callId,
|
|
1672
|
-
name: call.name,
|
|
1673
|
-
displayName,
|
|
1674
|
-
input: effectiveInput,
|
|
1710
|
+
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
|
|
1675
1711
|
coercions,
|
|
1676
1712
|
schema: toolDef.spec.inputSchema
|
|
1677
1713
|
});
|
|
@@ -1685,11 +1721,7 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1685
1721
|
runToolCounts
|
|
1686
1722
|
});
|
|
1687
1723
|
await ctx.hooks.callHook("tool:before", {
|
|
1688
|
-
turnId,
|
|
1689
|
-
callId,
|
|
1690
|
-
name: call.name,
|
|
1691
|
-
displayName,
|
|
1692
|
-
input: effectiveInput,
|
|
1724
|
+
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
|
|
1693
1725
|
runToolCounts,
|
|
1694
1726
|
...coercions ? { coercions } : {}
|
|
1695
1727
|
});
|
|
@@ -1712,18 +1744,16 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1712
1744
|
turnId,
|
|
1713
1745
|
callId,
|
|
1714
1746
|
runId: ctx.runId,
|
|
1747
|
+
...ctx.parentRunId !== void 0 ? { parentRunId: ctx.parentRunId } : {},
|
|
1715
1748
|
...ctx.session ? { session: ctx.session } : {},
|
|
1749
|
+
...ctx.readState ? { readState: ctx.readState } : {},
|
|
1716
1750
|
...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
|
|
1717
1751
|
};
|
|
1718
1752
|
output = await toolDef.execute(effectiveInput, toolCtx);
|
|
1719
1753
|
} catch (err) {
|
|
1720
1754
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1721
1755
|
const errorCtx = {
|
|
1722
|
-
turnId,
|
|
1723
|
-
callId,
|
|
1724
|
-
name: call.name,
|
|
1725
|
-
displayName,
|
|
1726
|
-
input: effectiveInput,
|
|
1756
|
+
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
|
|
1727
1757
|
error
|
|
1728
1758
|
};
|
|
1729
1759
|
await ctx.hooks.callHook("tool:error", errorCtx);
|
|
@@ -1761,9 +1791,11 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1761
1791
|
* across five call sites.
|
|
1762
1792
|
*/
|
|
1763
1793
|
async function fireDispatched(ctx, params) {
|
|
1764
|
-
const { reason,
|
|
1794
|
+
const { turnId, callId, name, displayName, input, outcome, reason, runToolCounts } = params;
|
|
1765
1795
|
await ctx.hooks.callHook("tool:dispatched", {
|
|
1766
|
-
...
|
|
1796
|
+
...buildToolHookBase(ctx, turnId, callId, name, displayName, input),
|
|
1797
|
+
outcome,
|
|
1798
|
+
runToolCounts,
|
|
1767
1799
|
...reason !== void 0 ? { reason } : {}
|
|
1768
1800
|
});
|
|
1769
1801
|
}
|
|
@@ -1784,11 +1816,7 @@ async function emitToolResult(ctx, params) {
|
|
|
1784
1816
|
let output = params.output;
|
|
1785
1817
|
let isError = params.isError;
|
|
1786
1818
|
const transformCtx = {
|
|
1787
|
-
turnId,
|
|
1788
|
-
callId,
|
|
1789
|
-
name,
|
|
1790
|
-
displayName,
|
|
1791
|
-
input,
|
|
1819
|
+
...buildToolHookBase(ctx, turnId, callId, name, displayName, input),
|
|
1792
1820
|
result: output,
|
|
1793
1821
|
isError,
|
|
1794
1822
|
outputBytes: toolOutputByteLength(output),
|
|
@@ -1813,11 +1841,7 @@ async function emitToolResult(ctx, params) {
|
|
|
1813
1841
|
}
|
|
1814
1842
|
output = stripImagesForNonVision(ctx.provider, output);
|
|
1815
1843
|
await ctx.hooks.callHook("tool:after", {
|
|
1816
|
-
turnId,
|
|
1817
|
-
callId,
|
|
1818
|
-
name,
|
|
1819
|
-
displayName,
|
|
1820
|
-
input,
|
|
1844
|
+
...buildToolHookBase(ctx, turnId, callId, name, displayName, input),
|
|
1821
1845
|
result: output,
|
|
1822
1846
|
outputBytes: toolOutputByteLength(output),
|
|
1823
1847
|
runToolCounts,
|
|
@@ -1828,57 +1852,170 @@ async function emitToolResult(ctx, params) {
|
|
|
1828
1852
|
isError
|
|
1829
1853
|
};
|
|
1830
1854
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1855
|
+
/** Default cap on in-flight tools per turn. Mirrors Claude Code's `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY`. */
|
|
1856
|
+
const DEFAULT_MAX_CONCURRENT_TOOLS = 10;
|
|
1857
|
+
/** Canonical name of the shell tool — referenced for cascade-cancel semantics. */
|
|
1858
|
+
const SHELL_TOOL_NAME = "shell";
|
|
1859
|
+
/** Reason surfaced on `siblingAbort.signal` when a shell error cancels its fleet. */
|
|
1860
|
+
const SHELL_CASCADE_REASON = "sibling-shell-error";
|
|
1861
|
+
/**
|
|
1862
|
+
* Canonical `tool_result.content` text emitted to siblings that were
|
|
1863
|
+
* cancelled by a `shell` error in the same batch. Distinct from
|
|
1864
|
+
* {@link INTERRUPT_MESSAGE_FOR_TOOL_USE} (user-issued abort) and
|
|
1865
|
+
* {@link TOOL_USE_SKIPPED_MESSAGE} (steered) so consumers can split
|
|
1866
|
+
* the three causes by string-match.
|
|
1867
|
+
*/
|
|
1868
|
+
const SHELL_CASCADE_CANCEL_MESSAGE = "Cancelled: a sibling `shell` call in the same batch errored; re-run independently if still needed.";
|
|
1869
|
+
/**
|
|
1870
|
+
* Resolve a tool's concurrency-safety verdict for a specific call.
|
|
1871
|
+
*
|
|
1872
|
+
* - Missing toolDef (unknown tool) → `false`. `executeSingleTool` handles
|
|
1873
|
+
* the unknown-tool path itself; barriering it keeps the unknown-tool
|
|
1874
|
+
* error from racing with siblings.
|
|
1875
|
+
* - Static `true` / `false` → use as-is.
|
|
1876
|
+
* - Function → invoke; any throw is treated as `false` (fail-closed) so a
|
|
1877
|
+
* buggy predicate can't accidentally widen the safety window.
|
|
1878
|
+
*
|
|
1879
|
+
* Pure / sync — pre-computed once per call before dispatch begins, so the
|
|
1880
|
+
* scheduler's hot path stays branch-light.
|
|
1881
|
+
*/
|
|
1882
|
+
function resolveConcurrencySafe(def, input) {
|
|
1883
|
+
if (!def) return false;
|
|
1884
|
+
const flag = def.isConcurrencySafe;
|
|
1885
|
+
if (flag === void 0) return false;
|
|
1886
|
+
if (typeof flag === "boolean") return flag;
|
|
1887
|
+
try {
|
|
1888
|
+
return flag(input) === true;
|
|
1889
|
+
} catch {
|
|
1890
|
+
return false;
|
|
1867
1891
|
}
|
|
1868
|
-
return results;
|
|
1869
1892
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1893
|
+
/**
|
|
1894
|
+
* Unified per-turn tool dispatcher.
|
|
1895
|
+
*
|
|
1896
|
+
* Walks `toolCalls` in submission order. For each call:
|
|
1897
|
+
*
|
|
1898
|
+
* - **Concurrency-safe + fleet is all safe + room under the cap** → fires
|
|
1899
|
+
* asynchronously and the loop advances to the next call. The fleet runs
|
|
1900
|
+
* in parallel up to `behavior.maxConcurrentTools` (default {@link
|
|
1901
|
+
* DEFAULT_MAX_CONCURRENT_TOOLS}).
|
|
1902
|
+
* - **Unsafe** (or the in-flight fleet contains anything unsafe) → acts
|
|
1903
|
+
* as a barrier: waits for the fleet to drain, then runs alone, then
|
|
1904
|
+
* unblocks the queue.
|
|
1905
|
+
*
|
|
1906
|
+
* Results are written into a fixed `results[index]` array on completion
|
|
1907
|
+
* and yielded back in submission order, so the model sees deterministic
|
|
1908
|
+
* adjacency regardless of which call finished first.
|
|
1909
|
+
*
|
|
1910
|
+
* **Failure modes:**
|
|
1911
|
+
*
|
|
1912
|
+
* - **Hook throws / tool body throws** — captured per-call into a
|
|
1913
|
+
* `tool_result` so the assistant turn's `tool_use` IDs always have
|
|
1914
|
+
* matching `tool_result` IDs (providers reject orphan IDs).
|
|
1915
|
+
* - **Parent abort** mid-batch — drains the in-flight fleet (their
|
|
1916
|
+
* `AbortError` becomes `INTERRUPT_MESSAGE_FOR_TOOL_USE`), then synthesizes
|
|
1917
|
+
* interrupt results for any unstarted calls so the turn closes cleanly.
|
|
1918
|
+
* - **Steering queue populated** between dispatches — same drain + a
|
|
1919
|
+
* `TOOL_USE_SKIPPED_MESSAGE` result for unstarted calls. The outer loop
|
|
1920
|
+
* picks up the steer at the next checkpoint.
|
|
1921
|
+
* - **Shell error in a fleet** — `siblingAbort.abort('sibling-shell-error')`
|
|
1922
|
+
* tears down concurrently-running siblings. Mirrors the convention that
|
|
1923
|
+
* shell commands often chain (`mkdir foo && cd foo`); one failing
|
|
1924
|
+
* sibling commonly invalidates the rest. Non-shell errors are isolated.
|
|
1925
|
+
*
|
|
1926
|
+
* A child `AbortController` (`siblingAbort`) forwards the parent abort
|
|
1927
|
+
* AND carries the shell-cascade signal — siblings see one signal source.
|
|
1928
|
+
*/
|
|
1929
|
+
async function executeToolBatch(ctx, toolCalls, turnId) {
|
|
1930
|
+
if (toolCalls.length === 0) return [];
|
|
1931
|
+
const N = toolCalls.length;
|
|
1932
|
+
const maxConcurrent = Math.max(1, ctx.maxConcurrentTools ?? DEFAULT_MAX_CONCURRENT_TOOLS);
|
|
1933
|
+
const results = Array.from({ length: N });
|
|
1934
|
+
const safe = Array.from({ length: N });
|
|
1935
|
+
for (let i = 0; i < N; i++) safe[i] = resolveConcurrencySafe(ctx.tools[toolCalls[i].name], toolCalls[i].input);
|
|
1936
|
+
const siblingAbort = new AbortController();
|
|
1937
|
+
let parentAbortListener;
|
|
1938
|
+
if (ctx.signal.aborted) siblingAbort.abort(ctx.signal.reason ?? "parent-aborted");
|
|
1939
|
+
else {
|
|
1940
|
+
parentAbortListener = () => siblingAbort.abort(ctx.signal.reason ?? "parent-aborted");
|
|
1941
|
+
ctx.signal.addEventListener("abort", parentAbortListener, { once: true });
|
|
1942
|
+
}
|
|
1943
|
+
const childCtx = {
|
|
1944
|
+
...ctx,
|
|
1945
|
+
signal: siblingAbort.signal
|
|
1946
|
+
};
|
|
1947
|
+
/** Indices currently in flight. Tracked for fleet-safety + cap checks. */
|
|
1948
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1949
|
+
/**
|
|
1950
|
+
* Distinguish a shell-cascade kill from a user-issued abort so the
|
|
1951
|
+
* model sees actionable text. When BOTH the parent signal and the
|
|
1952
|
+
* sibling signal are aborted, the parent wins — user-issued aborts
|
|
1953
|
+
* take precedence (the model is being interrupted by the human, not
|
|
1954
|
+
* by a sibling's failure).
|
|
1955
|
+
*/
|
|
1956
|
+
const cancelMessage = () => {
|
|
1957
|
+
if (ctx.signal.aborted) return INTERRUPT_MESSAGE_FOR_TOOL_USE;
|
|
1958
|
+
if (siblingAbort.signal.reason === SHELL_CASCADE_REASON) return SHELL_CASCADE_CANCEL_MESSAGE;
|
|
1959
|
+
return INTERRUPT_MESSAGE_FOR_TOOL_USE;
|
|
1960
|
+
};
|
|
1961
|
+
const dispatch = (index) => {
|
|
1962
|
+
const call = toolCalls[index];
|
|
1963
|
+
return executeSingleTool(childCtx, call, turnId).then(({ result }) => {
|
|
1964
|
+
results[index] = result;
|
|
1965
|
+
if (result.isError && call.name === SHELL_TOOL_NAME && !siblingAbort.signal.aborted) siblingAbort.abort(SHELL_CASCADE_REASON);
|
|
1966
|
+
}, (err) => {
|
|
1967
|
+
const isAbort = siblingAbort.signal.aborted || ctx.signal.aborted || err instanceof Error && err.name === "AbortError";
|
|
1968
|
+
results[index] = {
|
|
1969
|
+
id: call.id,
|
|
1970
|
+
content: isAbort ? cancelMessage() : `Error: ${errorMessage(err)}`,
|
|
1971
|
+
isError: true
|
|
1972
|
+
};
|
|
1973
|
+
}).finally(() => {
|
|
1974
|
+
inFlight.delete(index);
|
|
1975
|
+
});
|
|
1976
|
+
};
|
|
1977
|
+
const drain = async () => {
|
|
1978
|
+
if (inFlight.size > 0) await Promise.all([...inFlight.values()]);
|
|
1979
|
+
};
|
|
1980
|
+
/** Whether every in-flight call is concurrency-safe. */
|
|
1981
|
+
const fleetAllSafe = () => {
|
|
1982
|
+
for (const idx of inFlight.keys()) if (!safe[idx]) return false;
|
|
1983
|
+
return true;
|
|
1984
|
+
};
|
|
1985
|
+
/**
|
|
1986
|
+
* Fill all unstarted slots (`results[j]` still undefined) with the
|
|
1987
|
+
* canonical text + `isError: true`. Used at every short-circuit
|
|
1988
|
+
* branch (abort / steer) so the assistant turn's `tool_use` IDs
|
|
1989
|
+
* always have matching `tool_result` IDs — providers reject orphan
|
|
1990
|
+
* IDs loudly.
|
|
1991
|
+
*/
|
|
1992
|
+
const fillUnstarted = (from, content) => {
|
|
1993
|
+
for (let j = from; j < N; j++) if (!results[j]) results[j] = {
|
|
1994
|
+
id: toolCalls[j].id,
|
|
1995
|
+
content,
|
|
1879
1996
|
isError: true
|
|
1880
1997
|
};
|
|
1881
|
-
}
|
|
1998
|
+
};
|
|
1999
|
+
try {
|
|
2000
|
+
for (let i = 0; i < N; i++) {
|
|
2001
|
+
if (!safe[i] || !fleetAllSafe() || inFlight.size >= maxConcurrent) await drain();
|
|
2002
|
+
if (ctx.signal.aborted) {
|
|
2003
|
+
await drain();
|
|
2004
|
+
fillUnstarted(i, INTERRUPT_MESSAGE_FOR_TOOL_USE);
|
|
2005
|
+
return results;
|
|
2006
|
+
}
|
|
2007
|
+
if (ctx.steeringQueue.length > 0) {
|
|
2008
|
+
await drain();
|
|
2009
|
+
fillUnstarted(i, TOOL_USE_SKIPPED_MESSAGE);
|
|
2010
|
+
return results;
|
|
2011
|
+
}
|
|
2012
|
+
inFlight.set(i, dispatch(i));
|
|
2013
|
+
}
|
|
2014
|
+
await drain();
|
|
2015
|
+
return results;
|
|
2016
|
+
} finally {
|
|
2017
|
+
if (parentAbortListener) ctx.signal.removeEventListener("abort", parentAbortListener);
|
|
2018
|
+
}
|
|
1882
2019
|
}
|
|
1883
2020
|
//#endregion
|
|
1884
2021
|
//#region src/prompt.ts
|
|
@@ -2098,6 +2235,7 @@ function looksBinary(text, sniffBytes = SNIFF_BYTES) {
|
|
|
2098
2235
|
function createSkillsReadTool(options) {
|
|
2099
2236
|
const byName = new Map(options.catalog.map((s) => [s.name, s]));
|
|
2100
2237
|
return {
|
|
2238
|
+
isConcurrencySafe: true,
|
|
2101
2239
|
spec: {
|
|
2102
2240
|
name: "skills_read",
|
|
2103
2241
|
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\").",
|
|
@@ -2361,6 +2499,7 @@ function createToolSearchTool(options) {
|
|
|
2361
2499
|
}
|
|
2362
2500
|
const maxLimit = Math.max(options.catalog.length, 1);
|
|
2363
2501
|
return {
|
|
2502
|
+
isConcurrencySafe: true,
|
|
2364
2503
|
spec: {
|
|
2365
2504
|
name: "tool_search",
|
|
2366
2505
|
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.",
|
|
@@ -2576,7 +2715,7 @@ async function synthesizeMissingToolResults(turns, syntheticTurnId, runId, provi
|
|
|
2576
2715
|
}
|
|
2577
2716
|
function resolveBehavior(agentBehavior, runBehavior) {
|
|
2578
2717
|
return {
|
|
2579
|
-
|
|
2718
|
+
maxConcurrentTools: runBehavior?.maxConcurrentTools ?? agentBehavior?.maxConcurrentTools,
|
|
2580
2719
|
maxTurns: runBehavior?.maxTurns ?? agentBehavior?.maxTurns,
|
|
2581
2720
|
maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens,
|
|
2582
2721
|
thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
|
|
@@ -2745,7 +2884,7 @@ function initialRunCounter(session) {
|
|
|
2745
2884
|
for (const t of session.turns) consider(t.runId);
|
|
2746
2885
|
return max;
|
|
2747
2886
|
}
|
|
2748
|
-
function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, mcpConnector, eager, hooks: initialHooks }) {
|
|
2887
|
+
function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, readState: agentReadState, skills: agentSkills, mcpConnector, eager, hooks: initialHooks }) {
|
|
2749
2888
|
const hooks = createHooks();
|
|
2750
2889
|
const executionContext = execution ?? createProcessContext();
|
|
2751
2890
|
const sourceTools = agentTools ?? {};
|
|
@@ -2886,7 +3025,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
2886
3025
|
const thinking = options.thinking ?? "off";
|
|
2887
3026
|
const model = options.model ?? provider.meta.defaultModel;
|
|
2888
3027
|
const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
|
|
2889
|
-
const {
|
|
3028
|
+
const { maxConcurrentTools, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, persistThreshold, persistExcludeTools, persistDir, strictToolPairing } = resolvedBehavior;
|
|
2890
3029
|
let system = options.system || agentSystem || "You are a helpful assistant.";
|
|
2891
3030
|
if (skillsCatalog) system = `${system}\n\n${skillsCatalog}`;
|
|
2892
3031
|
const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? {
|
|
@@ -3049,7 +3188,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3049
3188
|
model,
|
|
3050
3189
|
system,
|
|
3051
3190
|
thinking,
|
|
3052
|
-
|
|
3191
|
+
...maxConcurrentTools !== void 0 ? { maxConcurrentTools } : {},
|
|
3053
3192
|
signal: abortController.signal,
|
|
3054
3193
|
execution: executionContext,
|
|
3055
3194
|
handle: executionHandle,
|
|
@@ -3061,6 +3200,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3061
3200
|
maxTurns,
|
|
3062
3201
|
maxTokens,
|
|
3063
3202
|
...session ? { session } : {},
|
|
3203
|
+
...agentReadState ? { readState: agentReadState } : {},
|
|
3204
|
+
...options.parentRunId ? { parentRunId: options.parentRunId } : {},
|
|
3064
3205
|
depth: runDepth,
|
|
3065
3206
|
thinkingBudget,
|
|
3066
3207
|
schema,
|
|
@@ -3624,12 +3765,14 @@ const edit = {
|
|
|
3624
3765
|
} catch {
|
|
3625
3766
|
return `Edit error: file not found: ${target}.${await suggestionFor(ctx.execution, ctx.handle, target)}`;
|
|
3626
3767
|
}
|
|
3627
|
-
if (ctx.behavior?.requireReadBeforeEdit
|
|
3628
|
-
const readState =
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3768
|
+
if (ctx.behavior?.requireReadBeforeEdit) {
|
|
3769
|
+
const readState = resolveReadStateMap(ctx);
|
|
3770
|
+
if (readState) {
|
|
3771
|
+
const absKey = readStateKey(ctx.handle.cwd, target);
|
|
3772
|
+
const prior = readState.get(absKey);
|
|
3773
|
+
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.)`;
|
|
3774
|
+
if (prior.contentHash !== hashContent(original)) return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
|
|
3775
|
+
}
|
|
3633
3776
|
}
|
|
3634
3777
|
const match = resolveOldString(original, find);
|
|
3635
3778
|
if (!match) {
|
|
@@ -3642,11 +3785,11 @@ const edit = {
|
|
|
3642
3785
|
const updated = replaceAll ? original.split(actual).join(styledReplacement) : original.replace(actual, styledReplacement);
|
|
3643
3786
|
if (updated === original) return `Edit error: replacement produced no change in ${target}.`;
|
|
3644
3787
|
await ctx.execution.writeFile(ctx.handle, target, updated);
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
const absKey =
|
|
3648
|
-
const prior = readState
|
|
3649
|
-
if (
|
|
3788
|
+
const readState = resolveReadStateMap(ctx);
|
|
3789
|
+
if (readState) {
|
|
3790
|
+
const absKey = readStateKey(ctx.handle.cwd, target);
|
|
3791
|
+
const prior = readState.get(absKey);
|
|
3792
|
+
if (prior) readState.set(absKey, {
|
|
3650
3793
|
...prior,
|
|
3651
3794
|
contentHash: hashContent(updated),
|
|
3652
3795
|
mtimeMs: Date.now()
|
|
@@ -3725,6 +3868,7 @@ async function globViaShell(pattern, ctx, limit) {
|
|
|
3725
3868
|
return result.stdout.split("\n").filter((line) => line.length > 0);
|
|
3726
3869
|
}
|
|
3727
3870
|
const glob = {
|
|
3871
|
+
isConcurrencySafe: true,
|
|
3728
3872
|
spec: {
|
|
3729
3873
|
name: "glob",
|
|
3730
3874
|
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.",
|
|
@@ -3790,6 +3934,7 @@ const glob = {
|
|
|
3790
3934
|
const DEFAULT_HEAD_LIMIT = 250;
|
|
3791
3935
|
const DEFAULT_OUTPUT_MODE = "files_with_matches";
|
|
3792
3936
|
const grep = {
|
|
3937
|
+
isConcurrencySafe: true,
|
|
3793
3938
|
spec: {
|
|
3794
3939
|
name: "grep",
|
|
3795
3940
|
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.",
|
|
@@ -4025,6 +4170,7 @@ function createInteractionTool(options) {
|
|
|
4025
4170
|
//#endregion
|
|
4026
4171
|
//#region src/tools/list-files.ts
|
|
4027
4172
|
const listFiles = {
|
|
4173
|
+
isConcurrencySafe: true,
|
|
4028
4174
|
spec: {
|
|
4029
4175
|
name: "list_files",
|
|
4030
4176
|
description: "List files and directories at the given path (relative to project root).",
|
|
@@ -4085,12 +4231,14 @@ const multiEdit = {
|
|
|
4085
4231
|
} catch {
|
|
4086
4232
|
return `multi_edit error: file not found: ${target}.${await suggestionFor(ctx.execution, ctx.handle, target)}`;
|
|
4087
4233
|
}
|
|
4088
|
-
if (ctx.behavior?.requireReadBeforeEdit
|
|
4089
|
-
const readState =
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4234
|
+
if (ctx.behavior?.requireReadBeforeEdit) {
|
|
4235
|
+
const readState = resolveReadStateMap(ctx);
|
|
4236
|
+
if (readState) {
|
|
4237
|
+
const absKey = readStateKey(ctx.handle.cwd, target);
|
|
4238
|
+
const prior = readState.get(absKey);
|
|
4239
|
+
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.)`;
|
|
4240
|
+
if (prior.contentHash !== hashContent(current)) return `multi_edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
|
|
4241
|
+
}
|
|
4094
4242
|
}
|
|
4095
4243
|
let totalReplacements = 0;
|
|
4096
4244
|
for (let i = 0; i < steps.length; i++) {
|
|
@@ -4110,11 +4258,11 @@ const multiEdit = {
|
|
|
4110
4258
|
totalReplacements += occurrences;
|
|
4111
4259
|
}
|
|
4112
4260
|
await ctx.execution.writeFile(ctx.handle, target, current);
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
const absKey =
|
|
4116
|
-
const prior = readState
|
|
4117
|
-
if (
|
|
4261
|
+
const readState = resolveReadStateMap(ctx);
|
|
4262
|
+
if (readState) {
|
|
4263
|
+
const absKey = readStateKey(ctx.handle.cwd, target);
|
|
4264
|
+
const prior = readState.get(absKey);
|
|
4265
|
+
if (prior) readState.set(absKey, {
|
|
4118
4266
|
...prior,
|
|
4119
4267
|
contentHash: hashContent(current),
|
|
4120
4268
|
mtimeMs: Date.now()
|
|
@@ -4207,6 +4355,7 @@ const DEFAULT_BYTE_CAP = 262144;
|
|
|
4207
4355
|
*/
|
|
4208
4356
|
const DEFAULT_IMAGE_BYTE_CAP = 5 * 1024 * 1024;
|
|
4209
4357
|
const readFile$1 = {
|
|
4358
|
+
isConcurrencySafe: true,
|
|
4210
4359
|
spec: {
|
|
4211
4360
|
name: "read_file",
|
|
4212
4361
|
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.",
|
|
@@ -4264,14 +4413,16 @@ const readFile$1 = {
|
|
|
4264
4413
|
return `File not found: ${path}.${await suggestionFor(ctx.execution, ctx.handle, path)}`;
|
|
4265
4414
|
}
|
|
4266
4415
|
const totalBytes = Buffer.byteLength(raw);
|
|
4267
|
-
const
|
|
4268
|
-
const
|
|
4416
|
+
const dedupEnabled = ctx.behavior?.dedupReads !== false;
|
|
4417
|
+
const gateEnabled = ctx.behavior?.requireReadBeforeEdit === true;
|
|
4418
|
+
const readState = dedupEnabled || gateEnabled ? resolveReadStateMap(ctx) : void 0;
|
|
4419
|
+
const absKey = readStateKey(ctx.handle.cwd, path);
|
|
4269
4420
|
const offsetForKey = normalizeInteger(offset, 1);
|
|
4270
4421
|
const limitForKey = normalizeInteger(limit, DEFAULT_LINE_LIMIT);
|
|
4271
4422
|
const maxBytesForKey = normalizeInteger(maxBytes, DEFAULT_BYTE_CAP);
|
|
4272
4423
|
const showLineNumbers = typeof lineNumbers === "boolean" ? lineNumbers : ctx.behavior?.readLineNumbers ?? true;
|
|
4273
4424
|
const currentHash = readState ? hashContent(raw) : "";
|
|
4274
|
-
if (readState) {
|
|
4425
|
+
if (dedupEnabled && readState) {
|
|
4275
4426
|
const prior = readState.get(absKey);
|
|
4276
4427
|
if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey && prior.lineNumbers === showLineNumbers) return `File ${path} unchanged since the previous read in this session — the prior result is still current.`;
|
|
4277
4428
|
}
|
|
@@ -4399,7 +4550,110 @@ function extractTrailingCommand(command) {
|
|
|
4399
4550
|
* context's own default (30 s for in-process).
|
|
4400
4551
|
*/
|
|
4401
4552
|
const DEFAULT_MAX_OUTPUT_BYTES = 32768;
|
|
4553
|
+
/**
|
|
4554
|
+
* Best-effort read-only allow-list for the leading command token. Members
|
|
4555
|
+
* are commands whose stock behavior cannot mutate the workspace under any
|
|
4556
|
+
* argument combination — `ls`, `cat`, `pwd`, etc. Commands that *can*
|
|
4557
|
+
* mutate depending on flags (`find -delete`, `git tag <name>`, `tar -x`)
|
|
4558
|
+
* are intentionally excluded; the input-aware {@link isReadOnlyShellCommand}
|
|
4559
|
+
* predicate falls back to the conservative "not safe" answer for them, so
|
|
4560
|
+
* the scheduler barriers them.
|
|
4561
|
+
*/
|
|
4562
|
+
const SHELL_READ_ONLY_COMMANDS = new Set([
|
|
4563
|
+
"ls",
|
|
4564
|
+
"cat",
|
|
4565
|
+
"head",
|
|
4566
|
+
"tail",
|
|
4567
|
+
"wc",
|
|
4568
|
+
"pwd",
|
|
4569
|
+
"whoami",
|
|
4570
|
+
"id",
|
|
4571
|
+
"date",
|
|
4572
|
+
"uname",
|
|
4573
|
+
"hostname",
|
|
4574
|
+
"tty",
|
|
4575
|
+
"echo",
|
|
4576
|
+
"printf",
|
|
4577
|
+
"env",
|
|
4578
|
+
"printenv",
|
|
4579
|
+
"which",
|
|
4580
|
+
"type",
|
|
4581
|
+
"command",
|
|
4582
|
+
"file",
|
|
4583
|
+
"stat",
|
|
4584
|
+
"grep",
|
|
4585
|
+
"rg",
|
|
4586
|
+
"ag",
|
|
4587
|
+
"true",
|
|
4588
|
+
"false",
|
|
4589
|
+
"test"
|
|
4590
|
+
]);
|
|
4591
|
+
/**
|
|
4592
|
+
* `git` subcommands that are pure reads regardless of arguments. Excludes
|
|
4593
|
+
* `branch`/`tag`/`remote` (which can mutate when given a name) and
|
|
4594
|
+
* `config` (which writes when given a value).
|
|
4595
|
+
*/
|
|
4596
|
+
const GIT_READ_ONLY_SUBCOMMANDS = new Set([
|
|
4597
|
+
"status",
|
|
4598
|
+
"log",
|
|
4599
|
+
"diff",
|
|
4600
|
+
"show",
|
|
4601
|
+
"blame",
|
|
4602
|
+
"rev-parse",
|
|
4603
|
+
"ls-files",
|
|
4604
|
+
"ls-tree",
|
|
4605
|
+
"cat-file",
|
|
4606
|
+
"reflog",
|
|
4607
|
+
"shortlog",
|
|
4608
|
+
"describe",
|
|
4609
|
+
"rev-list",
|
|
4610
|
+
"name-rev",
|
|
4611
|
+
"whatchanged",
|
|
4612
|
+
"merge-base",
|
|
4613
|
+
"symbolic-ref"
|
|
4614
|
+
]);
|
|
4615
|
+
/**
|
|
4616
|
+
* Conservative read-only verdict for a shell command — used to opt a
|
|
4617
|
+
* `shell` invocation into the scheduler's concurrent fleet. Returns
|
|
4618
|
+
* `false` (fail-closed) on anything ambiguous so the scheduler barriers
|
|
4619
|
+
* it. Specifically:
|
|
4620
|
+
*
|
|
4621
|
+
* - Rejects compound commands (`;`, `&&`, `||`, `|`) and redirects (`>`,
|
|
4622
|
+
* `>>`, `<`) — even a pipe to a read-only sink is treated as too
|
|
4623
|
+
* complex to analyze.
|
|
4624
|
+
* - Rejects subshell / process substitution (`$(...)`, `` `...` ``,
|
|
4625
|
+
* `<(...)`, `>(...)`).
|
|
4626
|
+
* - Skips leading `VAR=value` env assignments to find the real
|
|
4627
|
+
* command token.
|
|
4628
|
+
* - Strips a possible absolute path on the command (`/usr/bin/ls` → `ls`).
|
|
4629
|
+
* - Allows the command iff its base name is in
|
|
4630
|
+
* {@link SHELL_READ_ONLY_COMMANDS} OR it's `git <subcmd>` where
|
|
4631
|
+
* `<subcmd>` is in {@link GIT_READ_ONLY_SUBCOMMANDS}.
|
|
4632
|
+
*
|
|
4633
|
+
* Cheap (no spawned process; regex + token scan). Safe to call from the
|
|
4634
|
+
* hot scheduler path.
|
|
4635
|
+
*/
|
|
4636
|
+
function isReadOnlyShellCommand(command) {
|
|
4637
|
+
if (typeof command !== "string") return false;
|
|
4638
|
+
const trimmed = command.trim();
|
|
4639
|
+
if (trimmed === "") return false;
|
|
4640
|
+
if (/[<>;&|`\n]/.test(trimmed)) return false;
|
|
4641
|
+
if (trimmed.includes("$(") || trimmed.includes("<(") || trimmed.includes(">(")) return false;
|
|
4642
|
+
const tokens = trimmed.split(/\s+/);
|
|
4643
|
+
let i = 0;
|
|
4644
|
+
while (i < tokens.length && /^[A-Z_]\w*=/i.test(tokens[i])) i++;
|
|
4645
|
+
const head = tokens[i];
|
|
4646
|
+
if (!head) return false;
|
|
4647
|
+
const base = head.split("/").pop() ?? head;
|
|
4648
|
+
if (SHELL_READ_ONLY_COMMANDS.has(base)) return true;
|
|
4649
|
+
if (base === "git") {
|
|
4650
|
+
const sub = tokens[i + 1];
|
|
4651
|
+
return typeof sub === "string" && GIT_READ_ONLY_SUBCOMMANDS.has(sub);
|
|
4652
|
+
}
|
|
4653
|
+
return false;
|
|
4654
|
+
}
|
|
4402
4655
|
const shell = {
|
|
4656
|
+
isConcurrencySafe: (input) => isReadOnlyShellCommand(input.command),
|
|
4403
4657
|
spec: {
|
|
4404
4658
|
name: "shell",
|
|
4405
4659
|
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.",
|
|
@@ -4676,6 +4930,7 @@ function createSpawnTool(options = {}) {
|
|
|
4676
4930
|
get totalChildStats() {
|
|
4677
4931
|
return { ...localStats };
|
|
4678
4932
|
},
|
|
4933
|
+
isConcurrencySafe: true,
|
|
4679
4934
|
spec: {
|
|
4680
4935
|
name: "spawn",
|
|
4681
4936
|
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.",
|
|
@@ -4717,18 +4972,23 @@ function createSpawnTool(options = {}) {
|
|
|
4717
4972
|
let result = "";
|
|
4718
4973
|
let unbubble;
|
|
4719
4974
|
try {
|
|
4720
|
-
const
|
|
4975
|
+
const parentPreset = {
|
|
4721
4976
|
...ctx.name !== void 0 ? { name: ctx.name } : {},
|
|
4722
4977
|
...ctx.system !== void 0 ? { system: ctx.system } : {},
|
|
4723
4978
|
tools: ctx.tools,
|
|
4724
4979
|
...ctx.toolAliases !== void 0 ? { toolAliases: ctx.toolAliases } : {},
|
|
4725
4980
|
...ctx.mcpServers !== void 0 ? { mcpServers: ctx.mcpServers } : {},
|
|
4726
4981
|
...ctx.skills !== void 0 ? { skills: ctx.skills } : {},
|
|
4727
|
-
...ctx.behavior !== void 0 ? { behavior: ctx.behavior } : {}
|
|
4982
|
+
...ctx.behavior !== void 0 ? { behavior: ctx.behavior } : {}
|
|
4983
|
+
};
|
|
4984
|
+
const sharedReadState = options.shareReadState ? resolveReadStateMap(ctx) : void 0;
|
|
4985
|
+
const agent = createAgent({
|
|
4986
|
+
...parentPreset,
|
|
4728
4987
|
...options.preset,
|
|
4729
4988
|
provider: ctx.provider,
|
|
4730
4989
|
execution: ctx.execution,
|
|
4731
|
-
...options.persist && ctx.session ? { session: ctx.session } : {}
|
|
4990
|
+
...options.persist && ctx.session ? { session: ctx.session } : {},
|
|
4991
|
+
...sharedReadState ? { readState: sharedReadState } : {}
|
|
4732
4992
|
});
|
|
4733
4993
|
if (forwardHooks) {
|
|
4734
4994
|
const unregisterEnricher = agent.hooks.hook("tool:before", async (toolCtx) => {
|
|
@@ -4903,6 +5163,6 @@ const writeFile$1 = {
|
|
|
4903
5163
|
}
|
|
4904
5164
|
};
|
|
4905
5165
|
//#endregion
|
|
4906
|
-
export { 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, 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,
|
|
5166
|
+
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 };
|
|
4907
5167
|
|
|
4908
|
-
//# sourceMappingURL=tools-
|
|
5168
|
+
//# sourceMappingURL=tools-DKdyPoUf.js.map
|