zidane 5.4.2 → 5.5.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 +45 -1
- package/dist/{agent-DxBoKDba.d.ts → agent-CvImMxMQ.d.ts} +256 -5
- package/dist/agent-CvImMxMQ.d.ts.map +1 -0
- package/dist/chat.d.ts +137 -16
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +3 -2
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts-DhmMlT2W.js +472 -0
- package/dist/contexts-DhmMlT2W.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/{errors-Byb0F8B9.js → errors-CDwtPIMX.js} +4 -2
- package/dist/{errors-Byb0F8B9.js.map → errors-CDwtPIMX.js.map} +1 -1
- package/dist/{index-BOtXdQkW.d.ts → index-B0uc2C5x.d.ts} +9 -3
- package/dist/index-B0uc2C5x.d.ts.map +1 -0
- package/dist/{index-BiO_5Hm4.d.ts → index-CbS75MD3.d.ts} +2 -2
- package/dist/index-CbS75MD3.d.ts.map +1 -0
- package/dist/{index-B2VOOijU.d.ts → index-CtXksgqb.d.ts} +73 -4
- package/dist/index-CtXksgqb.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +11 -11
- package/dist/{interpolate-ERgZUxgg.js → interpolate-BaaKaKzN.js} +156 -19
- package/dist/interpolate-BaaKaKzN.js.map +1 -0
- package/dist/{login-CJbeAadS.js → login-iTy-0wYz.js} +3 -3
- package/dist/{login-CJbeAadS.js.map → login-iTy-0wYz.js.map} +1 -1
- package/dist/{mcp-DhmmJfxK.js → mcp-CNUbvbsy.js} +2 -2
- package/dist/{mcp-DhmmJfxK.js.map → mcp-CNUbvbsy.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-D0xT979U.js → messages-fTR19Ga6.js} +2 -2
- package/dist/{messages-D0xT979U.js.map → messages-fTR19Ga6.js.map} +1 -1
- package/dist/{presets-MCcvxiNT.js → presets-h6UWhghO.js} +3 -2
- package/dist/presets-h6UWhghO.js.map +1 -0
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/{providers-x3LZByR5.js → providers-G0VBZK9j.js} +4 -4
- package/dist/{providers-x3LZByR5.js.map → providers-G0VBZK9j.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +2 -2
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +2 -1
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-BHZwxmfr.js → session-CbkiJDlH.js} +3 -2
- package/dist/session-CbkiJDlH.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -1
- package/dist/{tools-BNfyY14s.js → tools-D_icxa-V.js} +813 -284
- package/dist/tools-D_icxa-V.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +2 -2
- package/dist/{transcript-anchors-DonKvoh4.d.ts → transcript-anchors-3FFw2xuk.d.ts} +98 -15
- package/dist/transcript-anchors-3FFw2xuk.d.ts.map +1 -0
- package/dist/tui.d.ts +29 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +879 -70
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-TKvy0q29.js → turn-operations-CtgBlBHn.js} +412 -125
- package/dist/turn-operations-CtgBlBHn.js.map +1 -0
- package/dist/types-IcokUOyC.js.map +1 -1
- package/dist/types-KukEp-mi.d.ts +253 -0
- package/dist/types-KukEp-mi.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +1 -1
- package/docs/ARCHITECTURE.md +37 -3
- package/docs/CHAT.md +4 -2
- package/docs/RUN_IN_BACKGROUND.md +612 -0
- package/docs/SKILL.md +83 -14
- package/docs/TUI.md +40 -2
- package/package.json +4 -4
- package/dist/agent-DxBoKDba.d.ts.map +0 -1
- package/dist/contexts-BwiHIr2w.js +0 -129
- package/dist/contexts-BwiHIr2w.js.map +0 -1
- package/dist/index-B2VOOijU.d.ts.map +0 -1
- package/dist/index-BOtXdQkW.d.ts.map +0 -1
- package/dist/index-BiO_5Hm4.d.ts.map +0 -1
- package/dist/interpolate-ERgZUxgg.js.map +0 -1
- package/dist/presets-MCcvxiNT.js.map +0 -1
- package/dist/session-BHZwxmfr.js.map +0 -1
- package/dist/tools-BNfyY14s.js.map +0 -1
- package/dist/transcript-anchors-DonKvoh4.d.ts.map +0 -1
- package/dist/turn-operations-TKvy0q29.js.map +0 -1
- package/dist/types-Ce78ds4h.d.ts +0 -88
- package/dist/types-Ce78ds4h.d.ts.map +0 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { n as createProcessContext } from "./contexts-
|
|
2
|
-
import { a as AgentToolPairingError, l as toTypedError, r as AgentProviderError, s as errorMessage, t as AgentAbortedError } from "./errors-
|
|
1
|
+
import { n as createProcessContext } from "./contexts-DhmMlT2W.js";
|
|
2
|
+
import { a as AgentToolPairingError, l as toTypedError, r as AgentProviderError, s as errorMessage, t as AgentAbortedError } from "./errors-CDwtPIMX.js";
|
|
3
3
|
import { t as toolOutputByteLength } from "./types-IcokUOyC.js";
|
|
4
|
-
import { a as detectTurnInterruption, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureToolResultPairing, s as filterUnresolvedToolUses } from "./messages-
|
|
5
|
-
import { t as connectMcpServers } from "./mcp-
|
|
6
|
-
import { _ as validateResourcePath, b as createSkillActivationState, d as escapeXml, n as resolveSkills, p as installAllowedToolsGate, t as interpolateShellCommands, u as buildCatalog } from "./interpolate-
|
|
4
|
+
import { a as detectTurnInterruption, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureToolResultPairing, s as filterUnresolvedToolUses } from "./messages-fTR19Ga6.js";
|
|
5
|
+
import { t as connectMcpServers } from "./mcp-CNUbvbsy.js";
|
|
6
|
+
import { _ as validateResourcePath, b as createSkillActivationState, d as escapeXml, n as resolveSkills, p as installAllowedToolsGate, t as interpolateShellCommands, u as buildCatalog } from "./interpolate-BaaKaKzN.js";
|
|
7
7
|
import { n as formatTokenUsage, t as flattenTurns } from "./stats-DgOvY7wd.js";
|
|
8
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
9
|
import { createHooks } from "hookable";
|
|
10
|
+
import { homedir } from "node:os";
|
|
9
11
|
import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
10
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
11
12
|
import { Buffer } from "node:buffer";
|
|
12
13
|
//#region src/aliasing.ts
|
|
13
14
|
/**
|
|
@@ -103,6 +104,128 @@ function rewriteMessagesToWire(messages, maps) {
|
|
|
103
104
|
}));
|
|
104
105
|
}
|
|
105
106
|
//#endregion
|
|
107
|
+
//#region src/chat/format.ts
|
|
108
|
+
/** Compact token formatter — 12_415 → "12.4k", 1_234_567 → "1.23M". */
|
|
109
|
+
function fmtTokens(n) {
|
|
110
|
+
if (n < 1e3) return String(n);
|
|
111
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 2 : 1)}k`;
|
|
112
|
+
return `${(n / 1e6).toFixed(2)}M`;
|
|
113
|
+
}
|
|
114
|
+
/** Compact relative-time formatter — "just now / 5m / 3h / 2d". */
|
|
115
|
+
function ageString(ts, now = Date.now()) {
|
|
116
|
+
const m = Math.floor((now - ts) / 6e4);
|
|
117
|
+
if (m < 1) return "just now";
|
|
118
|
+
if (m < 60) return `${m}m ago`;
|
|
119
|
+
const h = Math.floor(m / 60);
|
|
120
|
+
if (h < 24) return `${h}h ago`;
|
|
121
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
122
|
+
}
|
|
123
|
+
/** Six-char short form of a session id for headers and lists. */
|
|
124
|
+
function shortId(id) {
|
|
125
|
+
return id.replace(/-/g, "").slice(0, 6);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Single-line preview of a multi-line string, capped at `max` chars and
|
|
129
|
+
* ellipsis-terminated when truncated.
|
|
130
|
+
*
|
|
131
|
+
* Whitespace runs (newlines, tabs, multiple spaces) collapse into one
|
|
132
|
+
* space so the rendered output stays on a single visual row no matter
|
|
133
|
+
* how the input was shaped. Used by every transcript "preview" surface
|
|
134
|
+
* (spawn-start task, `tool: shell (background): <command>`,
|
|
135
|
+
* `<task-notification>` summary line, etc.) — without the whitespace
|
|
136
|
+
* collapse, a 60-char `slice` on a string with an inline `\n\n` paints
|
|
137
|
+
* the second paragraph below the first, producing the visible
|
|
138
|
+
* "preview text spills onto multiple lines" bug (and, downstream,
|
|
139
|
+
* misaligned spawn markers when the wrapped lines collide with
|
|
140
|
+
* other events).
|
|
141
|
+
*
|
|
142
|
+
* Reserves one slot for the `…` so the displayed width is exactly
|
|
143
|
+
* `max` when truncation kicks in.
|
|
144
|
+
*/
|
|
145
|
+
function previewLine(s, max) {
|
|
146
|
+
const single = s.replace(/\s+/g, " ").trim();
|
|
147
|
+
if (single.length <= max) return single;
|
|
148
|
+
return `${single.slice(0, max - 1)}…`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Compact human-readable duration formatter shared by background-task
|
|
152
|
+
* surfaces (the `<task-notification>` summary, the TUI banner, the
|
|
153
|
+
* `shell_kill` tool result, etc.).
|
|
154
|
+
*
|
|
155
|
+
* Format ladder:
|
|
156
|
+
* - `< 1s` → `"Nms"`
|
|
157
|
+
* - `< 10s` → `"N.Ns"` (one decimal)
|
|
158
|
+
* - `< 1m` → `"Ns"` (whole seconds)
|
|
159
|
+
* - `< 1h` → `"NmNs"` / `"Nm"` when seconds round to 0
|
|
160
|
+
* - `≥ 1h` → `"NhNm"` / `"Nh"` when minutes round to 0
|
|
161
|
+
*
|
|
162
|
+
* Single source of truth so a 60s task renders the same across the
|
|
163
|
+
* model-facing XML summary and the user-facing banner. Earlier
|
|
164
|
+
* separate formatters disagreed (XML said `"60.0s"`, banner said `"1m"`)
|
|
165
|
+
* which was confusing to the user reading both side by side.
|
|
166
|
+
*/
|
|
167
|
+
function formatDuration(ms) {
|
|
168
|
+
if (ms < 0) ms = 0;
|
|
169
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
170
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)}s`;
|
|
171
|
+
const minutes = Math.floor(ms / 6e4);
|
|
172
|
+
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
173
|
+
if (minutes < 60) return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
|
|
174
|
+
const hours = Math.floor(minutes / 60);
|
|
175
|
+
const remMinutes = minutes % 60;
|
|
176
|
+
return remMinutes > 0 ? `${hours}h${remMinutes}m` : `${hours}h`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Status label for a terminated background task — `"exited <code>"`
|
|
180
|
+
* for natural exits, `"killed"` (with the signal name when known)
|
|
181
|
+
* for our-issued SIGTERMs.
|
|
182
|
+
*
|
|
183
|
+
* Pulled out as its own function so the `<task-notification>` XML
|
|
184
|
+
* summary, the TUI banner header, the `shell_kill` tool result, and
|
|
185
|
+
* future surfaces all read the same string.
|
|
186
|
+
*/
|
|
187
|
+
function formatTaskStatus(info) {
|
|
188
|
+
return info.status === "killed" ? `killed${info.signal ? ` (${info.signal})` : ""}` : `exited ${info.exitCode}`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* One-line summary of a terminated background task — the shape used by
|
|
192
|
+
* the `<task-notification>` XML's `<summary>` tag AND the TUI banner's
|
|
193
|
+
* `event.text` fallback string. Three dot-separated segments:
|
|
194
|
+
*
|
|
195
|
+
* `<command preview · status · duration>`
|
|
196
|
+
*
|
|
197
|
+
* Centralizes the format so live + replay + wire all agree, and so a
|
|
198
|
+
* future cosmetic tweak (separator glyph, segment ordering) lands in
|
|
199
|
+
* exactly one place.
|
|
200
|
+
*/
|
|
201
|
+
function formatTaskSummary(info, maxCommandChars = 80) {
|
|
202
|
+
return `${previewLine(info.command, maxCommandChars)} · ${formatTaskStatus(info)} · ${formatDuration(info.durationMs)}`;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Compact an absolute path for display: replace the user's `$HOME`
|
|
206
|
+
* prefix with `~` (so `/Users/yael/Code/zidane` → `~/Code/zidane`),
|
|
207
|
+
* and optionally left-truncate with an ellipsis when the result
|
|
208
|
+
* still exceeds `maxWidth` (so the path's *tail* — the part the user
|
|
209
|
+
* recognizes — stays visible: `…/zidane` rather than `/Users/yaeluil…`).
|
|
210
|
+
*
|
|
211
|
+
* `maxWidth` is the maximum *display width* in cells. Omit to skip
|
|
212
|
+
* truncation. Paths outside `$HOME` are returned verbatim modulo
|
|
213
|
+
* truncation. The ellipsis (`…`) counts as one cell.
|
|
214
|
+
*
|
|
215
|
+
* `home` overrides `os.homedir()` for tests; production callers leave
|
|
216
|
+
* it undefined and pay the cheap one-syscall lookup per call.
|
|
217
|
+
*/
|
|
218
|
+
function compactPath(path, maxWidth, home) {
|
|
219
|
+
const h = home ?? homedir();
|
|
220
|
+
let display = path;
|
|
221
|
+
if (h) {
|
|
222
|
+
if (path === h) display = "~";
|
|
223
|
+
else if (path.startsWith(`${h}/`)) display = `~${path.slice(h.length)}`;
|
|
224
|
+
}
|
|
225
|
+
if (maxWidth !== void 0 && maxWidth > 1 && display.length > maxWidth) return `…${display.slice(display.length - maxWidth + 1)}`;
|
|
226
|
+
return display;
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
106
229
|
//#region src/tools/read-state.ts
|
|
107
230
|
const STATE = /* @__PURE__ */ new WeakMap();
|
|
108
231
|
/**
|
|
@@ -293,6 +416,20 @@ function resolvePersistDir(opts) {
|
|
|
293
416
|
return join(opts.userDir, "tool-results", opts.sessionId);
|
|
294
417
|
}
|
|
295
418
|
/**
|
|
419
|
+
* Resolve the per-session background-tasks directory under
|
|
420
|
+
* `<userDir>/<sessionId>/tasks/`.
|
|
421
|
+
*
|
|
422
|
+
* The chat layer calls this at session activation and forwards the result
|
|
423
|
+
* via `behavior.tasksDir`. Same shape as {@link resolvePersistDir}: hosts
|
|
424
|
+
* get a single source of truth for "where do task log files live".
|
|
425
|
+
* Created on first write; cleanup is the session-delete path's job.
|
|
426
|
+
*/
|
|
427
|
+
function resolveTasksDir(opts) {
|
|
428
|
+
if (!isAbsolute(opts.userDir)) throw new Error(`resolveTasksDir: userDir must be absolute, got "${opts.userDir}"`);
|
|
429
|
+
if (!opts.sessionId) throw new Error("resolveTasksDir: sessionId must be a non-empty string");
|
|
430
|
+
return join(opts.userDir, opts.sessionId, "tasks");
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
296
433
|
* Decide-and-persist for a single tool result. Pure decision + filesystem
|
|
297
434
|
* side-effect; returns the new wire-level `output` string when substitution
|
|
298
435
|
* happened, otherwise tells the caller to leave the result alone.
|
|
@@ -812,6 +949,27 @@ const INTERRUPT_MESSAGE_FOR_TOOL_USE = "[Request interrupted by user for tool us
|
|
|
812
949
|
*/
|
|
813
950
|
const TOOL_USE_SKIPPED_MESSAGE = "[Tool use skipped — superseded by user message]";
|
|
814
951
|
/**
|
|
952
|
+
* Canonical tool_result text emitted when a single tool call is cancelled
|
|
953
|
+
* mid-flight via `agent.cancelTool(callId)` (typically the TUI's
|
|
954
|
+
* "cancel this tool" affordance). Distinguished from
|
|
955
|
+
* {@link INTERRUPT_MESSAGE_FOR_TOOL_USE} (run-wide user abort) and
|
|
956
|
+
* {@link TOOL_USE_SKIPPED_MESSAGE} (steered) so the model — and downstream
|
|
957
|
+
* consumers — can tell the three apart by string match.
|
|
958
|
+
*
|
|
959
|
+
* Always paired with `isError: true` on the wire so the model treats the
|
|
960
|
+
* call as failed rather than as a successful response. The remaining tool
|
|
961
|
+
* calls in the batch continue running, in contrast with a full-run abort.
|
|
962
|
+
*/
|
|
963
|
+
const TOOL_USE_CANCELLED_MESSAGE = "[Tool call cancelled by user]";
|
|
964
|
+
/**
|
|
965
|
+
* Sentinel message rejected from the per-call cancellation promise inside
|
|
966
|
+
* {@link executeSingleTool}'s race. Plain-string and module-private — only
|
|
967
|
+
* the surrounding code reads it (via the `perCallAbort.signal.aborted`
|
|
968
|
+
* guard, NOT the message itself), so it just needs to be a stable
|
|
969
|
+
* non-empty identifier the rejection can carry.
|
|
970
|
+
*/
|
|
971
|
+
const CANCELLED_BY_USER_SENTINEL = "zidane:tool:cancelled-by-user";
|
|
972
|
+
/**
|
|
815
973
|
* Compute the effective thinking budget for a given run-relative turn, given
|
|
816
974
|
* the configured decay schedule. Pure helper — exported for tests and so
|
|
817
975
|
* downstream tooling can preview decay curves without spinning up the loop.
|
|
@@ -1670,6 +1828,29 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1670
1828
|
const callId = call.id;
|
|
1671
1829
|
const displayName = toWireName(call.name, ctx.aliasMaps);
|
|
1672
1830
|
const runToolCounts = Object.freeze({ ...ctx.runToolCounts });
|
|
1831
|
+
const perCallAbort = new AbortController();
|
|
1832
|
+
ctx.pendingToolCancels?.set(callId, perCallAbort);
|
|
1833
|
+
try {
|
|
1834
|
+
return await runSingleToolDispatch(ctx, call, turnId, {
|
|
1835
|
+
toolDef,
|
|
1836
|
+
callId,
|
|
1837
|
+
displayName,
|
|
1838
|
+
runToolCounts,
|
|
1839
|
+
perCallAbort
|
|
1840
|
+
});
|
|
1841
|
+
} finally {
|
|
1842
|
+
ctx.pendingToolCancels?.delete(callId);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Body of {@link executeSingleTool}. Hoisted into its own function purely so
|
|
1847
|
+
* the per-call cancel registration (which spans every exit path) can sit in
|
|
1848
|
+
* a tight `try / finally` at the call site. Behavior is unchanged from the
|
|
1849
|
+
* pre-cancel implementation aside from the user-cancel branch in the
|
|
1850
|
+
* execute body — see {@link TOOL_USE_CANCELLED_MESSAGE}.
|
|
1851
|
+
*/
|
|
1852
|
+
async function runSingleToolDispatch(ctx, call, turnId, fixed) {
|
|
1853
|
+
const { toolDef, callId, displayName, runToolCounts, perCallAbort } = fixed;
|
|
1673
1854
|
const gateCtx = {
|
|
1674
1855
|
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, call.input),
|
|
1675
1856
|
block: false,
|
|
@@ -1795,12 +1976,14 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1795
1976
|
runToolCounts,
|
|
1796
1977
|
...coercions ? { coercions } : {}
|
|
1797
1978
|
});
|
|
1798
|
-
let output;
|
|
1979
|
+
let output = "";
|
|
1799
1980
|
let isError = false;
|
|
1981
|
+
let cancelledByUser = false;
|
|
1982
|
+
const childSignal = typeof AbortSignal.any === "function" ? AbortSignal.any([ctx.signal, perCallAbort.signal]) : ctx.signal;
|
|
1800
1983
|
try {
|
|
1801
1984
|
const toolCtx = {
|
|
1802
1985
|
provider: ctx.provider,
|
|
1803
|
-
signal:
|
|
1986
|
+
signal: childSignal,
|
|
1804
1987
|
execution: ctx.execution,
|
|
1805
1988
|
handle: ctx.handle,
|
|
1806
1989
|
hooks: ctx.hooks,
|
|
@@ -1819,16 +2002,50 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
1819
2002
|
...ctx.readState ? { readState: ctx.readState } : {},
|
|
1820
2003
|
...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
|
|
1821
2004
|
};
|
|
1822
|
-
|
|
2005
|
+
const bodyPromise = toolDef.execute(effectiveInput, toolCtx);
|
|
2006
|
+
bodyPromise.catch(() => {});
|
|
2007
|
+
let removeAbortListener;
|
|
2008
|
+
const cancellationPromise = new Promise((_, reject) => {
|
|
2009
|
+
if (perCallAbort.signal.aborted) {
|
|
2010
|
+
reject(new Error(CANCELLED_BY_USER_SENTINEL));
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
const onAbort = () => reject(new Error(CANCELLED_BY_USER_SENTINEL));
|
|
2014
|
+
perCallAbort.signal.addEventListener("abort", onAbort, { once: true });
|
|
2015
|
+
removeAbortListener = () => perCallAbort.signal.removeEventListener("abort", onAbort);
|
|
2016
|
+
});
|
|
2017
|
+
try {
|
|
2018
|
+
output = await Promise.race([bodyPromise, cancellationPromise]);
|
|
2019
|
+
} finally {
|
|
2020
|
+
removeAbortListener?.();
|
|
2021
|
+
}
|
|
1823
2022
|
} catch (err) {
|
|
1824
|
-
const
|
|
1825
|
-
const
|
|
2023
|
+
const isOurSentinel = err instanceof Error && err.message === CANCELLED_BY_USER_SENTINEL;
|
|
2024
|
+
const isAbortError = err instanceof Error && err.name === "AbortError";
|
|
2025
|
+
if (isOurSentinel || isAbortError && perCallAbort.signal.aborted) cancelledByUser = true;
|
|
2026
|
+
else {
|
|
2027
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2028
|
+
const errorCtx = {
|
|
2029
|
+
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
|
|
2030
|
+
error
|
|
2031
|
+
};
|
|
2032
|
+
await ctx.hooks.callHook("tool:error", errorCtx);
|
|
2033
|
+
output = errorCtx.result ?? `Tool error: ${error.message}`;
|
|
2034
|
+
isError = true;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (cancelledByUser) {
|
|
2038
|
+
const reason = typeof perCallAbort.signal.reason === "string" ? perCallAbort.signal.reason : "cancelled-by-user";
|
|
2039
|
+
await ctx.hooks.callHook("tool:cancelled", {
|
|
1826
2040
|
...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2041
|
+
reason,
|
|
2042
|
+
runToolCounts
|
|
2043
|
+
});
|
|
2044
|
+
return { result: {
|
|
2045
|
+
id: callId,
|
|
2046
|
+
content: TOOL_USE_CANCELLED_MESSAGE,
|
|
2047
|
+
isError: true
|
|
2048
|
+
} };
|
|
1832
2049
|
}
|
|
1833
2050
|
const emitted = await emitToolResult(ctx, {
|
|
1834
2051
|
turnId,
|
|
@@ -2032,7 +2249,8 @@ async function executeToolBatch(ctx, toolCalls, turnId) {
|
|
|
2032
2249
|
const call = toolCalls[index];
|
|
2033
2250
|
return executeSingleTool(childCtx, call, turnId).then(({ result }) => {
|
|
2034
2251
|
results[index] = result;
|
|
2035
|
-
|
|
2252
|
+
const isUserCancel = typeof result.content === "string" && result.content === "[Tool call cancelled by user]";
|
|
2253
|
+
if (result.isError && !isUserCancel && call.name === SHELL_TOOL_NAME && !siblingAbort.signal.aborted) siblingAbort.abort(SHELL_CASCADE_REASON);
|
|
2036
2254
|
}, (err) => {
|
|
2037
2255
|
const isAbort = siblingAbort.signal.aborted || ctx.signal.aborted || err instanceof Error && err.name === "AbortError";
|
|
2038
2256
|
results[index] = {
|
|
@@ -2259,6 +2477,376 @@ function defaultBlockMessage(tool, max) {
|
|
|
2259
2477
|
return `Tool '${tool}' has reached its per-run budget of ${max} calls; further invocations are refused.`;
|
|
2260
2478
|
}
|
|
2261
2479
|
//#endregion
|
|
2480
|
+
//#region src/tools/shell-semantics.ts
|
|
2481
|
+
const DEFAULT_SEMANTIC = (exitCode) => ({
|
|
2482
|
+
isError: exitCode !== 0,
|
|
2483
|
+
message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
|
|
2484
|
+
});
|
|
2485
|
+
const COMMAND_SEMANTICS = new Map([
|
|
2486
|
+
["grep", (exit) => ({
|
|
2487
|
+
isError: exit >= 2,
|
|
2488
|
+
message: exit === 1 ? "No matches found" : void 0
|
|
2489
|
+
})],
|
|
2490
|
+
["rg", (exit) => ({
|
|
2491
|
+
isError: exit >= 2,
|
|
2492
|
+
message: exit === 1 ? "No matches found" : void 0
|
|
2493
|
+
})],
|
|
2494
|
+
["diff", (exit) => ({
|
|
2495
|
+
isError: exit >= 2,
|
|
2496
|
+
message: exit === 1 ? "Files differ" : void 0
|
|
2497
|
+
})],
|
|
2498
|
+
["find", (exit) => ({
|
|
2499
|
+
isError: exit >= 2,
|
|
2500
|
+
message: exit === 1 ? "Some directories were inaccessible" : void 0
|
|
2501
|
+
})],
|
|
2502
|
+
["test", (exit) => ({
|
|
2503
|
+
isError: exit >= 2,
|
|
2504
|
+
message: exit === 1 ? "Condition is false" : void 0
|
|
2505
|
+
})],
|
|
2506
|
+
["[", (exit) => ({
|
|
2507
|
+
isError: exit >= 2,
|
|
2508
|
+
message: exit === 1 ? "Condition is false" : void 0
|
|
2509
|
+
})]
|
|
2510
|
+
]);
|
|
2511
|
+
/**
|
|
2512
|
+
* Pick the semantic for a command line. Best-effort: walks the command from
|
|
2513
|
+
* right to left, taking the last segment after `|` / `&&` / `||` / `;` —
|
|
2514
|
+
* that's the segment whose exit code propagates. Don't depend on this for
|
|
2515
|
+
* security; it's a heuristic, not a parser.
|
|
2516
|
+
*/
|
|
2517
|
+
function interpretShellResult(command, exitCode) {
|
|
2518
|
+
const base = extractTrailingCommand(command);
|
|
2519
|
+
return (COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC)(exitCode);
|
|
2520
|
+
}
|
|
2521
|
+
function extractTrailingCommand(command) {
|
|
2522
|
+
const segments = command.split(/\|\||&&|[;|\n]/);
|
|
2523
|
+
return (segments[segments.length - 1]?.trim() ?? command).split(/\s+/).filter((t) => !/^[A-Z_]\w*=/i.test(t))[0] ?? "";
|
|
2524
|
+
}
|
|
2525
|
+
//#endregion
|
|
2526
|
+
//#region src/tools/shell.ts
|
|
2527
|
+
/**
|
|
2528
|
+
* Execute a shell command in the agent's execution context.
|
|
2529
|
+
*
|
|
2530
|
+
* Truncation is **tail-priority**: when stdout+stderr combined exceeds
|
|
2531
|
+
* `maxOutputBytes`, the head is dropped and a marker `…(N bytes truncated
|
|
2532
|
+
* from head)…` is inserted before the tail. Errors and exit summaries
|
|
2533
|
+
* usually live at the end of output, so keeping the tail preserves the
|
|
2534
|
+
* model's most useful signal.
|
|
2535
|
+
*
|
|
2536
|
+
* Defaults are tuned for typical commands (build output, test runs): the
|
|
2537
|
+
* combined cap is 32 KiB and the per-call timeout follows the execution
|
|
2538
|
+
* context's own default (30 s for in-process).
|
|
2539
|
+
*/
|
|
2540
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 32768;
|
|
2541
|
+
/**
|
|
2542
|
+
* Best-effort read-only allow-list for the leading command token. Members
|
|
2543
|
+
* are commands whose stock behavior cannot mutate the workspace under any
|
|
2544
|
+
* argument combination — `ls`, `cat`, `pwd`, etc. Commands that *can*
|
|
2545
|
+
* mutate depending on flags (`find -delete`, `git tag <name>`, `tar -x`)
|
|
2546
|
+
* are intentionally excluded; the input-aware {@link isReadOnlyShellCommand}
|
|
2547
|
+
* predicate falls back to the conservative "not safe" answer for them, so
|
|
2548
|
+
* the scheduler barriers them.
|
|
2549
|
+
*/
|
|
2550
|
+
const SHELL_READ_ONLY_COMMANDS = new Set([
|
|
2551
|
+
"ls",
|
|
2552
|
+
"cat",
|
|
2553
|
+
"head",
|
|
2554
|
+
"tail",
|
|
2555
|
+
"wc",
|
|
2556
|
+
"pwd",
|
|
2557
|
+
"whoami",
|
|
2558
|
+
"id",
|
|
2559
|
+
"date",
|
|
2560
|
+
"uname",
|
|
2561
|
+
"hostname",
|
|
2562
|
+
"tty",
|
|
2563
|
+
"echo",
|
|
2564
|
+
"printf",
|
|
2565
|
+
"env",
|
|
2566
|
+
"printenv",
|
|
2567
|
+
"which",
|
|
2568
|
+
"type",
|
|
2569
|
+
"command",
|
|
2570
|
+
"file",
|
|
2571
|
+
"stat",
|
|
2572
|
+
"grep",
|
|
2573
|
+
"rg",
|
|
2574
|
+
"ag",
|
|
2575
|
+
"true",
|
|
2576
|
+
"false",
|
|
2577
|
+
"test"
|
|
2578
|
+
]);
|
|
2579
|
+
/**
|
|
2580
|
+
* `git` subcommands that are pure reads regardless of arguments. Excludes
|
|
2581
|
+
* `branch`/`tag`/`remote` (which can mutate when given a name) and
|
|
2582
|
+
* `config` (which writes when given a value).
|
|
2583
|
+
*/
|
|
2584
|
+
const GIT_READ_ONLY_SUBCOMMANDS = new Set([
|
|
2585
|
+
"status",
|
|
2586
|
+
"log",
|
|
2587
|
+
"diff",
|
|
2588
|
+
"show",
|
|
2589
|
+
"blame",
|
|
2590
|
+
"rev-parse",
|
|
2591
|
+
"ls-files",
|
|
2592
|
+
"ls-tree",
|
|
2593
|
+
"cat-file",
|
|
2594
|
+
"reflog",
|
|
2595
|
+
"shortlog",
|
|
2596
|
+
"describe",
|
|
2597
|
+
"rev-list",
|
|
2598
|
+
"name-rev",
|
|
2599
|
+
"whatchanged",
|
|
2600
|
+
"merge-base",
|
|
2601
|
+
"symbolic-ref"
|
|
2602
|
+
]);
|
|
2603
|
+
/**
|
|
2604
|
+
* Conservative read-only verdict for a shell command — used to opt a
|
|
2605
|
+
* `shell` invocation into the scheduler's concurrent fleet. Returns
|
|
2606
|
+
* `false` (fail-closed) on anything ambiguous so the scheduler barriers
|
|
2607
|
+
* it. Specifically:
|
|
2608
|
+
*
|
|
2609
|
+
* - Rejects compound commands (`;`, `&&`, `||`, `|`) and redirects (`>`,
|
|
2610
|
+
* `>>`, `<`) — even a pipe to a read-only sink is treated as too
|
|
2611
|
+
* complex to analyze.
|
|
2612
|
+
* - Rejects subshell / process substitution (`$(...)`, `` `...` ``,
|
|
2613
|
+
* `<(...)`, `>(...)`).
|
|
2614
|
+
* - Skips leading `VAR=value` env assignments to find the real
|
|
2615
|
+
* command token.
|
|
2616
|
+
* - Strips a possible absolute path on the command (`/usr/bin/ls` → `ls`).
|
|
2617
|
+
* - Allows the command iff its base name is in
|
|
2618
|
+
* {@link SHELL_READ_ONLY_COMMANDS} OR it's `git <subcmd>` where
|
|
2619
|
+
* `<subcmd>` is in {@link GIT_READ_ONLY_SUBCOMMANDS}.
|
|
2620
|
+
*
|
|
2621
|
+
* Cheap (no spawned process; regex + token scan). Safe to call from the
|
|
2622
|
+
* hot scheduler path.
|
|
2623
|
+
*/
|
|
2624
|
+
function isReadOnlyShellCommand(command) {
|
|
2625
|
+
if (typeof command !== "string") return false;
|
|
2626
|
+
const trimmed = command.trim();
|
|
2627
|
+
if (trimmed === "") return false;
|
|
2628
|
+
if (/[<>;&|`\n]/.test(trimmed)) return false;
|
|
2629
|
+
if (trimmed.includes("$(") || trimmed.includes("<(") || trimmed.includes(">(")) return false;
|
|
2630
|
+
const tokens = trimmed.split(/\s+/);
|
|
2631
|
+
let i = 0;
|
|
2632
|
+
while (i < tokens.length && /^[A-Z_]\w*=/i.test(tokens[i])) i++;
|
|
2633
|
+
const head = tokens[i];
|
|
2634
|
+
if (!head) return false;
|
|
2635
|
+
const base = head.split("/").pop() ?? head;
|
|
2636
|
+
if (SHELL_READ_ONLY_COMMANDS.has(base)) return true;
|
|
2637
|
+
if (base === "git") {
|
|
2638
|
+
const sub = tokens[i + 1];
|
|
2639
|
+
return typeof sub === "string" && GIT_READ_ONLY_SUBCOMMANDS.has(sub);
|
|
2640
|
+
}
|
|
2641
|
+
return false;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Build the `shell` tool's description text. The background-mode
|
|
2645
|
+
* paragraphs are appended only when `allowBackground` is true so the
|
|
2646
|
+
* model isn't pointed at a feature the agent has disabled.
|
|
2647
|
+
*/
|
|
2648
|
+
function buildShellDescription({ allowBackground }) {
|
|
2649
|
+
const lines = [
|
|
2650
|
+
"Execute a shell command in the project root and return its combined stdout/stderr.",
|
|
2651
|
+
"Output is tail-priority truncated at 32 KiB by default; errors and exit-code summaries live in the tail.",
|
|
2652
|
+
"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."
|
|
2653
|
+
];
|
|
2654
|
+
if (allowBackground) lines.push("", "Long-running commands (`npm run dev`, `python train.py`, anything that would otherwise block your turn for minutes) → `run_in_background: true`. The call returns immediately with `{ task_id, output_path, pid }`; stdout + stderr stream to the log file at `output_path`.", "", "After spawning a background task: end your current turn (do NOT keep iterating). A `<task-notification>` arrives on the agent's NEXT user-turn with the final status. Polling the log file in a loop wastes tokens and blocks your turn — the notification IS the wake-up. If you NEED to check progress immediately (rare), call `read_file({ path: output_path, ... })` exactly once and decide. To terminate, use `shell_kill({ task_id })`.", "", "When called from inside a `spawn`'d subagent: you have NO next user-turn — your `agent.run` ends as soon as you finish responding. Start the background task, return a brief summary including the `task_id`, and end your turn. Ownership of the task is transferred to the parent agent when your run finishes; the parent will see the notification on ITS next user-turn.");
|
|
2655
|
+
return lines.join("\n");
|
|
2656
|
+
}
|
|
2657
|
+
/**
|
|
2658
|
+
* Build the `shell` tool's JSON-schema. The `run_in_background` field
|
|
2659
|
+
* is included only when `allowBackground` is true; the `timeout` /
|
|
2660
|
+
* `maxOutputBytes` / `metadata` field descriptions also drop their
|
|
2661
|
+
* "Ignored in background mode" qualifier when there's no background
|
|
2662
|
+
* mode to ignore.
|
|
2663
|
+
*/
|
|
2664
|
+
function buildShellInputSchema({ allowBackground }) {
|
|
2665
|
+
const bgQualifier = allowBackground ? " Ignored in background mode." : "";
|
|
2666
|
+
const bgQualifierOutput = allowBackground ? " Ignored in background mode (output streams to disk)." : "";
|
|
2667
|
+
const properties = {
|
|
2668
|
+
command: {
|
|
2669
|
+
type: "string",
|
|
2670
|
+
description: "Shell command to run."
|
|
2671
|
+
},
|
|
2672
|
+
timeout: {
|
|
2673
|
+
type: "integer",
|
|
2674
|
+
description: `Per-call timeout in milliseconds.${bgQualifier}`
|
|
2675
|
+
},
|
|
2676
|
+
maxOutputBytes: {
|
|
2677
|
+
type: "integer",
|
|
2678
|
+
description: `Truncate combined stdout+stderr beyond this many bytes. Default: 32768. Set 0 for unlimited.${bgQualifierOutput}`
|
|
2679
|
+
},
|
|
2680
|
+
metadata: {
|
|
2681
|
+
type: "boolean",
|
|
2682
|
+
description: `Append \`(exit N, Nms)\` footer and surface non-empty stderr on success. Default: true.${bgQualifier}`
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
if (allowBackground) properties.run_in_background = {
|
|
2686
|
+
type: "boolean",
|
|
2687
|
+
description: "Start the command in the background, returning a task handle. See the tool description for the full flow."
|
|
2688
|
+
};
|
|
2689
|
+
return {
|
|
2690
|
+
type: "object",
|
|
2691
|
+
properties,
|
|
2692
|
+
required: ["command"]
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
/**
|
|
2696
|
+
* Factory for the `shell` tool. The default exported `shell` is
|
|
2697
|
+
* equivalent to `createShellTool({ allowBackground: true })`. The
|
|
2698
|
+
* factory is the entry point hosts use when they want to override the
|
|
2699
|
+
* default — e.g. to ship a preset that always disables background mode
|
|
2700
|
+
* regardless of `behavior.tasksDir`.
|
|
2701
|
+
*
|
|
2702
|
+
* Hosts that use the framework's `createAgent` typically don't need to
|
|
2703
|
+
* call this directly: when `behavior.tasksDir` is unset or
|
|
2704
|
+
* `behavior.disableBackgroundTasks: true` is set, the agent
|
|
2705
|
+
* automatically rewrites the registered `shell` (if it's the
|
|
2706
|
+
* framework's built-in) using this factory.
|
|
2707
|
+
*/
|
|
2708
|
+
function createShellTool(opts = {}) {
|
|
2709
|
+
const allowBackground = opts.allowBackground !== false;
|
|
2710
|
+
return {
|
|
2711
|
+
isConcurrencySafe: (input) => isReadOnlyShellCommand(input.command),
|
|
2712
|
+
spec: {
|
|
2713
|
+
name: "shell",
|
|
2714
|
+
description: buildShellDescription({ allowBackground }),
|
|
2715
|
+
inputSchema: buildShellInputSchema({ allowBackground })
|
|
2716
|
+
},
|
|
2717
|
+
async execute({ command, timeout, maxOutputBytes, metadata, run_in_background }, ctx) {
|
|
2718
|
+
const cmd = command;
|
|
2719
|
+
if (run_in_background === true) {
|
|
2720
|
+
if (!allowBackground) return "shell error: background mode is disabled for this agent (no `behavior.tasksDir` set, or `behavior.disableBackgroundTasks: true`). Fall back to foreground (drop `run_in_background`).";
|
|
2721
|
+
return runBackground(cmd, ctx);
|
|
2722
|
+
}
|
|
2723
|
+
const execOpts = { signal: ctx.signal };
|
|
2724
|
+
if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) execOpts.timeout = Math.max(1, Math.ceil(timeout / 1e3));
|
|
2725
|
+
const wantMetadata = metadata !== false;
|
|
2726
|
+
const startedAt = Date.now();
|
|
2727
|
+
const result = await ctx.execution.exec(ctx.handle, cmd, execOpts);
|
|
2728
|
+
const durationMs = Date.now() - startedAt;
|
|
2729
|
+
const cap = normalizeCap(maxOutputBytes);
|
|
2730
|
+
const semantic = interpretShellResult(cmd, result.exitCode);
|
|
2731
|
+
if (result.exitCode === 0) {
|
|
2732
|
+
const stdoutTail = truncateTail(result.stdout || "(no output)", cap);
|
|
2733
|
+
if (!wantMetadata) return stdoutTail;
|
|
2734
|
+
const stderrTrimmed = result.stderr.trim();
|
|
2735
|
+
return `${stdoutTail}${stderrTrimmed ? `\n[stderr]\n${truncateTail(stderrTrimmed, Math.min(cap, 2048))}` : ""}\n(exit 0, ${durationMs}ms)`;
|
|
2736
|
+
}
|
|
2737
|
+
if (!semantic.isError) {
|
|
2738
|
+
const tail = truncateTail((result.stdout || result.stderr || "").trim(), cap);
|
|
2739
|
+
const semanticFooter = semantic.message ? `\n(${semantic.message})` : "";
|
|
2740
|
+
const timingFooter = wantMetadata ? `\n(exit ${result.exitCode}, ${durationMs}ms)` : "";
|
|
2741
|
+
return `${tail.length > 0 ? tail : semantic.message ?? "(no output)"}${semanticFooter}${timingFooter}`;
|
|
2742
|
+
}
|
|
2743
|
+
const combined = `${result.stdout}\n${result.stderr}`.trim();
|
|
2744
|
+
return `${wantMetadata ? `Exit code ${result.exitCode} (${durationMs}ms)` : `Exit code ${result.exitCode}`}\n${truncateTail(combined, cap)}`;
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Default `shell` tool with background mode enabled.
|
|
2750
|
+
*
|
|
2751
|
+
* Most hosts use this directly via `basicTools`. When the agent's
|
|
2752
|
+
* `behavior.tasksDir` is unset OR `behavior.disableBackgroundTasks:
|
|
2753
|
+
* true` is set, `createAgent` auto-rewrites this identity to a
|
|
2754
|
+
* `createShellTool({ allowBackground: false })` variant so the model
|
|
2755
|
+
* never sees a flag it can't use. Hosts who want to bypass that
|
|
2756
|
+
* auto-rewrite can register a `createShellTool({ allowBackground })`
|
|
2757
|
+
* directly — the rewrite only fires on identity-equal references to
|
|
2758
|
+
* this constant.
|
|
2759
|
+
*/
|
|
2760
|
+
const shell = createShellTool({ allowBackground: true });
|
|
2761
|
+
/**
|
|
2762
|
+
* Background-mode entry point for the `shell` tool. Settles fast,
|
|
2763
|
+
* registers an `onExit` callback that fires `background:exit` on the
|
|
2764
|
+
* agent's hook bus, and returns a structured one-liner to the model.
|
|
2765
|
+
*
|
|
2766
|
+
* Reachable only via the `allowBackground: true` variant; the
|
|
2767
|
+
* `allowBackground: false` variant short-circuits before this is
|
|
2768
|
+
* called and `createAgent` auto-rewrites the built-in to the
|
|
2769
|
+
* `false` variant when `behavior.tasksDir` is unset or
|
|
2770
|
+
* `behavior.disableBackgroundTasks: true` is set. The runtime checks
|
|
2771
|
+
* below are defense-in-depth for hosts who skip the auto-rewrite
|
|
2772
|
+
* (custom shell tool, run-level tools override, etc.):
|
|
2773
|
+
*
|
|
2774
|
+
* - `behavior.tasksDir` unset → host opted into the schema but
|
|
2775
|
+
* forgot to wire the log dir. Clean error, model can fall back to
|
|
2776
|
+
* foreground.
|
|
2777
|
+
* - `ctx.execution.execBackground` undefined → the context doesn't
|
|
2778
|
+
* support it (some remote sandboxes).
|
|
2779
|
+
* - `mkdir` on the output dir fails → the underlying filesystem
|
|
2780
|
+
* can't accommodate. Surface the error verbatim.
|
|
2781
|
+
*/
|
|
2782
|
+
async function runBackground(command, ctx) {
|
|
2783
|
+
const tasksDir = ctx.behavior?.tasksDir;
|
|
2784
|
+
if (typeof tasksDir !== "string" || tasksDir.length === 0) return "shell error: background mode requires `behavior.tasksDir` to be set on the agent. The host has not opted into background tasks — either fall back to foreground (drop `run_in_background`) or ask the user to enable it.";
|
|
2785
|
+
if (!ctx.execution.execBackground) return `shell error: the active execution context (${ctx.execution.type}) does not support background tasks. Fall back to foreground (drop \`run_in_background\`).`;
|
|
2786
|
+
try {
|
|
2787
|
+
const handle = await ctx.execution.execBackground(ctx.handle, command, {
|
|
2788
|
+
outputDir: tasksDir,
|
|
2789
|
+
onExit: (info) => {
|
|
2790
|
+
Promise.resolve(ctx.hooks.callHook("background:exit", info)).catch((err) => {
|
|
2791
|
+
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/shell] background:exit hook rejected: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
});
|
|
2795
|
+
Promise.resolve(ctx.hooks.callHook("background:start", {
|
|
2796
|
+
taskId: handle.taskId,
|
|
2797
|
+
pid: handle.pid,
|
|
2798
|
+
command,
|
|
2799
|
+
cwd: ctx.handle.cwd,
|
|
2800
|
+
outputPath: handle.outputPath,
|
|
2801
|
+
startedAt: Date.now()
|
|
2802
|
+
})).catch((err) => {
|
|
2803
|
+
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/shell] background:start hook rejected: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2804
|
+
});
|
|
2805
|
+
const cmdPreview = previewLine(command, 60);
|
|
2806
|
+
const wakeupHint = (ctx.depth ?? 0) > 0 ? "You are inside a `spawn`'d subagent — you have NO next user-turn to receive the notification on. Return a brief summary INCLUDING this task_id and end your turn now. Ownership of the task transfers to the parent agent when your run completes; the parent will see the `<task-notification>` on its next user-turn." : "The task is running in the background. You'll receive a <task-notification> on your NEXT user-turn with the final status. End your current turn now — do NOT poll the output file in a loop, the notification IS the wake-up. To inspect progress, call `read_file({ path: <output> })` once. To terminate, use `shell_kill({ task_id })`.";
|
|
2807
|
+
return [
|
|
2808
|
+
`Started ${handle.taskId} (pid ${handle.pid}).`,
|
|
2809
|
+
` command: ${cmdPreview}`,
|
|
2810
|
+
` output: ${handle.outputPath}`,
|
|
2811
|
+
"",
|
|
2812
|
+
wakeupHint
|
|
2813
|
+
].join("\n");
|
|
2814
|
+
} catch (err) {
|
|
2815
|
+
return `shell error: failed to start background task: ${err instanceof Error ? err.message : String(err)}`;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
function normalizeCap(value) {
|
|
2819
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_MAX_OUTPUT_BYTES;
|
|
2820
|
+
if (value < 0) return DEFAULT_MAX_OUTPUT_BYTES;
|
|
2821
|
+
return Math.floor(value);
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Tail-priority byte truncation. When `text` exceeds `cap` bytes, the head is
|
|
2825
|
+
* dropped and replaced with a marker. Always cuts on character boundaries (no
|
|
2826
|
+
* mid-codepoint splits) by walking from the end with `Buffer.byteLength`.
|
|
2827
|
+
*
|
|
2828
|
+
* `cap === 0` disables truncation. `cap` is interpreted as a UTF-8 byte budget
|
|
2829
|
+
* for the tail itself — the marker is added on top and may push the visible
|
|
2830
|
+
* length slightly past `cap`. That tradeoff is intentional: a marker that
|
|
2831
|
+
* always fits inside the budget would shrink the actual content displayed.
|
|
2832
|
+
*/
|
|
2833
|
+
function truncateTail(text, cap) {
|
|
2834
|
+
if (cap === 0) return text;
|
|
2835
|
+
const totalBytes = Buffer.byteLength(text);
|
|
2836
|
+
if (totalBytes <= cap) return text;
|
|
2837
|
+
let bytes = 0;
|
|
2838
|
+
let charIdx = text.length;
|
|
2839
|
+
while (charIdx > 0) {
|
|
2840
|
+
const ch = text[charIdx - 1];
|
|
2841
|
+
const chBytes = Buffer.byteLength(ch);
|
|
2842
|
+
if (bytes + chBytes > cap) break;
|
|
2843
|
+
bytes += chBytes;
|
|
2844
|
+
charIdx--;
|
|
2845
|
+
}
|
|
2846
|
+
const tail = text.slice(charIdx);
|
|
2847
|
+
return `…(${totalBytes - Buffer.byteLength(tail)} bytes truncated from head)…\n${tail}`;
|
|
2848
|
+
}
|
|
2849
|
+
//#endregion
|
|
2262
2850
|
//#region src/tools/binary-detect.ts
|
|
2263
2851
|
/**
|
|
2264
2852
|
* Heuristics for detecting binary content in UTF-8-decoded strings.
|
|
@@ -2426,7 +3014,10 @@ function createSkillsRunScriptTool(options) {
|
|
|
2426
3014
|
if (!validated.valid) return `Error: ${validated.error}`;
|
|
2427
3015
|
const cmd = [validated.absolutePath, ...args].map(alwaysQuote).join(" ");
|
|
2428
3016
|
try {
|
|
2429
|
-
const result = await ctx.execution.exec(ctx.handle, cmd, {
|
|
3017
|
+
const result = await ctx.execution.exec(ctx.handle, cmd, {
|
|
3018
|
+
timeout: Math.max(1, Math.round(timeoutMs / 1e3)),
|
|
3019
|
+
signal: ctx.signal
|
|
3020
|
+
});
|
|
2430
3021
|
return JSON.stringify({
|
|
2431
3022
|
exitCode: result.exitCode,
|
|
2432
3023
|
stdout: result.stdout,
|
|
@@ -2480,14 +3071,21 @@ function createSkillsUseTool(options) {
|
|
|
2480
3071
|
return {
|
|
2481
3072
|
spec: {
|
|
2482
3073
|
name: "skills_use",
|
|
2483
|
-
description: "Activate a specialized skill
|
|
3074
|
+
description: "Activate or deactivate a specialized skill. Call with `mode: \"activate\"` (default) to load a skill's full instructions when a task matches its catalog description. Call with `mode: \"deactivate\"` to release a skill whose allowed-tools restrictions are now in the way — e.g. when the skill's work is done or the active skill is blocking unrelated tool calls. After activating, follow the returned instructions; use skills_read to load referenced files and skills_run_script to execute bundled scripts.",
|
|
2484
3075
|
inputSchema: {
|
|
2485
3076
|
type: "object",
|
|
2486
|
-
properties: {
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
3077
|
+
properties: {
|
|
3078
|
+
name: {
|
|
3079
|
+
type: "string",
|
|
3080
|
+
enum: options.catalog.map((s) => s.name),
|
|
3081
|
+
description: "The name of the skill to activate or deactivate (must be in the available skills catalog)."
|
|
3082
|
+
},
|
|
3083
|
+
mode: {
|
|
3084
|
+
type: "string",
|
|
3085
|
+
enum: ["activate", "deactivate"],
|
|
3086
|
+
description: "Whether to activate (load + apply the skill) or deactivate (release an active skill). Default: \"activate\"."
|
|
3087
|
+
}
|
|
3088
|
+
},
|
|
2491
3089
|
required: ["name"],
|
|
2492
3090
|
additionalProperties: false
|
|
2493
3091
|
}
|
|
@@ -2496,8 +3094,18 @@ function createSkillsUseTool(options) {
|
|
|
2496
3094
|
const skillName = input.name;
|
|
2497
3095
|
const skill = byName.get(skillName);
|
|
2498
3096
|
if (!skill) return `Error: unknown skill "${skillName}". Available skills: ${[...byName.keys()].join(", ") || "<none>"}.`;
|
|
3097
|
+
if ((input.mode ?? "activate") === "deactivate") {
|
|
3098
|
+
const removed = options.state.deactivate(skillName);
|
|
3099
|
+
if (!removed) return `Skill "${skillName}" was not active — nothing to deactivate.`;
|
|
3100
|
+
await options.hooks.callHook("skills:deactivate", {
|
|
3101
|
+
skill: removed.skill,
|
|
3102
|
+
reason: "model"
|
|
3103
|
+
});
|
|
3104
|
+
const remaining = options.state.active().map((a) => a.skill.name);
|
|
3105
|
+
return `Skill "${skillName}" deactivated — its allowed-tools restrictions no longer apply.${remaining.length > 0 ? ` Remaining active skills: ${remaining.join(", ")}.` : " No skills are currently active."}`;
|
|
3106
|
+
}
|
|
2499
3107
|
if (!options.state.isActive(skillName)) {
|
|
2500
|
-
if (options.state.activate(skill, "model") === "cap-reached") return `Error: cannot activate "${skillName}" — the maxActive skill cap has been reached. Currently active: ${options.state.active().map((a) => a.skill.name).join(", ")}.
|
|
3108
|
+
if (options.state.activate(skill, "model") === "cap-reached") return `Error: cannot activate "${skillName}" — the maxActive skill cap has been reached. Currently active: ${options.state.active().map((a) => a.skill.name).join(", ")}. Call \`skills_use\` with \`mode: "deactivate"\` and one of those names first.`;
|
|
2501
3109
|
await options.hooks.callHook("skills:activate", {
|
|
2502
3110
|
skill,
|
|
2503
3111
|
via: "model"
|
|
@@ -2656,6 +3264,39 @@ function createToolSearchTool(options) {
|
|
|
2656
3264
|
}
|
|
2657
3265
|
//#endregion
|
|
2658
3266
|
//#region src/agent.ts
|
|
3267
|
+
/**
|
|
3268
|
+
* Authoritative list of hook event names. Kept in sync with `AgentHooks` at
|
|
3269
|
+
* compile time: the `satisfies` assertion below rejects any drift.
|
|
3270
|
+
*/
|
|
3271
|
+
/**
|
|
3272
|
+
* Canonical XML wire format for a background-task completion notification.
|
|
3273
|
+
*
|
|
3274
|
+
* Rendered as a leading `text` content block in the next user-turn so
|
|
3275
|
+
* the model can pattern-match on the outer tag. The shape uses kebab-case
|
|
3276
|
+
* tags (matching zidane's other XML conventions). Every field is
|
|
3277
|
+
* structured — `<summary>` is a derived display string, NOT the source
|
|
3278
|
+
* of truth for the underlying data (replay reads `<command>` /
|
|
3279
|
+
* `<duration-ms>` / etc. directly, never the summary, so changing the
|
|
3280
|
+
* summary format can't break replay).
|
|
3281
|
+
*
|
|
3282
|
+
* Field-level escaping uses {@link escapeXml} so commands containing
|
|
3283
|
+
* `<` / `>` / `&` round-trip cleanly through persistence + replay.
|
|
3284
|
+
*/
|
|
3285
|
+
function renderTaskNotificationXml(info) {
|
|
3286
|
+
const summary = formatTaskSummary(info);
|
|
3287
|
+
return [
|
|
3288
|
+
"<task-notification>",
|
|
3289
|
+
` <task-id>${escapeXml(info.taskId)}</task-id>`,
|
|
3290
|
+
` <status>${info.status}</status>`,
|
|
3291
|
+
` <exit-code>${info.exitCode}</exit-code>`,
|
|
3292
|
+
...info.signal ? [` <signal>${escapeXml(info.signal)}</signal>`] : [],
|
|
3293
|
+
` <command>${escapeXml(info.command)}</command>`,
|
|
3294
|
+
` <output-file>${escapeXml(info.outputPath)}</output-file>`,
|
|
3295
|
+
` <duration-ms>${info.durationMs}</duration-ms>`,
|
|
3296
|
+
` <summary>${escapeXml(summary)}</summary>`,
|
|
3297
|
+
"</task-notification>"
|
|
3298
|
+
].join("\n");
|
|
3299
|
+
}
|
|
2659
3300
|
const HOOK_EVENT_SET = new Set([
|
|
2660
3301
|
"system:before",
|
|
2661
3302
|
"agent:start",
|
|
@@ -2673,6 +3314,7 @@ const HOOK_EVENT_SET = new Set([
|
|
|
2673
3314
|
"tool:before",
|
|
2674
3315
|
"tool:after",
|
|
2675
3316
|
"tool:error",
|
|
3317
|
+
"tool:cancelled",
|
|
2676
3318
|
"tool:transform",
|
|
2677
3319
|
"tool:unknown",
|
|
2678
3320
|
"validation:reject",
|
|
@@ -2694,6 +3336,10 @@ const HOOK_EVENT_SET = new Set([
|
|
|
2694
3336
|
"child:tool:after",
|
|
2695
3337
|
"child:tool:transform",
|
|
2696
3338
|
"child:tool:error",
|
|
3339
|
+
"child:tool:cancelled",
|
|
3340
|
+
"child:background:start",
|
|
3341
|
+
"child:background:exit",
|
|
3342
|
+
"child:background:reassign",
|
|
2697
3343
|
"child:turn:after",
|
|
2698
3344
|
"mcp:connect",
|
|
2699
3345
|
"mcp:error",
|
|
@@ -2710,6 +3356,9 @@ const HOOK_EVENT_SET = new Set([
|
|
|
2710
3356
|
"mcp:tool:after",
|
|
2711
3357
|
"mcp:tool:transform",
|
|
2712
3358
|
"mcp:tool:error",
|
|
3359
|
+
"background:start",
|
|
3360
|
+
"background:exit",
|
|
3361
|
+
"background:reassign",
|
|
2713
3362
|
"skills:resolve",
|
|
2714
3363
|
"skills:catalog",
|
|
2715
3364
|
"skills:activate",
|
|
@@ -2810,6 +3459,8 @@ function resolveBehavior(agentBehavior, runBehavior) {
|
|
|
2810
3459
|
persistThreshold: runBehavior?.persistThreshold ?? agentBehavior?.persistThreshold,
|
|
2811
3460
|
persistExcludeTools: runBehavior?.persistExcludeTools ?? agentBehavior?.persistExcludeTools,
|
|
2812
3461
|
persistDir: runBehavior?.persistDir ?? agentBehavior?.persistDir,
|
|
3462
|
+
tasksDir: runBehavior?.tasksDir ?? agentBehavior?.tasksDir,
|
|
3463
|
+
disableBackgroundTasks: runBehavior?.disableBackgroundTasks ?? agentBehavior?.disableBackgroundTasks,
|
|
2813
3464
|
strictToolPairing: runBehavior?.strictToolPairing ?? agentBehavior?.strictToolPairing ?? false
|
|
2814
3465
|
};
|
|
2815
3466
|
}
|
|
@@ -2973,6 +3624,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
2973
3624
|
let running = false;
|
|
2974
3625
|
let idleResolve;
|
|
2975
3626
|
let idlePromise;
|
|
3627
|
+
const pendingTaskNotifications = /* @__PURE__ */ new Map();
|
|
3628
|
+
const pendingToolCancels = /* @__PURE__ */ new Map();
|
|
2976
3629
|
let executionHandle = null;
|
|
2977
3630
|
let mcpConnection = null;
|
|
2978
3631
|
let mcpWarmupPromise = null;
|
|
@@ -3040,6 +3693,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3040
3693
|
running = true;
|
|
3041
3694
|
try {
|
|
3042
3695
|
abortController = new AbortController();
|
|
3696
|
+
runCounter = Math.max(runCounter, initialRunCounter(session));
|
|
3043
3697
|
const runId = `run_${++runCounter}`;
|
|
3044
3698
|
const promptLabel = typeof options.prompt === "string" ? options.prompt : Array.isArray(options.prompt) ? options.prompt.filter((p) => p.type === "text").map((p) => p.text).join("\n") : "";
|
|
3045
3699
|
session?.startRun(runId, promptLabel, {
|
|
@@ -3090,20 +3744,27 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3090
3744
|
await ensureSkillsResolved();
|
|
3091
3745
|
if (resolvedSkills && session && session.turns.length > 0 && skillActivationState.active().length === 0) {
|
|
3092
3746
|
const skillsByName = new Map(resolvedSkills.map((s) => [s.name, s]));
|
|
3747
|
+
const lastModeBySkill = /* @__PURE__ */ new Map();
|
|
3093
3748
|
for (const turn of session.turns) {
|
|
3094
3749
|
if (turn.role !== "assistant") continue;
|
|
3095
3750
|
for (const block of turn.content) {
|
|
3096
3751
|
if (block.type !== "tool_call" || block.name !== "skills_use") continue;
|
|
3097
|
-
const
|
|
3752
|
+
const input = block.input;
|
|
3753
|
+
const skillName = input?.name;
|
|
3098
3754
|
if (!skillName) continue;
|
|
3099
|
-
const
|
|
3100
|
-
|
|
3101
|
-
if (skillActivationState.activate(skill, "resume") === "ok") await hooks.callHook("skills:activate", {
|
|
3102
|
-
skill,
|
|
3103
|
-
via: "resume"
|
|
3104
|
-
});
|
|
3755
|
+
const mode = input?.mode === "deactivate" ? "deactivate" : "activate";
|
|
3756
|
+
lastModeBySkill.set(skillName, mode);
|
|
3105
3757
|
}
|
|
3106
3758
|
}
|
|
3759
|
+
for (const [skillName, mode] of lastModeBySkill) {
|
|
3760
|
+
if (mode !== "activate") continue;
|
|
3761
|
+
const skill = skillsByName.get(skillName);
|
|
3762
|
+
if (!skill) continue;
|
|
3763
|
+
if (skillActivationState.activate(skill, "resume") === "ok") await hooks.callHook("skills:activate", {
|
|
3764
|
+
skill,
|
|
3765
|
+
via: "resume"
|
|
3766
|
+
});
|
|
3767
|
+
}
|
|
3107
3768
|
}
|
|
3108
3769
|
const thinking = options.thinking ?? "off";
|
|
3109
3770
|
const model = options.model ?? provider.meta.defaultModel;
|
|
@@ -3135,6 +3796,9 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3135
3796
|
} : runBaseTools;
|
|
3136
3797
|
const toolsPreSearch = {};
|
|
3137
3798
|
for (const tool of Object.values(mergedWithSkills)) toolsPreSearch[tool.spec.name] = tool;
|
|
3799
|
+
if (toolsPreSearch.shell === shell) {
|
|
3800
|
+
if (!(typeof resolvedBehavior?.tasksDir === "string" && resolvedBehavior.tasksDir.length > 0 && resolvedBehavior.disableBackgroundTasks !== true)) toolsPreSearch.shell = createShellTool({ allowBackground: false });
|
|
3801
|
+
}
|
|
3138
3802
|
const disclosure = partitionToolDisclosure(toolsPreSearch, mcpToolNames, mcpServers, toolDisclosure, toolAliases);
|
|
3139
3803
|
const unlocked = new Set(disclosure.eagerCanonicalNames);
|
|
3140
3804
|
const hostDefinedToolSearch = !!toolsPreSearch.tool_search;
|
|
@@ -3169,8 +3833,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3169
3833
|
}
|
|
3170
3834
|
const formattedTools = buildFormattedTools();
|
|
3171
3835
|
const turns = [];
|
|
3172
|
-
|
|
3173
|
-
if (isResume) {
|
|
3836
|
+
if (session && session.turns.length > 0 && (session.runs.length > 0 || !options.prompt) && !options.parentRunId) {
|
|
3174
3837
|
const childRunIds = new Set(session.runs.filter((r) => (r.depth ?? 0) > 0).map((r) => r.id));
|
|
3175
3838
|
const resumed = childRunIds.size === 0 ? session.turns : session.turns.filter((t) => !t.runId || !childRunIds.has(t.runId));
|
|
3176
3839
|
const filteredForRuntime = resumeFilteredTurns && resumed === session.turns ? resumeFilteredTurns : filterUnresolvedToolUses(resumed);
|
|
@@ -3178,19 +3841,28 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3178
3841
|
}
|
|
3179
3842
|
const runTurnStart = turns.length;
|
|
3180
3843
|
if (options.system) await hooks.callHook("system:before", { system: options.system });
|
|
3844
|
+
const drainedNotifications = [];
|
|
3845
|
+
if (pendingTaskNotifications.size > 0) {
|
|
3846
|
+
for (const notif of pendingTaskNotifications.values()) drainedNotifications.push({
|
|
3847
|
+
type: "text",
|
|
3848
|
+
text: renderTaskNotificationXml(notif)
|
|
3849
|
+
});
|
|
3850
|
+
pendingTaskNotifications.clear();
|
|
3851
|
+
}
|
|
3852
|
+
let lastPersistedTurnCount = turns.length;
|
|
3181
3853
|
const promptParts = canonicalizePrompt(options.prompt);
|
|
3182
|
-
if (promptParts) {
|
|
3183
|
-
const promptMsg = buildPromptMessage(provider, promptParts);
|
|
3854
|
+
if (promptParts || drainedNotifications.length > 0) {
|
|
3855
|
+
const promptMsg = promptParts ? buildPromptMessage(provider, promptParts) : null;
|
|
3856
|
+
const content = [...drainedNotifications, ...promptMsg ? promptMsg.content : []];
|
|
3184
3857
|
turns.push({
|
|
3185
3858
|
id: crypto.randomUUID(),
|
|
3186
3859
|
runId,
|
|
3187
|
-
role: promptMsg.role,
|
|
3188
|
-
content
|
|
3860
|
+
role: promptMsg ? promptMsg.role : "user",
|
|
3861
|
+
content,
|
|
3189
3862
|
createdAt: Date.now()
|
|
3190
3863
|
});
|
|
3191
3864
|
}
|
|
3192
3865
|
conversationTurns = turns;
|
|
3193
|
-
let lastPersistedTurnCount = isResume ? session.turns.length : 0;
|
|
3194
3866
|
if (session && turns.length > lastPersistedTurnCount) {
|
|
3195
3867
|
const seededTurns = turns.slice(lastPersistedTurnCount);
|
|
3196
3868
|
await session.appendTurns(seededTurns);
|
|
@@ -3301,7 +3973,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3301
3973
|
...strictToolPairing ? { strictToolPairing: true } : {},
|
|
3302
3974
|
providerName: provider.name,
|
|
3303
3975
|
runStartMs,
|
|
3304
|
-
runToolCounts: {}
|
|
3976
|
+
runToolCounts: {},
|
|
3977
|
+
pendingToolCancels
|
|
3305
3978
|
});
|
|
3306
3979
|
const parentTurnCost = stats.turnUsage?.reduce((sum, t) => sum + (t.cost ?? 0), 0) ?? 0;
|
|
3307
3980
|
let childrenIn = 0;
|
|
@@ -3398,6 +4071,17 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3398
4071
|
function abort() {
|
|
3399
4072
|
abortController?.abort();
|
|
3400
4073
|
}
|
|
4074
|
+
function cancelTool(callId, reason) {
|
|
4075
|
+
const controller = pendingToolCancels.get(callId);
|
|
4076
|
+
if (!controller || controller.signal.aborted) return false;
|
|
4077
|
+
controller.abort(reason ?? "user-cancelled-tool");
|
|
4078
|
+
return true;
|
|
4079
|
+
}
|
|
4080
|
+
async function killBackgroundTask(taskId) {
|
|
4081
|
+
if (!executionHandle) return false;
|
|
4082
|
+
if (!executionContext.killBackground) return false;
|
|
4083
|
+
return await executionContext.killBackground(executionHandle, taskId) !== null;
|
|
4084
|
+
}
|
|
3401
4085
|
function steer(message) {
|
|
3402
4086
|
steeringQueue.push(message);
|
|
3403
4087
|
}
|
|
@@ -3412,6 +4096,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3412
4096
|
conversationTurns = [];
|
|
3413
4097
|
steeringQueue.length = 0;
|
|
3414
4098
|
followUpQueue.length = 0;
|
|
4099
|
+
pendingTaskNotifications.clear();
|
|
3415
4100
|
const cleared = skillActivationState.clear();
|
|
3416
4101
|
for (const record of cleared) await hooks.callHook("skills:deactivate", {
|
|
3417
4102
|
skill: record.skill,
|
|
@@ -3440,6 +4125,33 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3440
4125
|
reason: "explicit"
|
|
3441
4126
|
});
|
|
3442
4127
|
}
|
|
4128
|
+
hooks.hook("background:exit", (ctx) => {
|
|
4129
|
+
pendingTaskNotifications.set(ctx.taskId, {
|
|
4130
|
+
taskId: ctx.taskId,
|
|
4131
|
+
status: ctx.status,
|
|
4132
|
+
exitCode: ctx.exitCode,
|
|
4133
|
+
...ctx.signal ? { signal: ctx.signal } : {},
|
|
4134
|
+
outputPath: ctx.outputPath,
|
|
4135
|
+
durationMs: ctx.durationMs,
|
|
4136
|
+
command: ctx.command
|
|
4137
|
+
});
|
|
4138
|
+
});
|
|
4139
|
+
hooks.hook("tool:after", (ctx) => {
|
|
4140
|
+
if (ctx.name === "shell_kill") {
|
|
4141
|
+
const taskId = ctx.input?.task_id;
|
|
4142
|
+
if (typeof taskId === "string") pendingTaskNotifications.delete(taskId);
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
if (ctx.name === "read_file" || ctx.name === "read") {
|
|
4146
|
+
const rawPath = ctx.input?.path;
|
|
4147
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) return;
|
|
4148
|
+
const requested = resolve(rawPath);
|
|
4149
|
+
for (const notif of pendingTaskNotifications.values()) if (resolve(notif.outputPath) === requested) {
|
|
4150
|
+
pendingTaskNotifications.delete(notif.taskId);
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
});
|
|
3443
4155
|
if (session) {
|
|
3444
4156
|
const originalSave = session.save.bind(session);
|
|
3445
4157
|
const originalSetMeta = session.setMeta.bind(session);
|
|
@@ -3491,6 +4203,9 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3491
4203
|
async function destroy() {
|
|
3492
4204
|
if (destroyed) return;
|
|
3493
4205
|
destroyed = true;
|
|
4206
|
+
pendingTaskNotifications.clear();
|
|
4207
|
+
for (const controller of pendingToolCancels.values()) if (!controller.signal.aborted) controller.abort("agent-destroyed");
|
|
4208
|
+
pendingToolCancels.clear();
|
|
3494
4209
|
if (mcpWarmupPromise) try {
|
|
3495
4210
|
await mcpWarmupPromise;
|
|
3496
4211
|
} catch {}
|
|
@@ -3502,6 +4217,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3502
4217
|
await executionContext.destroy(executionHandle);
|
|
3503
4218
|
executionHandle = null;
|
|
3504
4219
|
}
|
|
4220
|
+
pendingTaskNotifications.clear();
|
|
3505
4221
|
skillsCleanup();
|
|
3506
4222
|
skillsCleanup = () => {};
|
|
3507
4223
|
}
|
|
@@ -3511,6 +4227,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3511
4227
|
hooks,
|
|
3512
4228
|
run,
|
|
3513
4229
|
abort,
|
|
4230
|
+
cancelTool,
|
|
4231
|
+
killBackgroundTask,
|
|
3514
4232
|
steer,
|
|
3515
4233
|
followUp: followUpFn,
|
|
3516
4234
|
waitForIdle,
|
|
@@ -4643,254 +5361,39 @@ function normalizeInteger(value, fallback) {
|
|
|
4643
5361
|
return Math.floor(value);
|
|
4644
5362
|
}
|
|
4645
5363
|
//#endregion
|
|
4646
|
-
//#region src/tools/shell-
|
|
4647
|
-
const
|
|
4648
|
-
|
|
4649
|
-
message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
|
|
4650
|
-
});
|
|
4651
|
-
const COMMAND_SEMANTICS = new Map([
|
|
4652
|
-
["grep", (exit) => ({
|
|
4653
|
-
isError: exit >= 2,
|
|
4654
|
-
message: exit === 1 ? "No matches found" : void 0
|
|
4655
|
-
})],
|
|
4656
|
-
["rg", (exit) => ({
|
|
4657
|
-
isError: exit >= 2,
|
|
4658
|
-
message: exit === 1 ? "No matches found" : void 0
|
|
4659
|
-
})],
|
|
4660
|
-
["diff", (exit) => ({
|
|
4661
|
-
isError: exit >= 2,
|
|
4662
|
-
message: exit === 1 ? "Files differ" : void 0
|
|
4663
|
-
})],
|
|
4664
|
-
["find", (exit) => ({
|
|
4665
|
-
isError: exit >= 2,
|
|
4666
|
-
message: exit === 1 ? "Some directories were inaccessible" : void 0
|
|
4667
|
-
})],
|
|
4668
|
-
["test", (exit) => ({
|
|
4669
|
-
isError: exit >= 2,
|
|
4670
|
-
message: exit === 1 ? "Condition is false" : void 0
|
|
4671
|
-
})],
|
|
4672
|
-
["[", (exit) => ({
|
|
4673
|
-
isError: exit >= 2,
|
|
4674
|
-
message: exit === 1 ? "Condition is false" : void 0
|
|
4675
|
-
})]
|
|
4676
|
-
]);
|
|
4677
|
-
/**
|
|
4678
|
-
* Pick the semantic for a command line. Best-effort: walks the command from
|
|
4679
|
-
* right to left, taking the last segment after `|` / `&&` / `||` / `;` —
|
|
4680
|
-
* that's the segment whose exit code propagates. Don't depend on this for
|
|
4681
|
-
* security; it's a heuristic, not a parser.
|
|
4682
|
-
*/
|
|
4683
|
-
function interpretShellResult(command, exitCode) {
|
|
4684
|
-
const base = extractTrailingCommand(command);
|
|
4685
|
-
return (COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC)(exitCode);
|
|
4686
|
-
}
|
|
4687
|
-
function extractTrailingCommand(command) {
|
|
4688
|
-
const segments = command.split(/\|\||&&|[;|\n]/);
|
|
4689
|
-
return (segments[segments.length - 1]?.trim() ?? command).split(/\s+/).filter((t) => !/^[A-Z_]\w*=/i.test(t))[0] ?? "";
|
|
4690
|
-
}
|
|
4691
|
-
//#endregion
|
|
4692
|
-
//#region src/tools/shell.ts
|
|
4693
|
-
/**
|
|
4694
|
-
* Execute a shell command in the agent's execution context.
|
|
4695
|
-
*
|
|
4696
|
-
* Truncation is **tail-priority**: when stdout+stderr combined exceeds
|
|
4697
|
-
* `maxOutputBytes`, the head is dropped and a marker `…(N bytes truncated
|
|
4698
|
-
* from head)…` is inserted before the tail. Errors and exit summaries
|
|
4699
|
-
* usually live at the end of output, so keeping the tail preserves the
|
|
4700
|
-
* model's most useful signal.
|
|
4701
|
-
*
|
|
4702
|
-
* Defaults are tuned for typical commands (build output, test runs): the
|
|
4703
|
-
* combined cap is 32 KiB and the per-call timeout follows the execution
|
|
4704
|
-
* context's own default (30 s for in-process).
|
|
4705
|
-
*/
|
|
4706
|
-
const DEFAULT_MAX_OUTPUT_BYTES = 32768;
|
|
4707
|
-
/**
|
|
4708
|
-
* Best-effort read-only allow-list for the leading command token. Members
|
|
4709
|
-
* are commands whose stock behavior cannot mutate the workspace under any
|
|
4710
|
-
* argument combination — `ls`, `cat`, `pwd`, etc. Commands that *can*
|
|
4711
|
-
* mutate depending on flags (`find -delete`, `git tag <name>`, `tar -x`)
|
|
4712
|
-
* are intentionally excluded; the input-aware {@link isReadOnlyShellCommand}
|
|
4713
|
-
* predicate falls back to the conservative "not safe" answer for them, so
|
|
4714
|
-
* the scheduler barriers them.
|
|
4715
|
-
*/
|
|
4716
|
-
const SHELL_READ_ONLY_COMMANDS = new Set([
|
|
4717
|
-
"ls",
|
|
4718
|
-
"cat",
|
|
4719
|
-
"head",
|
|
4720
|
-
"tail",
|
|
4721
|
-
"wc",
|
|
4722
|
-
"pwd",
|
|
4723
|
-
"whoami",
|
|
4724
|
-
"id",
|
|
4725
|
-
"date",
|
|
4726
|
-
"uname",
|
|
4727
|
-
"hostname",
|
|
4728
|
-
"tty",
|
|
4729
|
-
"echo",
|
|
4730
|
-
"printf",
|
|
4731
|
-
"env",
|
|
4732
|
-
"printenv",
|
|
4733
|
-
"which",
|
|
4734
|
-
"type",
|
|
4735
|
-
"command",
|
|
4736
|
-
"file",
|
|
4737
|
-
"stat",
|
|
4738
|
-
"grep",
|
|
4739
|
-
"rg",
|
|
4740
|
-
"ag",
|
|
4741
|
-
"true",
|
|
4742
|
-
"false",
|
|
4743
|
-
"test"
|
|
4744
|
-
]);
|
|
4745
|
-
/**
|
|
4746
|
-
* `git` subcommands that are pure reads regardless of arguments. Excludes
|
|
4747
|
-
* `branch`/`tag`/`remote` (which can mutate when given a name) and
|
|
4748
|
-
* `config` (which writes when given a value).
|
|
4749
|
-
*/
|
|
4750
|
-
const GIT_READ_ONLY_SUBCOMMANDS = new Set([
|
|
4751
|
-
"status",
|
|
4752
|
-
"log",
|
|
4753
|
-
"diff",
|
|
4754
|
-
"show",
|
|
4755
|
-
"blame",
|
|
4756
|
-
"rev-parse",
|
|
4757
|
-
"ls-files",
|
|
4758
|
-
"ls-tree",
|
|
4759
|
-
"cat-file",
|
|
4760
|
-
"reflog",
|
|
4761
|
-
"shortlog",
|
|
4762
|
-
"describe",
|
|
4763
|
-
"rev-list",
|
|
4764
|
-
"name-rev",
|
|
4765
|
-
"whatchanged",
|
|
4766
|
-
"merge-base",
|
|
4767
|
-
"symbolic-ref"
|
|
4768
|
-
]);
|
|
4769
|
-
/**
|
|
4770
|
-
* Conservative read-only verdict for a shell command — used to opt a
|
|
4771
|
-
* `shell` invocation into the scheduler's concurrent fleet. Returns
|
|
4772
|
-
* `false` (fail-closed) on anything ambiguous so the scheduler barriers
|
|
4773
|
-
* it. Specifically:
|
|
4774
|
-
*
|
|
4775
|
-
* - Rejects compound commands (`;`, `&&`, `||`, `|`) and redirects (`>`,
|
|
4776
|
-
* `>>`, `<`) — even a pipe to a read-only sink is treated as too
|
|
4777
|
-
* complex to analyze.
|
|
4778
|
-
* - Rejects subshell / process substitution (`$(...)`, `` `...` ``,
|
|
4779
|
-
* `<(...)`, `>(...)`).
|
|
4780
|
-
* - Skips leading `VAR=value` env assignments to find the real
|
|
4781
|
-
* command token.
|
|
4782
|
-
* - Strips a possible absolute path on the command (`/usr/bin/ls` → `ls`).
|
|
4783
|
-
* - Allows the command iff its base name is in
|
|
4784
|
-
* {@link SHELL_READ_ONLY_COMMANDS} OR it's `git <subcmd>` where
|
|
4785
|
-
* `<subcmd>` is in {@link GIT_READ_ONLY_SUBCOMMANDS}.
|
|
4786
|
-
*
|
|
4787
|
-
* Cheap (no spawned process; regex + token scan). Safe to call from the
|
|
4788
|
-
* hot scheduler path.
|
|
4789
|
-
*/
|
|
4790
|
-
function isReadOnlyShellCommand(command) {
|
|
4791
|
-
if (typeof command !== "string") return false;
|
|
4792
|
-
const trimmed = command.trim();
|
|
4793
|
-
if (trimmed === "") return false;
|
|
4794
|
-
if (/[<>;&|`\n]/.test(trimmed)) return false;
|
|
4795
|
-
if (trimmed.includes("$(") || trimmed.includes("<(") || trimmed.includes(">(")) return false;
|
|
4796
|
-
const tokens = trimmed.split(/\s+/);
|
|
4797
|
-
let i = 0;
|
|
4798
|
-
while (i < tokens.length && /^[A-Z_]\w*=/i.test(tokens[i])) i++;
|
|
4799
|
-
const head = tokens[i];
|
|
4800
|
-
if (!head) return false;
|
|
4801
|
-
const base = head.split("/").pop() ?? head;
|
|
4802
|
-
if (SHELL_READ_ONLY_COMMANDS.has(base)) return true;
|
|
4803
|
-
if (base === "git") {
|
|
4804
|
-
const sub = tokens[i + 1];
|
|
4805
|
-
return typeof sub === "string" && GIT_READ_ONLY_SUBCOMMANDS.has(sub);
|
|
4806
|
-
}
|
|
4807
|
-
return false;
|
|
4808
|
-
}
|
|
4809
|
-
const shell = {
|
|
4810
|
-
isConcurrencySafe: (input) => isReadOnlyShellCommand(input.command),
|
|
5364
|
+
//#region src/tools/shell-kill.ts
|
|
5365
|
+
const shellKill = {
|
|
5366
|
+
isConcurrencySafe: true,
|
|
4811
5367
|
spec: {
|
|
4812
|
-
name: "
|
|
4813
|
-
description:
|
|
5368
|
+
name: "shell_kill",
|
|
5369
|
+
description: [
|
|
5370
|
+
"Terminate a running background task started by `shell({ run_in_background: true })`.",
|
|
5371
|
+
"Sends SIGTERM to the whole process group so the shell wrapper AND its child commands die together.",
|
|
5372
|
+
"Returns the final exit info (status, exit code, output path) or a \"no such task\" message when the id is unknown / already cleaned up.",
|
|
5373
|
+
"Idempotent — calling on a task that has already terminated returns the cached exit info without re-killing."
|
|
5374
|
+
].join("\n"),
|
|
4814
5375
|
inputSchema: {
|
|
4815
5376
|
type: "object",
|
|
4816
|
-
properties: {
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
type: "integer",
|
|
4823
|
-
description: "Per-call timeout in milliseconds."
|
|
4824
|
-
},
|
|
4825
|
-
maxOutputBytes: {
|
|
4826
|
-
type: "integer",
|
|
4827
|
-
description: "Truncate combined stdout+stderr beyond this many bytes. Default: 32768. Set 0 for unlimited."
|
|
4828
|
-
},
|
|
4829
|
-
metadata: {
|
|
4830
|
-
type: "boolean",
|
|
4831
|
-
description: "Append `(exit N, Nms)` footer and surface non-empty stderr on success. Default: true."
|
|
4832
|
-
}
|
|
4833
|
-
},
|
|
4834
|
-
required: ["command"]
|
|
5377
|
+
properties: { task_id: {
|
|
5378
|
+
type: "string",
|
|
5379
|
+
description: "The task id returned by a prior `shell({ run_in_background: true })` call."
|
|
5380
|
+
} },
|
|
5381
|
+
required: ["task_id"],
|
|
5382
|
+
additionalProperties: false
|
|
4835
5383
|
}
|
|
4836
5384
|
},
|
|
4837
|
-
async execute(
|
|
4838
|
-
const
|
|
4839
|
-
if (
|
|
4840
|
-
const
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
if (result.exitCode === 0) {
|
|
4848
|
-
const stdoutTail = truncateTail(result.stdout || "(no output)", cap);
|
|
4849
|
-
if (!wantMetadata) return stdoutTail;
|
|
4850
|
-
const stderrTrimmed = result.stderr.trim();
|
|
4851
|
-
return `${stdoutTail}${stderrTrimmed ? `\n[stderr]\n${truncateTail(stderrTrimmed, Math.min(cap, 2048))}` : ""}\n(exit 0, ${durationMs}ms)`;
|
|
4852
|
-
}
|
|
4853
|
-
if (!semantic.isError) {
|
|
4854
|
-
const tail = truncateTail((result.stdout || result.stderr || "").trim(), cap);
|
|
4855
|
-
const semanticFooter = semantic.message ? `\n(${semantic.message})` : "";
|
|
4856
|
-
const timingFooter = wantMetadata ? `\n(exit ${result.exitCode}, ${durationMs}ms)` : "";
|
|
4857
|
-
return `${tail.length > 0 ? tail : semantic.message ?? "(no output)"}${semanticFooter}${timingFooter}`;
|
|
4858
|
-
}
|
|
4859
|
-
const combined = `${result.stdout}\n${result.stderr}`.trim();
|
|
4860
|
-
return `${wantMetadata ? `Exit code ${result.exitCode} (${durationMs}ms)` : `Exit code ${result.exitCode}`}\n${truncateTail(combined, cap)}`;
|
|
5385
|
+
async execute(input, ctx) {
|
|
5386
|
+
const taskId = input.task_id;
|
|
5387
|
+
if (!ctx.execution.killBackground) return `shell_kill error: the active execution context (${ctx.execution.type}) does not support background tasks.`;
|
|
5388
|
+
const info = await ctx.execution.killBackground(ctx.handle, taskId);
|
|
5389
|
+
if (!info) return `shell_kill: no such task "${taskId}". It may have already exited and been cleaned up, or it was never started in this session.`;
|
|
5390
|
+
return [
|
|
5391
|
+
`Killed ${info.taskId} — ${formatTaskStatus(info)} after ${formatDuration(info.durationMs)}.`,
|
|
5392
|
+
` command: ${previewLine(info.command, 60)}`,
|
|
5393
|
+
` output: ${info.outputPath}`
|
|
5394
|
+
].join("\n");
|
|
4861
5395
|
}
|
|
4862
5396
|
};
|
|
4863
|
-
function normalizeCap(value) {
|
|
4864
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_MAX_OUTPUT_BYTES;
|
|
4865
|
-
if (value < 0) return DEFAULT_MAX_OUTPUT_BYTES;
|
|
4866
|
-
return Math.floor(value);
|
|
4867
|
-
}
|
|
4868
|
-
/**
|
|
4869
|
-
* Tail-priority byte truncation. When `text` exceeds `cap` bytes, the head is
|
|
4870
|
-
* dropped and replaced with a marker. Always cuts on character boundaries (no
|
|
4871
|
-
* mid-codepoint splits) by walking from the end with `Buffer.byteLength`.
|
|
4872
|
-
*
|
|
4873
|
-
* `cap === 0` disables truncation. `cap` is interpreted as a UTF-8 byte budget
|
|
4874
|
-
* for the tail itself — the marker is added on top and may push the visible
|
|
4875
|
-
* length slightly past `cap`. That tradeoff is intentional: a marker that
|
|
4876
|
-
* always fits inside the budget would shrink the actual content displayed.
|
|
4877
|
-
*/
|
|
4878
|
-
function truncateTail(text, cap) {
|
|
4879
|
-
if (cap === 0) return text;
|
|
4880
|
-
const totalBytes = Buffer.byteLength(text);
|
|
4881
|
-
if (totalBytes <= cap) return text;
|
|
4882
|
-
let bytes = 0;
|
|
4883
|
-
let charIdx = text.length;
|
|
4884
|
-
while (charIdx > 0) {
|
|
4885
|
-
const ch = text[charIdx - 1];
|
|
4886
|
-
const chBytes = Buffer.byteLength(ch);
|
|
4887
|
-
if (bytes + chBytes > cap) break;
|
|
4888
|
-
bytes += chBytes;
|
|
4889
|
-
charIdx--;
|
|
4890
|
-
}
|
|
4891
|
-
const tail = text.slice(charIdx);
|
|
4892
|
-
return `…(${totalBytes - Buffer.byteLength(tail)} bytes truncated from head)…\n${tail}`;
|
|
4893
|
-
}
|
|
4894
5397
|
//#endregion
|
|
4895
5398
|
//#region src/tools/spawn.ts
|
|
4896
5399
|
const BUBBLED_EVENTS = [
|
|
@@ -4902,6 +5405,10 @@ const BUBBLED_EVENTS = [
|
|
|
4902
5405
|
"tool:before",
|
|
4903
5406
|
"tool:after",
|
|
4904
5407
|
"tool:error",
|
|
5408
|
+
"tool:cancelled",
|
|
5409
|
+
"background:start",
|
|
5410
|
+
"background:exit",
|
|
5411
|
+
"background:reassign",
|
|
4905
5412
|
"turn:after"
|
|
4906
5413
|
];
|
|
4907
5414
|
const BUBBLED_MUTABLE_EVENTS = [
|
|
@@ -4918,6 +5425,10 @@ const CHILD_EVENT_NAME = {
|
|
|
4918
5425
|
"tool:before": "child:tool:before",
|
|
4919
5426
|
"tool:after": "child:tool:after",
|
|
4920
5427
|
"tool:error": "child:tool:error",
|
|
5428
|
+
"tool:cancelled": "child:tool:cancelled",
|
|
5429
|
+
"background:start": "child:background:start",
|
|
5430
|
+
"background:exit": "child:background:exit",
|
|
5431
|
+
"background:reassign": "child:background:reassign",
|
|
4921
5432
|
"turn:after": "child:turn:after"
|
|
4922
5433
|
};
|
|
4923
5434
|
const CHILD_MUTABLE_EVENT_NAME = {
|
|
@@ -5146,7 +5657,7 @@ function createSpawnTool(options = {}) {
|
|
|
5146
5657
|
});
|
|
5147
5658
|
if (forwardHooks) {
|
|
5148
5659
|
const unregisterEnricher = agent.hooks.hook("tool:before", async (toolCtx) => {
|
|
5149
|
-
if (toolCtx.name !== "write_file") return;
|
|
5660
|
+
if (toolCtx.name !== "write_file" && toolCtx.name !== "edit" && toolCtx.name !== "multi_edit") return;
|
|
5150
5661
|
if (!agent.handle) return;
|
|
5151
5662
|
const inputPath = toolCtx.input?.path;
|
|
5152
5663
|
if (typeof inputPath !== "string") return;
|
|
@@ -5232,6 +5743,24 @@ function createSpawnTool(options = {}) {
|
|
|
5232
5743
|
});
|
|
5233
5744
|
}
|
|
5234
5745
|
} finally {
|
|
5746
|
+
const childHandle = agent.handle;
|
|
5747
|
+
if (childHandle && ctx.execution.reassignBackgroundTasks) try {
|
|
5748
|
+
const reassigned = await ctx.execution.reassignBackgroundTasks(childHandle, ctx.handle, (info) => {
|
|
5749
|
+
ctx.hooks.callHook("background:exit", info);
|
|
5750
|
+
});
|
|
5751
|
+
for (const entry of reassigned) await ctx.hooks.callHook("background:reassign", {
|
|
5752
|
+
taskId: entry.taskId,
|
|
5753
|
+
fromHandleId: childHandle.id,
|
|
5754
|
+
toHandleId: ctx.handle.id,
|
|
5755
|
+
childId: id,
|
|
5756
|
+
pid: entry.pid,
|
|
5757
|
+
command: entry.command,
|
|
5758
|
+
outputPath: entry.outputPath,
|
|
5759
|
+
startedAt: entry.startedAt
|
|
5760
|
+
});
|
|
5761
|
+
} catch (err) {
|
|
5762
|
+
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/spawn] reassignBackgroundTasks failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
5763
|
+
}
|
|
5235
5764
|
try {
|
|
5236
5765
|
await agent.destroy();
|
|
5237
5766
|
} catch (err) {
|
|
@@ -5321,6 +5850,6 @@ const writeFile$1 = {
|
|
|
5321
5850
|
}
|
|
5322
5851
|
};
|
|
5323
5852
|
//#endregion
|
|
5324
|
-
export {
|
|
5853
|
+
export { resolvePersistDir as A, formatTaskStatus as B, TOOL_USE_SKIPPED_MESSAGE as C, buildPersistedStub as D, PERSISTENCE_PREVIEW_BYTES as E, resolveReadStateMap as F, previewLine as H, ageString as I, compactPath as L, getReadState as M, hashContent as N, cleanupPersistedSession as O, readStateKey as P, fmtTokens as R, TOOL_USE_CANCELLED_MESSAGE as S, PERSISTED_STUB_PREFIX as T, shortId as U, formatTaskSummary as V, createSkillsReadTool as _, multiEdit as a, INTERRUPT_MESSAGE_FOR_TOOL_USE as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, resolveTasksDir as j, maybePersistToolResult as k, glob as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shellKill as r, createInteractionTool as s, writeFile$1 as t, edit as u, createShellTool as v, validateToolArgs as w, SHELL_CASCADE_CANCEL_MESSAGE as x, shell as y, formatDuration as z };
|
|
5325
5854
|
|
|
5326
|
-
//# sourceMappingURL=tools-
|
|
5855
|
+
//# sourceMappingURL=tools-D_icxa-V.js.map
|