zidane 5.10.13 → 5.11.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 +31 -5
- package/dist/{agent-BHkvYIH9.d.ts → agent-D0W9yClt.d.ts} +114 -27
- package/dist/agent-D0W9yClt.d.ts.map +1 -0
- package/dist/chat/pure.d.ts +3 -3
- package/dist/chat.d.ts +7 -7
- package/dist/chat.js +2 -2
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +53 -14
- package/dist/contexts/docker.js.map +1 -1
- package/dist/contexts/e2b.d.ts +168 -0
- package/dist/contexts/e2b.d.ts.map +1 -0
- package/dist/contexts/e2b.js +261 -0
- package/dist/contexts/e2b.js.map +1 -0
- package/dist/{contexts-BJVgG0LY.js → contexts-DglWSzmR.js} +59 -9
- package/dist/contexts-DglWSzmR.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +5 -5
- package/dist/eval.js.map +1 -1
- package/dist/{headless-CPaunZsU.js → headless-Bb5gU8AR.js} +6 -6
- package/dist/{headless-CPaunZsU.js.map → headless-Bb5gU8AR.js.map} +1 -1
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -1
- package/dist/{index-C_t8tW_X.d.ts → index-CrMb8jCE.d.ts} +2 -2
- package/dist/{index-C_t8tW_X.d.ts.map → index-CrMb8jCE.d.ts.map} +1 -1
- package/dist/{index-BIo67xLV.d.ts → index-D60tX5XC.d.ts} +10 -3
- package/dist/index-D60tX5XC.d.ts.map +1 -0
- package/dist/{index-C4aT2kO_.d.ts → index-DZR99FD4.d.ts} +30 -111
- package/dist/index-DZR99FD4.d.ts.map +1 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +11 -10
- package/dist/index.js.map +1 -1
- package/dist/{interpolate-Dy7Lunvg.js → interpolate-CTfr0GdR.js} +19 -1
- package/dist/{interpolate-Dy7Lunvg.js.map → interpolate-CTfr0GdR.js.map} +1 -1
- package/dist/logger-Ktm-lj1s.js +300 -0
- package/dist/logger-Ktm-lj1s.js.map +1 -0
- package/dist/logger-n4LsLISE.d.ts +102 -0
- package/dist/logger-n4LsLISE.d.ts.map +1 -0
- package/dist/{login-0jP1pnSJ.js → login-BHhOdTp9.js} +4 -301
- package/dist/login-BHhOdTp9.js.map +1 -0
- package/dist/{mcp-tevNihk_.js → mcp-Cy9mgCcr.js} +22 -9
- package/dist/mcp-Cy9mgCcr.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-C_1AmSpk.js → messages-RPKrEPvH.js} +6 -2
- package/dist/messages-RPKrEPvH.js.map +1 -0
- package/dist/output/stream-json.d.ts +2 -2
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +2 -2
- package/dist/output/terminal.js +1 -0
- package/dist/output/terminal.js.map +1 -1
- package/dist/{presets-Cm2BPJaU.js → presets-D5ibZTml.js} +2 -2
- package/dist/{presets-Cm2BPJaU.js.map → presets-D5ibZTml.js.map} +1 -1
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/{providers-BGBB18zz.js → providers-C2cxujp_.js} +85 -20
- package/dist/providers-C2cxujp_.js.map +1 -0
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +2 -2
- package/dist/restate.d.ts +2 -2
- package/dist/restate.js +4 -1
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +36 -4
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-CtAWwwkn.js → session-Do_TQV7c.js} +70 -22
- package/dist/session-Do_TQV7c.js.map +1 -0
- package/dist/session.d.ts +2 -2
- package/dist/session.js +3 -3
- package/dist/shell-quote-BmnhZmdM.js +33 -0
- package/dist/shell-quote-BmnhZmdM.js.map +1 -0
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/{tool-formatters-D_fX6FGl.d.ts → tool-formatters-RT5-gyE2.d.ts} +2 -2
- package/dist/{tool-formatters-D_fX6FGl.d.ts.map → tool-formatters-RT5-gyE2.d.ts.map} +1 -1
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/{tools-NxnEmzYg.js → tools-ZHKOh44k.js} +342 -123
- package/dist/tools-ZHKOh44k.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-DA6XawEU.d.ts → transcript-anchors-B4FxkG-8.d.ts} +10 -4
- package/dist/transcript-anchors-B4FxkG-8.d.ts.map +1 -0
- package/dist/{transcript-anchors-B_c7gWot.js → transcript-anchors-CS46ul6X.js} +10 -10
- package/dist/transcript-anchors-CS46ul6X.js.map +1 -0
- package/dist/tui.d.ts +3 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +167 -41
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-CCl7rpbT.d.ts → turn-operations-CoRj3mYZ.d.ts} +3 -3
- package/dist/{turn-operations-CCl7rpbT.d.ts.map → turn-operations-CoRj3mYZ.d.ts.map} +1 -1
- package/dist/{types-BibzMDjX.d.ts → types-B39tBba1.d.ts} +69 -2
- package/dist/types-B39tBba1.d.ts.map +1 -0
- package/dist/types-BiobHM1D.js.map +1 -1
- package/dist/types.d.ts +5 -5
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CHAT.md +3 -3
- package/docs/EXECUTION_CONTEXT.md +257 -0
- package/docs/RUN_IN_BACKGROUND.md +8 -0
- package/docs/SKILL.md +3 -3
- package/package.json +57 -24
- package/dist/agent-BHkvYIH9.d.ts.map +0 -1
- package/dist/contexts-BJVgG0LY.js.map +0 -1
- package/dist/index-BIo67xLV.d.ts.map +0 -1
- package/dist/index-C4aT2kO_.d.ts.map +0 -1
- package/dist/login-0jP1pnSJ.js.map +0 -1
- package/dist/mcp-tevNihk_.js.map +0 -1
- package/dist/messages-C_1AmSpk.js.map +0 -1
- package/dist/providers-BGBB18zz.js.map +0 -1
- package/dist/session-CtAWwwkn.js.map +0 -1
- package/dist/tools-NxnEmzYg.js.map +0 -1
- package/dist/transcript-anchors-B_c7gWot.js.map +0 -1
- package/dist/transcript-anchors-DA6XawEU.d.ts.map +0 -1
- package/dist/types-BibzMDjX.d.ts.map +0 -1
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import { r as utf8ByteLength } from "./utils-ngQzYzZD.js";
|
|
2
2
|
import { t as buildContextBreakdown } from "./context-breakdown-kO-pDsay.js";
|
|
3
3
|
import { a as formatTaskStatus, i as formatDuration, o as formatTaskSummary, s as previewLine } from "./format-BNOXpl-1.js";
|
|
4
|
-
import { a as createCursorOAuthProvider, c as baseten, d as anthropic,
|
|
4
|
+
import { a as createCursorOAuthProvider, c as baseten, d as anthropic, g as FAST_MODE_OPTIONS, h as ANTHROPIC_EXTRA_MODELS, m as writeFileAtomicAsync, n as openai, o as generatePkce, r as local, s as cerebras, t as openrouter, u as arcee } from "./providers-C2cxujp_.js";
|
|
5
5
|
import { i as AgentProviderError, l as errorMessage, n as AgentBudgetExceededError, o as AgentToolPairingError, p as toTypedError, t as AgentAbortedError } from "./errors-DkR6GPJw.js";
|
|
6
|
-
import { A as renderSystemForWire, D as appendStaticSection, a as detectTurnInterruption, c as filterUnresolvedToolUses, d as remintDuplicateToolCallIds, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-
|
|
6
|
+
import { A as renderSystemForWire, D as appendStaticSection, a as detectTurnInterruption, c as filterUnresolvedToolUses, d as remintDuplicateToolCallIds, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-RPKrEPvH.js";
|
|
7
7
|
import { a as toolResultToText, i as toolOutputByteLength, n as documentBlockMarker, r as toolOutputBudgetByteLength, t as DEFAULT_AGENT_CLOCK } from "./types-BiobHM1D.js";
|
|
8
8
|
import { t as reconcileImageMediaType } from "./image-sniff-B7uFSNO1.js";
|
|
9
|
-
import { r as createProcessContext, t as resolveDetachedTasksCapability } from "./contexts-
|
|
9
|
+
import { r as createProcessContext, t as resolveDetachedTasksCapability } from "./contexts-DglWSzmR.js";
|
|
10
10
|
import { i as styleReplacementForVia, n as resolveOldString, r as stripLineNumberPrefixes, t as describeVia } from "./edit-utils-EGosADZq.js";
|
|
11
11
|
import { a as markReadStateElided, n as getToolDedupState, o as readStateKey, r as hashContent, s as resolveReadStateMap } from "./read-state-BFqpQRc5.js";
|
|
12
|
-
import { S as escapeXml, d as buildCatalog, n as stripShellInterpolations, p as installAllowedToolsGate, r as resolveSkills, t as interpolateShellCommands, v as validateResourcePathReal, x as createSkillActivationState } from "./interpolate-
|
|
13
|
-
import { n as connectMcpServers } from "./mcp-
|
|
12
|
+
import { S as escapeXml, d as buildCatalog, n as stripShellInterpolations, p as installAllowedToolsGate, r as resolveSkills, t as interpolateShellCommands, v as validateResourcePathReal, x as createSkillActivationState } from "./interpolate-CTfr0GdR.js";
|
|
13
|
+
import { n as connectMcpServers } from "./mcp-Cy9mgCcr.js";
|
|
14
14
|
import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
|
|
15
|
-
import {
|
|
15
|
+
import { n as shellQuote, t as alwaysQuote } from "./shell-quote-BmnhZmdM.js";
|
|
16
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
16
17
|
import { createHooks } from "hookable";
|
|
17
18
|
import { getModel, getModels } from "@earendil-works/pi-ai";
|
|
18
19
|
import { Buffer } from "node:buffer";
|
|
19
20
|
import { refreshAnthropicToken, refreshOpenAICodexToken, registerOAuthProvider } from "@earendil-works/pi-ai/oauth";
|
|
21
|
+
import { glob, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
20
22
|
import { createServer } from "node:http";
|
|
21
23
|
import { randomBytes } from "node:crypto";
|
|
22
|
-
import { glob, mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
23
24
|
//#region src/aliasing.ts
|
|
24
25
|
/**
|
|
25
26
|
* Build alias lookup maps from a `toolAliases` record.
|
|
@@ -82,8 +83,8 @@ function toCanonicalName(wire, maps) {
|
|
|
82
83
|
* Why: SDK consumers and tool descriptions across the ecosystem
|
|
83
84
|
* inconsistently reference one form or the other (Anthropic's docs use
|
|
84
85
|
* double, zidane uses single). A Claude-Code-trained model emitting
|
|
85
|
-
* `
|
|
86
|
-
* `
|
|
86
|
+
* `mcp__acme__apply_migration` against a server zidane registered as
|
|
87
|
+
* `mcp_acme_apply_migration` would otherwise trip the `tool:unknown`
|
|
87
88
|
* path and waste a turn on the correction.
|
|
88
89
|
*
|
|
89
90
|
* Idempotent: a canonical that already has a double-underscore alias
|
|
@@ -1403,6 +1404,14 @@ function installDedupToolsGate(hooks, getDedupTools, getSession) {
|
|
|
1403
1404
|
*/
|
|
1404
1405
|
const PERSISTENCE_PREVIEW_BYTES = 2 * 1024;
|
|
1405
1406
|
/**
|
|
1407
|
+
* Preview size for persist-on-elide stubs. Much smaller than
|
|
1408
|
+
* {@link PERSISTENCE_PREVIEW_BYTES}: this content is being COMPACTED OUT (it's
|
|
1409
|
+
* old), so the stub only needs a hint plus the recovery path, not a full
|
|
1410
|
+
* preview. Keeping it small stops the elided region from ballooning ~40× over
|
|
1411
|
+
* the lossy stub it replaces while still being recoverable by reading `path`.
|
|
1412
|
+
*/
|
|
1413
|
+
const ELIDE_PERSIST_PREVIEW_BYTES = 256;
|
|
1414
|
+
/**
|
|
1406
1415
|
* Byte-stable prefix every {@link buildPersistedStub} output starts with.
|
|
1407
1416
|
* Exported so wire-level passes (tail compaction, future stale-output
|
|
1408
1417
|
* elision) can recognize a persisted stub and preserve its path attribute
|
|
@@ -1632,7 +1641,7 @@ async function enforcePersistDirCap(persistDir, maxBytes, opts = {}) {
|
|
|
1632
1641
|
* surface.
|
|
1633
1642
|
*/
|
|
1634
1643
|
function buildPersistedStub(input) {
|
|
1635
|
-
const { slice: previewSlice, bytes: previewBytes } = sliceFirstBytes(input.output,
|
|
1644
|
+
const { slice: previewSlice, bytes: previewBytes } = sliceFirstBytes(input.output, input.previewBytes ?? 2048);
|
|
1636
1645
|
const previewMarker = previewSlice.length < input.output.length ? `\n…(${input.originalBytes - previewBytes} more bytes in persisted file)` : "";
|
|
1637
1646
|
return [
|
|
1638
1647
|
`${PERSISTED_STUB_PREFIX}${escapeXml(input.toolName)}" bytes="${input.originalBytes}" path="${escapeXml(input.persistedPath)}">`,
|
|
@@ -1662,30 +1671,76 @@ async function cleanupPersistedSession(persistRoot) {
|
|
|
1662
1671
|
}
|
|
1663
1672
|
}
|
|
1664
1673
|
/**
|
|
1665
|
-
*
|
|
1666
|
-
*
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1669
|
-
*
|
|
1670
|
-
*
|
|
1671
|
-
*
|
|
1672
|
-
*
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1675
|
-
* the
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1674
|
+
* Persist-on-elide: belatedly write tool results that tail compaction is
|
|
1675
|
+
* about to replace with the lossy `[…elided…]` stub, swapping in a
|
|
1676
|
+
* recoverable `<persisted-output … path=…>` stub instead. This is the lazy
|
|
1677
|
+
* counterpart to {@link maybePersistToolResult}'s eager (emit-time)
|
|
1678
|
+
* persistence — it only fires when a result is actually being compacted out,
|
|
1679
|
+
* so recent results stay full inline while OLD ones become recoverable rather
|
|
1680
|
+
* than destroyed. Closes the context-loss gap for non-read results between
|
|
1681
|
+
* the lossy stub and re-runnable side effects: after this, a compacted result
|
|
1682
|
+
* is always retrievable by `read_file`-ing its path.
|
|
1683
|
+
*
|
|
1684
|
+
* `items` are the {@link TailCompactionResult.persistableElided} entries (the
|
|
1685
|
+
* caller has already filtered out reads — which recover via the read-state
|
|
1686
|
+
* dedup re-serve — and results too small to be worth persisting). Each
|
|
1687
|
+
* `output` is the ORIGINAL content captured before the lossy stub replaced it.
|
|
1688
|
+
*
|
|
1689
|
+
* Idempotency: blobs are immutable (a tool result never changes), so we write
|
|
1690
|
+
* `<callId>.txt` only when it's absent and otherwise just rebuild the
|
|
1691
|
+
* byte-stable stub. The session keeps the original content; only the wire copy
|
|
1692
|
+
* carries the stub — consistent with compaction being wire-only. Returns the
|
|
1693
|
+
* messages with the lossy stubs swapped; unchanged when nothing was persisted.
|
|
1694
|
+
*/
|
|
1695
|
+
async function persistElidedToolResults(messages, items, opts) {
|
|
1696
|
+
if (items.length === 0 || !isAbsolute(opts.persistDir)) return messages;
|
|
1697
|
+
const stubByCallId = /* @__PURE__ */ new Map();
|
|
1698
|
+
let wroteAny = false;
|
|
1699
|
+
for (const item of items) {
|
|
1700
|
+
if (!SAFE_CALL_ID.test(item.callId) || item.callId.includes("..")) continue;
|
|
1701
|
+
const persistedPath = join(opts.persistDir, `${item.callId}.txt`);
|
|
1702
|
+
let exists = false;
|
|
1703
|
+
try {
|
|
1704
|
+
exists = (await stat(persistedPath)).isFile();
|
|
1705
|
+
} catch {
|
|
1706
|
+
exists = false;
|
|
1707
|
+
}
|
|
1708
|
+
if (!exists) try {
|
|
1709
|
+
await writeAtomic(persistedPath, item.output);
|
|
1710
|
+
wroteAny = true;
|
|
1711
|
+
} catch {
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
stubByCallId.set(item.callId, buildPersistedStub({
|
|
1715
|
+
toolName: item.toolName,
|
|
1716
|
+
originalBytes: toolOutputByteLength(item.output),
|
|
1717
|
+
persistedPath,
|
|
1718
|
+
output: item.output,
|
|
1719
|
+
previewBytes: ELIDE_PERSIST_PREVIEW_BYTES
|
|
1720
|
+
}));
|
|
1721
|
+
}
|
|
1722
|
+
if (wroteAny && typeof opts.maxBytes === "number" && Number.isFinite(opts.maxBytes) && opts.maxBytes > 0) await enforcePersistDirCap(opts.persistDir, opts.maxBytes);
|
|
1723
|
+
if (stubByCallId.size === 0) return messages;
|
|
1724
|
+
return messages.map((msg) => {
|
|
1725
|
+
if (!msg.content.some((b) => b.type === "tool_result" && stubByCallId.has(b.callId))) return msg;
|
|
1726
|
+
return {
|
|
1727
|
+
...msg,
|
|
1728
|
+
content: msg.content.map((b) => b.type === "tool_result" && stubByCallId.has(b.callId) ? {
|
|
1729
|
+
...b,
|
|
1730
|
+
output: stubByCallId.get(b.callId)
|
|
1731
|
+
} : b)
|
|
1732
|
+
};
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Write-then-rename for atomicity — delegates to the shared
|
|
1737
|
+
* {@link writeFileAtomicAsync} helper (unique pid+counter tmp suffix,
|
|
1738
|
+
* fsync before rename, tmp cleanup on failure). Creates the parent
|
|
1739
|
+
* directory on demand so the caller doesn't have to track which
|
|
1740
|
+
* sessions have already initialized their dir.
|
|
1678
1741
|
*/
|
|
1679
1742
|
async function writeAtomic(path, content) {
|
|
1680
|
-
await
|
|
1681
|
-
const tmp = `${path}.tmp`;
|
|
1682
|
-
try {
|
|
1683
|
-
await writeFile(tmp, content, "utf8");
|
|
1684
|
-
await rename(tmp, path);
|
|
1685
|
-
} catch (err) {
|
|
1686
|
-
await rm(tmp, { force: true }).catch(() => {});
|
|
1687
|
-
throw err;
|
|
1688
|
-
}
|
|
1743
|
+
await writeFileAtomicAsync(path, content, { ensureDir: true });
|
|
1689
1744
|
}
|
|
1690
1745
|
/**
|
|
1691
1746
|
* Take the first `cap` bytes of `text` without splitting a UTF-8
|
|
@@ -2125,6 +2180,25 @@ const SHELL_CASCADE_CANCEL_MESSAGE = "Cancelled: a sibling `shell` call in the s
|
|
|
2125
2180
|
*/
|
|
2126
2181
|
const CANCELLED_BY_USER_SENTINEL = "zidane:tool:cancelled-by-user";
|
|
2127
2182
|
/**
|
|
2183
|
+
* Grace window after a run-level abort before we force-settle a tool body that
|
|
2184
|
+
* hasn't returned. Well-behaved tools observe `childSignal` and finalize their
|
|
2185
|
+
* own bookkeeping (e.g. `spawn` awaits its child's session-persisting `finally`
|
|
2186
|
+
* block); they settle within this window and the forced rejection never fires.
|
|
2187
|
+
* Only a body that *ignores* its signal hits the timeout — that's the wedge we
|
|
2188
|
+
* guard against. Kept short so a real wedge doesn't stall `drain()` for long,
|
|
2189
|
+
* but long enough for a normal abort-driven cleanup to complete.
|
|
2190
|
+
*
|
|
2191
|
+
* Overridable via `ZIDANE_RUN_ABORT_GRACE_MS` (internal/testing only — lets
|
|
2192
|
+
* the wedge test exercise the forced-rejection path without a multi-second
|
|
2193
|
+
* real wait). Read per-use (not memoized) so tests can set it; falls back to
|
|
2194
|
+
* the 2s default on any non-finite value.
|
|
2195
|
+
*/
|
|
2196
|
+
const DEFAULT_RUN_ABORT_GRACE_MS = 2e3;
|
|
2197
|
+
function runAbortGraceMs() {
|
|
2198
|
+
const raw = Number(process.env.ZIDANE_RUN_ABORT_GRACE_MS);
|
|
2199
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_RUN_ABORT_GRACE_MS;
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2128
2202
|
* Compute the effective thinking budget for a given run-relative turn, given
|
|
2129
2203
|
* the configured decay schedule. Pure helper — exported for tests and so
|
|
2130
2204
|
* downstream tooling can preview decay curves without spinning up the loop.
|
|
@@ -2272,32 +2346,56 @@ function applyCompactSummaryCutoff(turns) {
|
|
|
2272
2346
|
* alone — those are consumer-controlled and the consumer is responsible for
|
|
2273
2347
|
* matching prompt parts to the provider's capabilities.
|
|
2274
2348
|
*/
|
|
2275
|
-
const COMPACTION_STUB = "[…elided
|
|
2349
|
+
const COMPACTION_STUB = "[…elided: older tool output trimmed to fit the context budget.]";
|
|
2276
2350
|
const COMPACTION_STUB_BYTES = toolOutputByteLength(COMPACTION_STUB);
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
* For Anthropic users, prefer the server-side `context-management-2025-06-27`
|
|
2289
|
-
* beta (token-accurate, no client-side approximation). This function is the
|
|
2290
|
-
* client-side fallback for OpenAI-compatible / OpenRouter / Cerebras runs
|
|
2291
|
-
* against OSS models that lack a server-side equivalent.
|
|
2292
|
-
*/
|
|
2293
|
-
function applyTailCompaction(messages, threshold, keepTurns) {
|
|
2294
|
-
if (messages.length === 0) return messages;
|
|
2351
|
+
const COMPACT_CHUNK_TURNS = 8;
|
|
2352
|
+
function applyTailCompaction(messages, threshold, keepTurns, options) {
|
|
2353
|
+
const elidedReadPaths = [];
|
|
2354
|
+
const persistableElided = [];
|
|
2355
|
+
const toolNameByCallId = options?.toolNameByCallId;
|
|
2356
|
+
const persistElideMinBytes = options?.persistElideMinBytes ?? 0;
|
|
2357
|
+
if (messages.length === 0) return {
|
|
2358
|
+
messages,
|
|
2359
|
+
elidedReadPaths,
|
|
2360
|
+
persistableElided
|
|
2361
|
+
};
|
|
2295
2362
|
let totalBytes = 0;
|
|
2296
2363
|
for (const msg of messages) for (const block of msg.content) if (block.type === "tool_result") totalBytes += toolOutputByteLength(block.output);
|
|
2297
|
-
if (totalBytes <= threshold) return
|
|
2364
|
+
if (totalBytes <= threshold) return {
|
|
2365
|
+
messages,
|
|
2366
|
+
elidedReadPaths,
|
|
2367
|
+
persistableElided
|
|
2368
|
+
};
|
|
2298
2369
|
const keep = Math.max(0, keepTurns);
|
|
2299
|
-
const
|
|
2300
|
-
if (
|
|
2370
|
+
const floorCutoff = messages.length - keep;
|
|
2371
|
+
if (floorCutoff <= 0) return {
|
|
2372
|
+
messages,
|
|
2373
|
+
elidedReadPaths,
|
|
2374
|
+
persistableElided
|
|
2375
|
+
};
|
|
2376
|
+
let keptBytes = 0;
|
|
2377
|
+
let budgetCutoff = floorCutoff;
|
|
2378
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2379
|
+
for (const block of messages[i].content) if (block.type === "tool_result") keptBytes += toolOutputByteLength(block.output);
|
|
2380
|
+
if (keptBytes > threshold) {
|
|
2381
|
+
budgetCutoff = i + 1;
|
|
2382
|
+
break;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
const rawCutoff = Math.min(budgetCutoff, floorCutoff);
|
|
2386
|
+
if (rawCutoff <= 0) return {
|
|
2387
|
+
messages,
|
|
2388
|
+
elidedReadPaths,
|
|
2389
|
+
persistableElided
|
|
2390
|
+
};
|
|
2391
|
+
const chunk = Math.max(1, options?.chunkTurns ?? 1);
|
|
2392
|
+
const cutoff = Math.floor(rawCutoff / chunk) * chunk;
|
|
2393
|
+
if (cutoff <= 0) return {
|
|
2394
|
+
messages,
|
|
2395
|
+
elidedReadPaths,
|
|
2396
|
+
persistableElided
|
|
2397
|
+
};
|
|
2398
|
+
const readPathByCallId = options?.readPathByCallId;
|
|
2301
2399
|
let changed = false;
|
|
2302
2400
|
const out = messages.slice();
|
|
2303
2401
|
for (let i = 0; i < cutoff; i++) {
|
|
@@ -2305,10 +2403,18 @@ function applyTailCompaction(messages, threshold, keepTurns) {
|
|
|
2305
2403
|
let msgChanged = false;
|
|
2306
2404
|
const newContent = msg.content.map((block) => {
|
|
2307
2405
|
if (block.type !== "tool_result") return block;
|
|
2308
|
-
|
|
2406
|
+
const existingBytes = toolOutputByteLength(block.output);
|
|
2407
|
+
if (existingBytes <= COMPACTION_STUB_BYTES) return block;
|
|
2309
2408
|
if (typeof block.output === "string" && block.output.startsWith("<persisted-output tool=\"")) return block;
|
|
2310
2409
|
msgChanged = true;
|
|
2311
2410
|
changed = true;
|
|
2411
|
+
const readPath = readPathByCallId?.get(block.callId);
|
|
2412
|
+
if (readPath !== void 0) elidedReadPaths.push(readPath);
|
|
2413
|
+
else if (persistElideMinBytes > 0 && typeof block.output === "string" && existingBytes > persistElideMinBytes) persistableElided.push({
|
|
2414
|
+
callId: block.callId,
|
|
2415
|
+
toolName: toolNameByCallId?.get(block.callId) ?? "tool",
|
|
2416
|
+
output: block.output
|
|
2417
|
+
});
|
|
2312
2418
|
return {
|
|
2313
2419
|
...block,
|
|
2314
2420
|
output: COMPACTION_STUB
|
|
@@ -2319,7 +2425,34 @@ function applyTailCompaction(messages, threshold, keepTurns) {
|
|
|
2319
2425
|
content: newContent
|
|
2320
2426
|
};
|
|
2321
2427
|
}
|
|
2322
|
-
return
|
|
2428
|
+
return {
|
|
2429
|
+
messages: changed ? out : messages,
|
|
2430
|
+
elidedReadPaths,
|
|
2431
|
+
persistableElided
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Build a call_id → file path map for `read_file` tool calls in `messages`.
|
|
2436
|
+
* Tail compaction uses it to learn which stubbed `tool_result`s were file
|
|
2437
|
+
* reads, so their read-state dedup can be invalidated. Resolve this from the
|
|
2438
|
+
* canonical, pre-alias history — call_ids are stable across the alias rewrite,
|
|
2439
|
+
* so the map still keys the wire-level `tool_result` blocks.
|
|
2440
|
+
*/
|
|
2441
|
+
function collectCallMaps(messages) {
|
|
2442
|
+
const readPathByCallId = /* @__PURE__ */ new Map();
|
|
2443
|
+
const toolNameByCallId = /* @__PURE__ */ new Map();
|
|
2444
|
+
for (const msg of messages) for (const block of msg.content) {
|
|
2445
|
+
if (block.type !== "tool_call") continue;
|
|
2446
|
+
toolNameByCallId.set(block.id, block.name);
|
|
2447
|
+
if (block.name === "read_file") {
|
|
2448
|
+
const path = block.input.path;
|
|
2449
|
+
if (typeof path === "string" && path.length > 0) readPathByCallId.set(block.id, path);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return {
|
|
2453
|
+
readPathByCallId,
|
|
2454
|
+
toolNameByCallId
|
|
2455
|
+
};
|
|
2323
2456
|
}
|
|
2324
2457
|
/**
|
|
2325
2458
|
* Replace `read_file` `tool_result` blocks with a short stub when a later
|
|
@@ -2338,7 +2471,7 @@ function applyTailCompaction(messages, threshold, keepTurns) {
|
|
|
2338
2471
|
* Pure function, exported for tests and so downstream tooling can preview
|
|
2339
2472
|
* elision without spinning up the loop.
|
|
2340
2473
|
*/
|
|
2341
|
-
const STALE_READ_STUB = "[…elided: file edited later in
|
|
2474
|
+
const STALE_READ_STUB = "[…elided: this file was edited later in the run, so this earlier read is stale.]";
|
|
2342
2475
|
function applyStaleReadElision(messages) {
|
|
2343
2476
|
if (messages.length === 0) return {
|
|
2344
2477
|
messages,
|
|
@@ -2394,7 +2527,7 @@ function applyStaleReadElision(messages) {
|
|
|
2394
2527
|
let msgChanged = false;
|
|
2395
2528
|
const newContent = msg.content.map((block) => {
|
|
2396
2529
|
if (block.type !== "tool_result" || !staleCallIds.has(block.callId)) return block;
|
|
2397
|
-
if (block.output === "[…elided: file edited later in
|
|
2530
|
+
if (block.output === "[…elided: this file was edited later in the run, so this earlier read is stale.]") return block;
|
|
2398
2531
|
if (typeof block.output === "string" && block.output.startsWith("<persisted-output tool=\"")) return block;
|
|
2399
2532
|
msgChanged = true;
|
|
2400
2533
|
changed = true;
|
|
@@ -2473,10 +2606,10 @@ function applyPairingRepair(ctx, messages, turnId) {
|
|
|
2473
2606
|
...ctx.providerName ? { provider: ctx.providerName } : {},
|
|
2474
2607
|
repairs
|
|
2475
2608
|
});
|
|
2476
|
-
for (const repair of repairs) ctx.hooks.callHook("pairing:repair", {
|
|
2609
|
+
for (const repair of repairs) Promise.resolve(ctx.hooks.callHook("pairing:repair", {
|
|
2477
2610
|
...repair,
|
|
2478
2611
|
turnId
|
|
2479
|
-
});
|
|
2612
|
+
})).catch(() => {});
|
|
2480
2613
|
return repaired;
|
|
2481
2614
|
}
|
|
2482
2615
|
function sanitizeStoredToolResults(provider, messages) {
|
|
@@ -3001,7 +3134,19 @@ async function executeTurn(ctx, turn, priorUsage) {
|
|
|
3001
3134
|
if (ctx.compactStrategy === "tail") {
|
|
3002
3135
|
const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
|
|
3003
3136
|
const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
|
|
3004
|
-
|
|
3137
|
+
const { readPathByCallId, toolNameByCallId } = collectCallMaps(canonicalMessages);
|
|
3138
|
+
const compacted = applyTailCompaction(sanitizedMessages, threshold, keep, {
|
|
3139
|
+
readPathByCallId,
|
|
3140
|
+
toolNameByCallId,
|
|
3141
|
+
chunkTurns: COMPACT_CHUNK_TURNS,
|
|
3142
|
+
persistElideMinBytes: ctx.persistDir ? PERSISTENCE_PREVIEW_BYTES : 0
|
|
3143
|
+
});
|
|
3144
|
+
sanitizedMessages = compacted.messages;
|
|
3145
|
+
markReadStateForElidedPaths(ctx, ctx.handle.cwd, compacted.elidedReadPaths);
|
|
3146
|
+
if (ctx.persistDir && compacted.persistableElided.length > 0) sanitizedMessages = await persistElidedToolResults(sanitizedMessages, compacted.persistableElided, {
|
|
3147
|
+
persistDir: ctx.persistDir,
|
|
3148
|
+
maxBytes: ctx.persistMaxBytes
|
|
3149
|
+
});
|
|
3005
3150
|
} else if (typeof ctx.compactStrategy === "function") {
|
|
3006
3151
|
const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
|
|
3007
3152
|
const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
|
|
@@ -3207,12 +3352,12 @@ async function executeTurn(ctx, turn, priorUsage) {
|
|
|
3207
3352
|
if (reminted.reminted.length > 0) {
|
|
3208
3353
|
canonicalToolCalls = reminted.toolCalls;
|
|
3209
3354
|
canonicalContent = reminted.content;
|
|
3210
|
-
for (const r of reminted.reminted) ctx.hooks.callHook("pairing:repair", {
|
|
3355
|
+
for (const r of reminted.reminted) Promise.resolve(ctx.hooks.callHook("pairing:repair", {
|
|
3211
3356
|
mode: "duplicate-tool-use-remint",
|
|
3212
3357
|
callId: r.oldId,
|
|
3213
3358
|
messageIndex: ctx.turns.length,
|
|
3214
3359
|
turnId
|
|
3215
|
-
});
|
|
3360
|
+
})).catch(() => {});
|
|
3216
3361
|
}
|
|
3217
3362
|
}
|
|
3218
3363
|
if (turnTtftMs !== void 0 && result.usage.timeToFirstTokenMs === void 0) result.usage.timeToFirstTokenMs = turnTtftMs;
|
|
@@ -3582,10 +3727,31 @@ async function runSingleToolDispatch(ctx, call, turnId, fixed) {
|
|
|
3582
3727
|
perCallAbort.signal.addEventListener("abort", onAbort, { once: true });
|
|
3583
3728
|
removeAbortListener = () => perCallAbort.signal.removeEventListener("abort", onAbort);
|
|
3584
3729
|
});
|
|
3730
|
+
let removeRunAbortListener;
|
|
3731
|
+
let runAbortTimer;
|
|
3732
|
+
const runAbortPromise = new Promise((_, reject) => {
|
|
3733
|
+
const forceReject = () => reject(new DOMException("Aborted", "AbortError"));
|
|
3734
|
+
const armGrace = () => {
|
|
3735
|
+
runAbortTimer = setTimeout(forceReject, runAbortGraceMs());
|
|
3736
|
+
runAbortTimer.unref?.();
|
|
3737
|
+
};
|
|
3738
|
+
if (ctx.signal.aborted) {
|
|
3739
|
+
armGrace();
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
ctx.signal.addEventListener("abort", armGrace, { once: true });
|
|
3743
|
+
removeRunAbortListener = () => ctx.signal.removeEventListener("abort", armGrace);
|
|
3744
|
+
});
|
|
3585
3745
|
try {
|
|
3586
|
-
output = await Promise.race([
|
|
3746
|
+
output = await Promise.race([
|
|
3747
|
+
bodyPromise,
|
|
3748
|
+
cancellationPromise,
|
|
3749
|
+
runAbortPromise
|
|
3750
|
+
]);
|
|
3587
3751
|
} finally {
|
|
3588
3752
|
removeAbortListener?.();
|
|
3753
|
+
removeRunAbortListener?.();
|
|
3754
|
+
if (runAbortTimer !== void 0) clearTimeout(runAbortTimer);
|
|
3589
3755
|
}
|
|
3590
3756
|
} catch (err) {
|
|
3591
3757
|
const isOurSentinel = err instanceof Error && err.message === CANCELLED_BY_USER_SENTINEL;
|
|
@@ -4419,13 +4585,43 @@ function buildPromptMessage(provider, parts) {
|
|
|
4419
4585
|
const DEFAULT_BLOCK_THRESHOLD = 4;
|
|
4420
4586
|
const DEFAULT_ABORT_THRESHOLD = 8;
|
|
4421
4587
|
/**
|
|
4422
|
-
*
|
|
4423
|
-
*
|
|
4424
|
-
*
|
|
4425
|
-
*
|
|
4588
|
+
* Tools watched in sliding-window mode even without an explicit `windowSize`.
|
|
4589
|
+
* A re-read loop cycles through a handful of slices of the same file (offset
|
|
4590
|
+
* 1 → 52 → 199 → 1 …), so consecutive-streak counting — which resets on every
|
|
4591
|
+
* changed offset — never trips. Exact-payload keying in a window catches the
|
|
4592
|
+
* revisited slice while leaving linear chunked reads (distinct slices, never
|
|
4593
|
+
* repeated) untouched. Other tracked tools (shell, SQL) keep streak mode.
|
|
4594
|
+
*/
|
|
4595
|
+
const READ_LOOP_TOOLS = new Set(["read_file"]);
|
|
4596
|
+
/**
|
|
4597
|
+
* Default window for {@link READ_LOOP_TOOLS} when the consumer sets no global
|
|
4598
|
+
* `windowSize`. Sized so a short revisit cycle still reaches the abort
|
|
4599
|
+
* threshold: a 3-slice cycle fills a 24-call window with 8 occurrences of each
|
|
4600
|
+
* slice.
|
|
4601
|
+
*/
|
|
4602
|
+
const DEFAULT_READ_LOOP_WINDOW = 24;
|
|
4603
|
+
/**
|
|
4604
|
+
* Universal repeat ceiling: the last-resort loop breaker. Independent of the
|
|
4605
|
+
* tracked-tool set, it counts exact `(tool, args)` repeats across EVERY tool —
|
|
4606
|
+
* catching loops the per-tool streak/window misses (a `grep`/`glob`/`edit`
|
|
4607
|
+
* cycle, or a model hammering a tool that keeps getting blocked). Set high
|
|
4608
|
+
* enough that legitimate identical repeats (a handful of retries) don't trip,
|
|
4609
|
+
* but a genuine spin does.
|
|
4610
|
+
*/
|
|
4611
|
+
const DEFAULT_GLOBAL_CEILING = 12;
|
|
4612
|
+
/** Sliding window (per run+tool) the global ceiling counts repeats within. */
|
|
4613
|
+
const DEFAULT_GLOBAL_CEILING_WINDOW = 40;
|
|
4614
|
+
/**
|
|
4615
|
+
* Default tracked-tool predicate: the built-in `shell` and `read_file` tools.
|
|
4616
|
+
* `shell` is an auto-approved, side-effect-light retry magnet; `read_file` is
|
|
4617
|
+
* the re-read loop magnet — a compaction/dedup interaction can strand the
|
|
4618
|
+
* model re-reading one file forever (see {@link READ_LOOP_TOOLS}). Both are the
|
|
4619
|
+
* loop shapes the abort escalation exists for. Consumers that want to track
|
|
4620
|
+
* additional tools (e.g. an MCP query runner) pass them via
|
|
4621
|
+
* {@link RepeatGuardConfig.tools}.
|
|
4426
4622
|
*/
|
|
4427
4623
|
function defaultRepeatGuardTracked(name) {
|
|
4428
|
-
return name === "shell" || name
|
|
4624
|
+
return name === "shell" || name === "read_file";
|
|
4429
4625
|
}
|
|
4430
4626
|
function compileMatchers(tools) {
|
|
4431
4627
|
if (tools === void 0) return defaultRepeatGuardTracked;
|
|
@@ -4471,10 +4667,12 @@ function normalizeShellCommand(command) {
|
|
|
4471
4667
|
}
|
|
4472
4668
|
/**
|
|
4473
4669
|
* Built-in normalizer used when the consumer doesn't supply one. Shell-shaped
|
|
4474
|
-
* tools get {@link normalizeShellCommand};
|
|
4475
|
-
*
|
|
4476
|
-
*
|
|
4477
|
-
*
|
|
4670
|
+
* tools get {@link normalizeShellCommand}; everything else falls back to a
|
|
4671
|
+
* stable JSON encoding. Returns `undefined` when there's no meaningful payload
|
|
4672
|
+
* to key on (so the call is excluded from streak tracking rather than keyed on
|
|
4673
|
+
* `'{}'`). Consumers needing semantic keying for their own tools (e.g.
|
|
4674
|
+
* collapsing a query's whitespace) supply a normalizer via
|
|
4675
|
+
* {@link RepeatGuardConfig.normalize}.
|
|
4478
4676
|
*/
|
|
4479
4677
|
function defaultRepeatGuardNormalize(name, input) {
|
|
4480
4678
|
if (name === "shell") {
|
|
@@ -4482,11 +4680,6 @@ function defaultRepeatGuardNormalize(name, input) {
|
|
|
4482
4680
|
if (typeof cmd !== "string" || cmd.trim().length === 0) return void 0;
|
|
4483
4681
|
return `shell:${normalizeShellCommand(cmd)}`;
|
|
4484
4682
|
}
|
|
4485
|
-
if (name.endsWith("_execute_sql")) {
|
|
4486
|
-
const q = input.query ?? input.sql ?? input.statement;
|
|
4487
|
-
if (typeof q !== "string" || q.trim().length === 0) return void 0;
|
|
4488
|
-
return `sql:${q.replace(/\s+/g, " ").trim().toLowerCase()}`;
|
|
4489
|
-
}
|
|
4490
4683
|
try {
|
|
4491
4684
|
return `json:${stableStringify(input)}`;
|
|
4492
4685
|
} catch {
|
|
@@ -4528,6 +4721,7 @@ function formatBlockReason(reason, name, count) {
|
|
|
4528
4721
|
function installRepeatGuard(hooks, getConfig, abort) {
|
|
4529
4722
|
const streaks = /* @__PURE__ */ new Map();
|
|
4530
4723
|
const windows = /* @__PURE__ */ new Map();
|
|
4724
|
+
const globalWindows = /* @__PURE__ */ new Map();
|
|
4531
4725
|
function streakKey(runId, name) {
|
|
4532
4726
|
return `${runId ?? "-"}::${name}`;
|
|
4533
4727
|
}
|
|
@@ -4540,20 +4734,60 @@ function installRepeatGuard(hooks, getConfig, abort) {
|
|
|
4540
4734
|
const reachable = Number.isFinite(abortThreshold) ? abortThreshold : blockThreshold;
|
|
4541
4735
|
windowSize = Math.max(Math.floor(config.windowSize), reachable);
|
|
4542
4736
|
}
|
|
4737
|
+
const rawGlobalCeiling = typeof config.globalCeiling === "number" && Number.isFinite(config.globalCeiling) ? Math.floor(config.globalCeiling) : DEFAULT_GLOBAL_CEILING;
|
|
4738
|
+
const globalCeiling = rawGlobalCeiling > 0 && Number.isFinite(abortThreshold) ? Math.max(2, rawGlobalCeiling, abortThreshold) : 0;
|
|
4739
|
+
const globalWindow = Math.max(globalCeiling, typeof config.globalCeilingWindow === "number" && Number.isFinite(config.globalCeilingWindow) ? Math.floor(config.globalCeilingWindow) : DEFAULT_GLOBAL_CEILING_WINDOW);
|
|
4740
|
+
const isGloballyExcluded = config.globalCeilingExclude === void 0 ? () => false : compileMatchers(config.globalCeilingExclude);
|
|
4543
4741
|
return {
|
|
4544
4742
|
isTracked: compileMatchers(config.tools),
|
|
4545
4743
|
normalize: config.normalize ?? defaultRepeatGuardNormalize,
|
|
4546
4744
|
blockThreshold,
|
|
4547
4745
|
abortThreshold,
|
|
4548
4746
|
countBlockedCalls: config.countBlockedCalls === true,
|
|
4549
|
-
windowSize
|
|
4747
|
+
windowSize,
|
|
4748
|
+
globalCeiling,
|
|
4749
|
+
globalWindow,
|
|
4750
|
+
isGloballyExcluded
|
|
4550
4751
|
};
|
|
4551
4752
|
}
|
|
4552
4753
|
async function gateHandler(ctx) {
|
|
4553
4754
|
if (ctx.result !== void 0) return;
|
|
4554
4755
|
const config = getConfig();
|
|
4555
4756
|
if (!config) return;
|
|
4556
|
-
const
|
|
4757
|
+
const resolved = resolve(config);
|
|
4758
|
+
const { isTracked, normalize, blockThreshold, abortThreshold, countBlockedCalls, windowSize } = resolved;
|
|
4759
|
+
if (resolved.globalCeiling > 0 && !resolved.isGloballyExcluded(ctx.name)) {
|
|
4760
|
+
let gkey;
|
|
4761
|
+
try {
|
|
4762
|
+
gkey = `${ctx.name}\u0000${stableStringify(ctx.input)}`;
|
|
4763
|
+
} catch {
|
|
4764
|
+
gkey = "";
|
|
4765
|
+
}
|
|
4766
|
+
if (gkey.length > 0) {
|
|
4767
|
+
const gslot = streakKey(ctx.runId, ctx.name);
|
|
4768
|
+
const recent = globalWindows.get(gslot) ?? [];
|
|
4769
|
+
recent.push(gkey);
|
|
4770
|
+
if (recent.length > resolved.globalWindow) recent.shift();
|
|
4771
|
+
globalWindows.set(gslot, recent);
|
|
4772
|
+
let gcount = 0;
|
|
4773
|
+
for (const k of recent) if (k === gkey) gcount++;
|
|
4774
|
+
if (gcount >= resolved.globalCeiling) {
|
|
4775
|
+
if (!ctx.block) {
|
|
4776
|
+
ctx.block = true;
|
|
4777
|
+
ctx.reason = formatBlockReason(config.blockReason, ctx.name, gcount);
|
|
4778
|
+
}
|
|
4779
|
+
await hooks.callHook("repeat-guard:exceeded", {
|
|
4780
|
+
tool: ctx.name,
|
|
4781
|
+
count: gcount,
|
|
4782
|
+
threshold: resolved.globalCeiling,
|
|
4783
|
+
turnId: ctx.turnId,
|
|
4784
|
+
action: "abort"
|
|
4785
|
+
});
|
|
4786
|
+
abort();
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4557
4791
|
if (!isTracked(ctx.name)) return;
|
|
4558
4792
|
if (ctx.block && !countBlockedCalls) return;
|
|
4559
4793
|
let key;
|
|
@@ -4565,11 +4799,12 @@ function installRepeatGuard(hooks, getConfig, abort) {
|
|
|
4565
4799
|
if (typeof key !== "string" || key.length === 0) return;
|
|
4566
4800
|
const blockedByPriorGate = ctx.block;
|
|
4567
4801
|
const slot = streakKey(ctx.runId, ctx.name);
|
|
4802
|
+
const effectiveWindow = windowSize ?? (READ_LOOP_TOOLS.has(ctx.name) ? Math.max(DEFAULT_READ_LOOP_WINDOW, Number.isFinite(abortThreshold) ? abortThreshold : DEFAULT_READ_LOOP_WINDOW) : void 0);
|
|
4568
4803
|
let count;
|
|
4569
|
-
if (
|
|
4804
|
+
if (effectiveWindow !== void 0) {
|
|
4570
4805
|
const recent = windows.get(slot) ?? [];
|
|
4571
4806
|
recent.push(key);
|
|
4572
|
-
if (recent.length >
|
|
4807
|
+
if (recent.length > effectiveWindow) recent.shift();
|
|
4573
4808
|
windows.set(slot, recent);
|
|
4574
4809
|
count = 0;
|
|
4575
4810
|
for (const k of recent) if (k === key) count++;
|
|
@@ -4613,6 +4848,7 @@ function installRepeatGuard(hooks, getConfig, abort) {
|
|
|
4613
4848
|
unregister();
|
|
4614
4849
|
streaks.clear();
|
|
4615
4850
|
windows.clear();
|
|
4851
|
+
globalWindows.clear();
|
|
4616
4852
|
};
|
|
4617
4853
|
}
|
|
4618
4854
|
//#endregion
|
|
@@ -5264,36 +5500,6 @@ function createSkillsReadTool(options) {
|
|
|
5264
5500
|
};
|
|
5265
5501
|
}
|
|
5266
5502
|
//#endregion
|
|
5267
|
-
//#region src/tools/shell-quote.ts
|
|
5268
|
-
/**
|
|
5269
|
-
* Shared shell-argument quoter for tool implementations.
|
|
5270
|
-
*
|
|
5271
|
-
* Single source of truth so `grep`, `binary-read`, and `skills-run-script`
|
|
5272
|
-
* don't drift on the POSIX `'\''` escape pattern.
|
|
5273
|
-
*/
|
|
5274
|
-
const SAFE_TOKEN_RE = /^[\w@%+=:,./-]+$/;
|
|
5275
|
-
const SINGLE_QUOTE_RE = /'/g;
|
|
5276
|
-
/**
|
|
5277
|
-
* Wrap an argument in single quotes, escaping embedded single quotes via the
|
|
5278
|
-
* standard POSIX `'\''` close-escape-reopen trick. Tokens that are already
|
|
5279
|
-
* shell-safe pass through unchanged so command lines stay readable in logs.
|
|
5280
|
-
*
|
|
5281
|
-
* NOT a sandbox — only safe when the caller controls the surrounding shell
|
|
5282
|
-
* context. Use it for arguments only, never for the verb / subcommand.
|
|
5283
|
-
*/
|
|
5284
|
-
function shellQuote(arg) {
|
|
5285
|
-
if (SAFE_TOKEN_RE.test(arg)) return arg;
|
|
5286
|
-
return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
|
|
5287
|
-
}
|
|
5288
|
-
/**
|
|
5289
|
-
* Variant that always quotes — useful when the caller doesn't want a
|
|
5290
|
-
* conditional `unquoted-when-safe` branch (consistent log shape, paranoid
|
|
5291
|
-
* inputs that contain whitespace by construction).
|
|
5292
|
-
*/
|
|
5293
|
-
function alwaysQuote(arg) {
|
|
5294
|
-
return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
|
|
5295
|
-
}
|
|
5296
|
-
//#endregion
|
|
5297
5503
|
//#region src/tools/skills-run-script.ts
|
|
5298
5504
|
const ABS_WINDOWS_RE = /^[a-z]:[\\/]/i;
|
|
5299
5505
|
const COLLAPSE_SLASHES_RE = /\/+/g;
|
|
@@ -5441,7 +5647,15 @@ function createSkillsUseTool(options) {
|
|
|
5441
5647
|
if (!skill.instructions.includes("!`")) body = skill.instructions;
|
|
5442
5648
|
else if (options.allowShellInterpolation !== false) {
|
|
5443
5649
|
const failures = [];
|
|
5444
|
-
|
|
5650
|
+
const approve = options.approveShellInterpolation;
|
|
5651
|
+
body = await interpolateShellCommands(skill.instructions, ctx.execution, ctx.handle, {
|
|
5652
|
+
onFailure: (command) => failures.push(command),
|
|
5653
|
+
...approve ? { approve: (command) => approve({
|
|
5654
|
+
skillName,
|
|
5655
|
+
source: skill.source,
|
|
5656
|
+
command
|
|
5657
|
+
}) } : {}
|
|
5658
|
+
});
|
|
5445
5659
|
if (failures.length > 0) body += `\n\nWarning: ${failures.length} shell interpolation command${failures.length === 1 ? "" : "s"} failed during activation (${failures.map((c) => `\`${c}\``).join(", ")}). The instructions above may be missing dynamic content — see the inline [command failed …] markers.`;
|
|
5446
5660
|
} else body = stripShellInterpolations(skill.instructions);
|
|
5447
5661
|
interpolatedBodyCache.set(skillName, body);
|
|
@@ -6227,7 +6441,7 @@ function installLazyDisclosureGate(hooks, lazyCanonicalNames, unlocked, discover
|
|
|
6227
6441
|
*
|
|
6228
6442
|
* # MCP Server Instructions
|
|
6229
6443
|
*
|
|
6230
|
-
* ##
|
|
6444
|
+
* ## acme
|
|
6231
6445
|
* The project is provisioned. Use `apply_migration` directly.
|
|
6232
6446
|
*
|
|
6233
6447
|
* ## linear
|
|
@@ -6273,12 +6487,12 @@ function childRunIdSet(session) {
|
|
|
6273
6487
|
* Throws on an unrecoverable resume request; otherwise returns the filtered
|
|
6274
6488
|
* turns (or `undefined` when the session has no turns).
|
|
6275
6489
|
*/
|
|
6276
|
-
function validateAndPrepareResume(session, prompt) {
|
|
6490
|
+
function validateAndPrepareResume(session, prompt, hasPendingTaskNotifications = false) {
|
|
6277
6491
|
const hasSessionTurns = !!session && session.turns.length > 0;
|
|
6278
|
-
if (!prompt && !hasSessionTurns) throw new Error("prompt is required when no session with existing turns is provided");
|
|
6492
|
+
if (!prompt && !hasSessionTurns && !hasPendingTaskNotifications) throw new Error("prompt is required when no session with existing turns is provided");
|
|
6279
6493
|
let resumeFilteredTurns;
|
|
6280
6494
|
if (hasSessionTurns) resumeFilteredTurns = filterUnresolvedToolUses(session.turns);
|
|
6281
|
-
if (!prompt && resumeFilteredTurns) {
|
|
6495
|
+
if (!prompt && resumeFilteredTurns && !hasPendingTaskNotifications) {
|
|
6282
6496
|
const lastTurn = resumeFilteredTurns.at(-1);
|
|
6283
6497
|
if (lastTurn && lastTurn.role !== "user") {
|
|
6284
6498
|
const detail = detectTurnInterruption(resumeFilteredTurns) === "completed" ? "last turn is a completed assistant message" : "last turn is mid-stream assistant content";
|
|
@@ -6386,7 +6600,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
6386
6600
|
const skillActivationState = createSkillActivationState({ maxActive: skillsConfig?.maxActive });
|
|
6387
6601
|
async function run(options) {
|
|
6388
6602
|
if (running) throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
|
|
6389
|
-
const resumeFilteredTurns = validateAndPrepareResume(session, options.prompt);
|
|
6603
|
+
const resumeFilteredTurns = validateAndPrepareResume(session, options.prompt, pendingTaskNotifications.size > 0);
|
|
6390
6604
|
const clock = options.clock ?? agentClock ?? DEFAULT_AGENT_CLOCK;
|
|
6391
6605
|
let externalAbortListener;
|
|
6392
6606
|
const externalSignal = options.signal;
|
|
@@ -6472,6 +6686,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
6472
6686
|
const thinking = options.thinking ?? "off";
|
|
6473
6687
|
const model = options.model ?? provider.meta.defaultModel;
|
|
6474
6688
|
const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
|
|
6689
|
+
if (provider.meta?.clearsContextServerSide) resolvedBehavior.dedupReads = false;
|
|
6475
6690
|
const { maxConcurrentTools, toolBatchExecutor, shouldRethrowToolError, maxTurns, maxTurnsWarning, maxCostUsd, maxTotalTokens, maxWallMs, maxTokens, retry, thinkingBudget, modelOptions: behaviorModelOptions, schema, cache, toolOutputBudget, toolOutputBudgetExcludeTools, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, repeatGuard, elideStaleReads, toolDisclosure, mcpToolNameSeparator, toolSearch, surfaceMcpInstructions, persistThreshold, persistExcludeTools, persistDir, persistMaxBytes, strictToolPairing, maxPairingRepairsPerTurn, maxConsecutivePauseTurns, persistTurns } = resolvedBehavior;
|
|
6476
6691
|
const modelOptions = options.modelOptions ?? behaviorModelOptions;
|
|
6477
6692
|
let system = options.system || agentSystem || "You are a helpful assistant.";
|
|
@@ -6492,7 +6707,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
6492
6707
|
catalog: resolvedSkills,
|
|
6493
6708
|
state: skillActivationState,
|
|
6494
6709
|
hooks,
|
|
6495
|
-
allowShellInterpolation: skillsConfig?.allowShellInterpolation
|
|
6710
|
+
allowShellInterpolation: skillsConfig?.allowShellInterpolation,
|
|
6711
|
+
approveShellInterpolation: skillsConfig?.approveShellInterpolation
|
|
6496
6712
|
}),
|
|
6497
6713
|
skills_read: createSkillsReadTool({
|
|
6498
6714
|
catalog: resolvedSkills,
|
|
@@ -7309,6 +7525,9 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
7309
7525
|
abort,
|
|
7310
7526
|
cancelTool,
|
|
7311
7527
|
killBackgroundTask,
|
|
7528
|
+
get hasPendingTaskNotifications() {
|
|
7529
|
+
return pendingTaskNotifications.size > 0;
|
|
7530
|
+
},
|
|
7312
7531
|
steer,
|
|
7313
7532
|
followUp: followUpFn,
|
|
7314
7533
|
waitForIdle,
|
|
@@ -8254,7 +8473,7 @@ const readFile$1 = {
|
|
|
8254
8473
|
const prior = readState.get(absKey);
|
|
8255
8474
|
if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey && prior.lineNumbers === showLineNumbers && prior.elided !== true) {
|
|
8256
8475
|
rememberRead();
|
|
8257
|
-
return `File ${path} unchanged since the previous read in this session — the prior result is still current
|
|
8476
|
+
return `File ${path} unchanged since the previous read in this session — the prior result above is still current, so this duplicate read was skipped to save tokens.`;
|
|
8258
8477
|
}
|
|
8259
8478
|
}
|
|
8260
8479
|
if (looksBinary(raw)) return `[binary file: ${path}, ${totalBytes} bytes; use shell with hexdump | xxd | od to inspect]`;
|
|
@@ -8782,7 +9001,7 @@ function createSpawnTool(options = {}) {
|
|
|
8782
9001
|
const childHandle = agent.handle;
|
|
8783
9002
|
if (childHandle && ctx.execution.reassignBackgroundTasks) try {
|
|
8784
9003
|
const reassigned = await ctx.execution.reassignBackgroundTasks(childHandle, ctx.handle, (info) => {
|
|
8785
|
-
ctx.hooks.callHook("background:exit", info);
|
|
9004
|
+
Promise.resolve(ctx.hooks.callHook("background:exit", info)).catch(() => {});
|
|
8786
9005
|
});
|
|
8787
9006
|
for (const entry of reassigned) await ctx.hooks.callHook("background:reassign", {
|
|
8788
9007
|
taskId: entry.taskId,
|
|
@@ -8903,6 +9122,6 @@ const writeFile$1 = {
|
|
|
8903
9122
|
}
|
|
8904
9123
|
};
|
|
8905
9124
|
//#endregion
|
|
8906
|
-
export {
|
|
9125
|
+
export { restoreModelOptions as $, PERSISTENCE_PREVIEW_BYTES as A, cerebrasDescriptor as B, stableStringify as C, TOOL_USE_SKIPPED_MESSAGE as D, TOOL_USE_CANCELLED_MESSAGE as E, resolvePersistDir as F, getModelInfo as G, effectiveContextWindow as H, resolveTasksDir as I, modelSupportsReasoning as J, localDescriptor as K, BUILTIN_PROVIDERS as L, cleanupPersistedSession as M, maybePersistToolResult as N, validateToolArgs as O, resolveMcpWarningsDir as P, piIdOf as Q, OUTPUT_RESERVE_TOKENS as R, normalizeShellCommand as S, SHELL_CASCADE_CANCEL_MESSAGE as T, enabledModelOptions as U, credKeyOf as V, getContextWindow as W, openaiDescriptor as X, modelsForDescriptor as Y, openrouterDescriptor as Z, createSkillsReadTool as _, multiEdit as a, defaultRepeatGuardNormalize as b, grep as c, createAgent as d, WAIT_TASK_TIMED_OUT_PREFIX as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, buildPersistedStub as j, PERSISTED_STUB_PREFIX as k, glob$1 as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, waitTask as p, modelOptionsFor as q, shellKill as r, createInteractionTool as s, writeFile$1 as t, edit as u, createShellTool as v, INTERRUPT_MESSAGE_FOR_TOOL_USE as w, defaultRepeatGuardTracked as x, shell as y, anthropicDescriptor as z };
|
|
8907
9126
|
|
|
8908
|
-
//# sourceMappingURL=tools-
|
|
9127
|
+
//# sourceMappingURL=tools-ZHKOh44k.js.map
|