zidane 5.4.3 → 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 +30 -1
- package/dist/{agent-Yu8uhpy-.d.ts → agent-CvImMxMQ.d.ts} +183 -3
- package/dist/agent-CvImMxMQ.d.ts.map +1 -0
- package/dist/chat.d.ts +93 -15
- 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/{index-DklfxeYy.d.ts → index-B0uc2C5x.d.ts} +3 -3
- package/dist/{index-DklfxeYy.d.ts.map → index-B0uc2C5x.d.ts.map} +1 -1
- 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-j9tY28ah.d.ts → index-CtXksgqb.d.ts} +60 -4
- package/dist/index-CtXksgqb.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +8 -8
- package/dist/{interpolate-CmtjEyRJ.js → interpolate-BaaKaKzN.js} +2 -2
- package/dist/{interpolate-CmtjEyRJ.js.map → interpolate-BaaKaKzN.js.map} +1 -1
- package/dist/{login-DxyAERe1.js → login-iTy-0wYz.js} +2 -2
- package/dist/{login-DxyAERe1.js.map → login-iTy-0wYz.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/{presets-D9IbaI40.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-CEzRFYtS.js → providers-G0VBZK9j.js} +2 -2
- package/dist/{providers-CEzRFYtS.js.map → providers-G0VBZK9j.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -0
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-kwsNnOmt.js → session-CbkiJDlH.js} +2 -1
- package/dist/session-CbkiJDlH.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +1 -1
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -1
- package/dist/{tools-BK2vG9UX.js → tools-D_icxa-V.js} +668 -256
- 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-DnaBcJej.d.ts → transcript-anchors-3FFw2xuk.d.ts} +49 -10
- package/dist/transcript-anchors-3FFw2xuk.d.ts.map +1 -0
- package/dist/tui.d.ts +27 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +239 -39
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-OzKEOXul.js → turn-operations-CtgBlBHn.js} +178 -79
- 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/docs/ARCHITECTURE.md +21 -0
- package/docs/CHAT.md +3 -1
- package/docs/RUN_IN_BACKGROUND.md +612 -0
- package/docs/SKILL.md +59 -0
- package/docs/TUI.md +16 -2
- package/package.json +2 -2
- package/dist/agent-Yu8uhpy-.d.ts.map +0 -1
- package/dist/contexts-BwiHIr2w.js +0 -129
- package/dist/contexts-BwiHIr2w.js.map +0 -1
- package/dist/index-BiO_5Hm4.d.ts.map +0 -1
- package/dist/index-j9tY28ah.d.ts.map +0 -1
- package/dist/presets-D9IbaI40.js.map +0 -1
- package/dist/session-kwsNnOmt.js.map +0 -1
- package/dist/tools-BK2vG9UX.js.map +0 -1
- package/dist/transcript-anchors-DnaBcJej.d.ts.map +0 -1
- package/dist/turn-operations-OzKEOXul.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-
|
|
1
|
+
import { n as createProcessContext } from "./contexts-DhmMlT2W.js";
|
|
2
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
4
|
import { a as detectTurnInterruption, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureToolResultPairing, s as filterUnresolvedToolUses } from "./messages-fTR19Ga6.js";
|
|
5
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-
|
|
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.
|
|
@@ -1883,7 +2020,9 @@ async function runSingleToolDispatch(ctx, call, turnId, fixed) {
|
|
|
1883
2020
|
removeAbortListener?.();
|
|
1884
2021
|
}
|
|
1885
2022
|
} catch (err) {
|
|
1886
|
-
|
|
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;
|
|
1887
2026
|
else {
|
|
1888
2027
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1889
2028
|
const errorCtx = {
|
|
@@ -2110,7 +2249,8 @@ async function executeToolBatch(ctx, toolCalls, turnId) {
|
|
|
2110
2249
|
const call = toolCalls[index];
|
|
2111
2250
|
return executeSingleTool(childCtx, call, turnId).then(({ result }) => {
|
|
2112
2251
|
results[index] = result;
|
|
2113
|
-
|
|
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);
|
|
2114
2254
|
}, (err) => {
|
|
2115
2255
|
const isAbort = siblingAbort.signal.aborted || ctx.signal.aborted || err instanceof Error && err.name === "AbortError";
|
|
2116
2256
|
results[index] = {
|
|
@@ -2337,6 +2477,376 @@ function defaultBlockMessage(tool, max) {
|
|
|
2337
2477
|
return `Tool '${tool}' has reached its per-run budget of ${max} calls; further invocations are refused.`;
|
|
2338
2478
|
}
|
|
2339
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
|
|
2340
2850
|
//#region src/tools/binary-detect.ts
|
|
2341
2851
|
/**
|
|
2342
2852
|
* Heuristics for detecting binary content in UTF-8-decoded strings.
|
|
@@ -2504,7 +3014,10 @@ function createSkillsRunScriptTool(options) {
|
|
|
2504
3014
|
if (!validated.valid) return `Error: ${validated.error}`;
|
|
2505
3015
|
const cmd = [validated.absolutePath, ...args].map(alwaysQuote).join(" ");
|
|
2506
3016
|
try {
|
|
2507
|
-
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
|
+
});
|
|
2508
3021
|
return JSON.stringify({
|
|
2509
3022
|
exitCode: result.exitCode,
|
|
2510
3023
|
stdout: result.stdout,
|
|
@@ -2751,6 +3264,39 @@ function createToolSearchTool(options) {
|
|
|
2751
3264
|
}
|
|
2752
3265
|
//#endregion
|
|
2753
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
|
+
}
|
|
2754
3300
|
const HOOK_EVENT_SET = new Set([
|
|
2755
3301
|
"system:before",
|
|
2756
3302
|
"agent:start",
|
|
@@ -2791,6 +3337,9 @@ const HOOK_EVENT_SET = new Set([
|
|
|
2791
3337
|
"child:tool:transform",
|
|
2792
3338
|
"child:tool:error",
|
|
2793
3339
|
"child:tool:cancelled",
|
|
3340
|
+
"child:background:start",
|
|
3341
|
+
"child:background:exit",
|
|
3342
|
+
"child:background:reassign",
|
|
2794
3343
|
"child:turn:after",
|
|
2795
3344
|
"mcp:connect",
|
|
2796
3345
|
"mcp:error",
|
|
@@ -2807,6 +3356,9 @@ const HOOK_EVENT_SET = new Set([
|
|
|
2807
3356
|
"mcp:tool:after",
|
|
2808
3357
|
"mcp:tool:transform",
|
|
2809
3358
|
"mcp:tool:error",
|
|
3359
|
+
"background:start",
|
|
3360
|
+
"background:exit",
|
|
3361
|
+
"background:reassign",
|
|
2810
3362
|
"skills:resolve",
|
|
2811
3363
|
"skills:catalog",
|
|
2812
3364
|
"skills:activate",
|
|
@@ -2907,6 +3459,8 @@ function resolveBehavior(agentBehavior, runBehavior) {
|
|
|
2907
3459
|
persistThreshold: runBehavior?.persistThreshold ?? agentBehavior?.persistThreshold,
|
|
2908
3460
|
persistExcludeTools: runBehavior?.persistExcludeTools ?? agentBehavior?.persistExcludeTools,
|
|
2909
3461
|
persistDir: runBehavior?.persistDir ?? agentBehavior?.persistDir,
|
|
3462
|
+
tasksDir: runBehavior?.tasksDir ?? agentBehavior?.tasksDir,
|
|
3463
|
+
disableBackgroundTasks: runBehavior?.disableBackgroundTasks ?? agentBehavior?.disableBackgroundTasks,
|
|
2910
3464
|
strictToolPairing: runBehavior?.strictToolPairing ?? agentBehavior?.strictToolPairing ?? false
|
|
2911
3465
|
};
|
|
2912
3466
|
}
|
|
@@ -3070,6 +3624,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3070
3624
|
let running = false;
|
|
3071
3625
|
let idleResolve;
|
|
3072
3626
|
let idlePromise;
|
|
3627
|
+
const pendingTaskNotifications = /* @__PURE__ */ new Map();
|
|
3073
3628
|
const pendingToolCancels = /* @__PURE__ */ new Map();
|
|
3074
3629
|
let executionHandle = null;
|
|
3075
3630
|
let mcpConnection = null;
|
|
@@ -3138,6 +3693,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3138
3693
|
running = true;
|
|
3139
3694
|
try {
|
|
3140
3695
|
abortController = new AbortController();
|
|
3696
|
+
runCounter = Math.max(runCounter, initialRunCounter(session));
|
|
3141
3697
|
const runId = `run_${++runCounter}`;
|
|
3142
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") : "";
|
|
3143
3699
|
session?.startRun(runId, promptLabel, {
|
|
@@ -3240,6 +3796,9 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3240
3796
|
} : runBaseTools;
|
|
3241
3797
|
const toolsPreSearch = {};
|
|
3242
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
|
+
}
|
|
3243
3802
|
const disclosure = partitionToolDisclosure(toolsPreSearch, mcpToolNames, mcpServers, toolDisclosure, toolAliases);
|
|
3244
3803
|
const unlocked = new Set(disclosure.eagerCanonicalNames);
|
|
3245
3804
|
const hostDefinedToolSearch = !!toolsPreSearch.tool_search;
|
|
@@ -3274,8 +3833,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3274
3833
|
}
|
|
3275
3834
|
const formattedTools = buildFormattedTools();
|
|
3276
3835
|
const turns = [];
|
|
3277
|
-
|
|
3278
|
-
if (isResume) {
|
|
3836
|
+
if (session && session.turns.length > 0 && (session.runs.length > 0 || !options.prompt) && !options.parentRunId) {
|
|
3279
3837
|
const childRunIds = new Set(session.runs.filter((r) => (r.depth ?? 0) > 0).map((r) => r.id));
|
|
3280
3838
|
const resumed = childRunIds.size === 0 ? session.turns : session.turns.filter((t) => !t.runId || !childRunIds.has(t.runId));
|
|
3281
3839
|
const filteredForRuntime = resumeFilteredTurns && resumed === session.turns ? resumeFilteredTurns : filterUnresolvedToolUses(resumed);
|
|
@@ -3283,19 +3841,28 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3283
3841
|
}
|
|
3284
3842
|
const runTurnStart = turns.length;
|
|
3285
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;
|
|
3286
3853
|
const promptParts = canonicalizePrompt(options.prompt);
|
|
3287
|
-
if (promptParts) {
|
|
3288
|
-
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 : []];
|
|
3289
3857
|
turns.push({
|
|
3290
3858
|
id: crypto.randomUUID(),
|
|
3291
3859
|
runId,
|
|
3292
|
-
role: promptMsg.role,
|
|
3293
|
-
content
|
|
3860
|
+
role: promptMsg ? promptMsg.role : "user",
|
|
3861
|
+
content,
|
|
3294
3862
|
createdAt: Date.now()
|
|
3295
3863
|
});
|
|
3296
3864
|
}
|
|
3297
3865
|
conversationTurns = turns;
|
|
3298
|
-
let lastPersistedTurnCount = isResume ? session.turns.length : 0;
|
|
3299
3866
|
if (session && turns.length > lastPersistedTurnCount) {
|
|
3300
3867
|
const seededTurns = turns.slice(lastPersistedTurnCount);
|
|
3301
3868
|
await session.appendTurns(seededTurns);
|
|
@@ -3510,6 +4077,11 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3510
4077
|
controller.abort(reason ?? "user-cancelled-tool");
|
|
3511
4078
|
return true;
|
|
3512
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
|
+
}
|
|
3513
4085
|
function steer(message) {
|
|
3514
4086
|
steeringQueue.push(message);
|
|
3515
4087
|
}
|
|
@@ -3524,6 +4096,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3524
4096
|
conversationTurns = [];
|
|
3525
4097
|
steeringQueue.length = 0;
|
|
3526
4098
|
followUpQueue.length = 0;
|
|
4099
|
+
pendingTaskNotifications.clear();
|
|
3527
4100
|
const cleared = skillActivationState.clear();
|
|
3528
4101
|
for (const record of cleared) await hooks.callHook("skills:deactivate", {
|
|
3529
4102
|
skill: record.skill,
|
|
@@ -3552,6 +4125,33 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3552
4125
|
reason: "explicit"
|
|
3553
4126
|
});
|
|
3554
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
|
+
});
|
|
3555
4155
|
if (session) {
|
|
3556
4156
|
const originalSave = session.save.bind(session);
|
|
3557
4157
|
const originalSetMeta = session.setMeta.bind(session);
|
|
@@ -3603,6 +4203,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3603
4203
|
async function destroy() {
|
|
3604
4204
|
if (destroyed) return;
|
|
3605
4205
|
destroyed = true;
|
|
4206
|
+
pendingTaskNotifications.clear();
|
|
3606
4207
|
for (const controller of pendingToolCancels.values()) if (!controller.signal.aborted) controller.abort("agent-destroyed");
|
|
3607
4208
|
pendingToolCancels.clear();
|
|
3608
4209
|
if (mcpWarmupPromise) try {
|
|
@@ -3616,6 +4217,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3616
4217
|
await executionContext.destroy(executionHandle);
|
|
3617
4218
|
executionHandle = null;
|
|
3618
4219
|
}
|
|
4220
|
+
pendingTaskNotifications.clear();
|
|
3619
4221
|
skillsCleanup();
|
|
3620
4222
|
skillsCleanup = () => {};
|
|
3621
4223
|
}
|
|
@@ -3626,6 +4228,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
3626
4228
|
run,
|
|
3627
4229
|
abort,
|
|
3628
4230
|
cancelTool,
|
|
4231
|
+
killBackgroundTask,
|
|
3629
4232
|
steer,
|
|
3630
4233
|
followUp: followUpFn,
|
|
3631
4234
|
waitForIdle,
|
|
@@ -4758,254 +5361,39 @@ function normalizeInteger(value, fallback) {
|
|
|
4758
5361
|
return Math.floor(value);
|
|
4759
5362
|
}
|
|
4760
5363
|
//#endregion
|
|
4761
|
-
//#region src/tools/shell-
|
|
4762
|
-
const
|
|
4763
|
-
|
|
4764
|
-
message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
|
|
4765
|
-
});
|
|
4766
|
-
const COMMAND_SEMANTICS = new Map([
|
|
4767
|
-
["grep", (exit) => ({
|
|
4768
|
-
isError: exit >= 2,
|
|
4769
|
-
message: exit === 1 ? "No matches found" : void 0
|
|
4770
|
-
})],
|
|
4771
|
-
["rg", (exit) => ({
|
|
4772
|
-
isError: exit >= 2,
|
|
4773
|
-
message: exit === 1 ? "No matches found" : void 0
|
|
4774
|
-
})],
|
|
4775
|
-
["diff", (exit) => ({
|
|
4776
|
-
isError: exit >= 2,
|
|
4777
|
-
message: exit === 1 ? "Files differ" : void 0
|
|
4778
|
-
})],
|
|
4779
|
-
["find", (exit) => ({
|
|
4780
|
-
isError: exit >= 2,
|
|
4781
|
-
message: exit === 1 ? "Some directories were inaccessible" : void 0
|
|
4782
|
-
})],
|
|
4783
|
-
["test", (exit) => ({
|
|
4784
|
-
isError: exit >= 2,
|
|
4785
|
-
message: exit === 1 ? "Condition is false" : void 0
|
|
4786
|
-
})],
|
|
4787
|
-
["[", (exit) => ({
|
|
4788
|
-
isError: exit >= 2,
|
|
4789
|
-
message: exit === 1 ? "Condition is false" : void 0
|
|
4790
|
-
})]
|
|
4791
|
-
]);
|
|
4792
|
-
/**
|
|
4793
|
-
* Pick the semantic for a command line. Best-effort: walks the command from
|
|
4794
|
-
* right to left, taking the last segment after `|` / `&&` / `||` / `;` —
|
|
4795
|
-
* that's the segment whose exit code propagates. Don't depend on this for
|
|
4796
|
-
* security; it's a heuristic, not a parser.
|
|
4797
|
-
*/
|
|
4798
|
-
function interpretShellResult(command, exitCode) {
|
|
4799
|
-
const base = extractTrailingCommand(command);
|
|
4800
|
-
return (COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC)(exitCode);
|
|
4801
|
-
}
|
|
4802
|
-
function extractTrailingCommand(command) {
|
|
4803
|
-
const segments = command.split(/\|\||&&|[;|\n]/);
|
|
4804
|
-
return (segments[segments.length - 1]?.trim() ?? command).split(/\s+/).filter((t) => !/^[A-Z_]\w*=/i.test(t))[0] ?? "";
|
|
4805
|
-
}
|
|
4806
|
-
//#endregion
|
|
4807
|
-
//#region src/tools/shell.ts
|
|
4808
|
-
/**
|
|
4809
|
-
* Execute a shell command in the agent's execution context.
|
|
4810
|
-
*
|
|
4811
|
-
* Truncation is **tail-priority**: when stdout+stderr combined exceeds
|
|
4812
|
-
* `maxOutputBytes`, the head is dropped and a marker `…(N bytes truncated
|
|
4813
|
-
* from head)…` is inserted before the tail. Errors and exit summaries
|
|
4814
|
-
* usually live at the end of output, so keeping the tail preserves the
|
|
4815
|
-
* model's most useful signal.
|
|
4816
|
-
*
|
|
4817
|
-
* Defaults are tuned for typical commands (build output, test runs): the
|
|
4818
|
-
* combined cap is 32 KiB and the per-call timeout follows the execution
|
|
4819
|
-
* context's own default (30 s for in-process).
|
|
4820
|
-
*/
|
|
4821
|
-
const DEFAULT_MAX_OUTPUT_BYTES = 32768;
|
|
4822
|
-
/**
|
|
4823
|
-
* Best-effort read-only allow-list for the leading command token. Members
|
|
4824
|
-
* are commands whose stock behavior cannot mutate the workspace under any
|
|
4825
|
-
* argument combination — `ls`, `cat`, `pwd`, etc. Commands that *can*
|
|
4826
|
-
* mutate depending on flags (`find -delete`, `git tag <name>`, `tar -x`)
|
|
4827
|
-
* are intentionally excluded; the input-aware {@link isReadOnlyShellCommand}
|
|
4828
|
-
* predicate falls back to the conservative "not safe" answer for them, so
|
|
4829
|
-
* the scheduler barriers them.
|
|
4830
|
-
*/
|
|
4831
|
-
const SHELL_READ_ONLY_COMMANDS = new Set([
|
|
4832
|
-
"ls",
|
|
4833
|
-
"cat",
|
|
4834
|
-
"head",
|
|
4835
|
-
"tail",
|
|
4836
|
-
"wc",
|
|
4837
|
-
"pwd",
|
|
4838
|
-
"whoami",
|
|
4839
|
-
"id",
|
|
4840
|
-
"date",
|
|
4841
|
-
"uname",
|
|
4842
|
-
"hostname",
|
|
4843
|
-
"tty",
|
|
4844
|
-
"echo",
|
|
4845
|
-
"printf",
|
|
4846
|
-
"env",
|
|
4847
|
-
"printenv",
|
|
4848
|
-
"which",
|
|
4849
|
-
"type",
|
|
4850
|
-
"command",
|
|
4851
|
-
"file",
|
|
4852
|
-
"stat",
|
|
4853
|
-
"grep",
|
|
4854
|
-
"rg",
|
|
4855
|
-
"ag",
|
|
4856
|
-
"true",
|
|
4857
|
-
"false",
|
|
4858
|
-
"test"
|
|
4859
|
-
]);
|
|
4860
|
-
/**
|
|
4861
|
-
* `git` subcommands that are pure reads regardless of arguments. Excludes
|
|
4862
|
-
* `branch`/`tag`/`remote` (which can mutate when given a name) and
|
|
4863
|
-
* `config` (which writes when given a value).
|
|
4864
|
-
*/
|
|
4865
|
-
const GIT_READ_ONLY_SUBCOMMANDS = new Set([
|
|
4866
|
-
"status",
|
|
4867
|
-
"log",
|
|
4868
|
-
"diff",
|
|
4869
|
-
"show",
|
|
4870
|
-
"blame",
|
|
4871
|
-
"rev-parse",
|
|
4872
|
-
"ls-files",
|
|
4873
|
-
"ls-tree",
|
|
4874
|
-
"cat-file",
|
|
4875
|
-
"reflog",
|
|
4876
|
-
"shortlog",
|
|
4877
|
-
"describe",
|
|
4878
|
-
"rev-list",
|
|
4879
|
-
"name-rev",
|
|
4880
|
-
"whatchanged",
|
|
4881
|
-
"merge-base",
|
|
4882
|
-
"symbolic-ref"
|
|
4883
|
-
]);
|
|
4884
|
-
/**
|
|
4885
|
-
* Conservative read-only verdict for a shell command — used to opt a
|
|
4886
|
-
* `shell` invocation into the scheduler's concurrent fleet. Returns
|
|
4887
|
-
* `false` (fail-closed) on anything ambiguous so the scheduler barriers
|
|
4888
|
-
* it. Specifically:
|
|
4889
|
-
*
|
|
4890
|
-
* - Rejects compound commands (`;`, `&&`, `||`, `|`) and redirects (`>`,
|
|
4891
|
-
* `>>`, `<`) — even a pipe to a read-only sink is treated as too
|
|
4892
|
-
* complex to analyze.
|
|
4893
|
-
* - Rejects subshell / process substitution (`$(...)`, `` `...` ``,
|
|
4894
|
-
* `<(...)`, `>(...)`).
|
|
4895
|
-
* - Skips leading `VAR=value` env assignments to find the real
|
|
4896
|
-
* command token.
|
|
4897
|
-
* - Strips a possible absolute path on the command (`/usr/bin/ls` → `ls`).
|
|
4898
|
-
* - Allows the command iff its base name is in
|
|
4899
|
-
* {@link SHELL_READ_ONLY_COMMANDS} OR it's `git <subcmd>` where
|
|
4900
|
-
* `<subcmd>` is in {@link GIT_READ_ONLY_SUBCOMMANDS}.
|
|
4901
|
-
*
|
|
4902
|
-
* Cheap (no spawned process; regex + token scan). Safe to call from the
|
|
4903
|
-
* hot scheduler path.
|
|
4904
|
-
*/
|
|
4905
|
-
function isReadOnlyShellCommand(command) {
|
|
4906
|
-
if (typeof command !== "string") return false;
|
|
4907
|
-
const trimmed = command.trim();
|
|
4908
|
-
if (trimmed === "") return false;
|
|
4909
|
-
if (/[<>;&|`\n]/.test(trimmed)) return false;
|
|
4910
|
-
if (trimmed.includes("$(") || trimmed.includes("<(") || trimmed.includes(">(")) return false;
|
|
4911
|
-
const tokens = trimmed.split(/\s+/);
|
|
4912
|
-
let i = 0;
|
|
4913
|
-
while (i < tokens.length && /^[A-Z_]\w*=/i.test(tokens[i])) i++;
|
|
4914
|
-
const head = tokens[i];
|
|
4915
|
-
if (!head) return false;
|
|
4916
|
-
const base = head.split("/").pop() ?? head;
|
|
4917
|
-
if (SHELL_READ_ONLY_COMMANDS.has(base)) return true;
|
|
4918
|
-
if (base === "git") {
|
|
4919
|
-
const sub = tokens[i + 1];
|
|
4920
|
-
return typeof sub === "string" && GIT_READ_ONLY_SUBCOMMANDS.has(sub);
|
|
4921
|
-
}
|
|
4922
|
-
return false;
|
|
4923
|
-
}
|
|
4924
|
-
const shell = {
|
|
4925
|
-
isConcurrencySafe: (input) => isReadOnlyShellCommand(input.command),
|
|
5364
|
+
//#region src/tools/shell-kill.ts
|
|
5365
|
+
const shellKill = {
|
|
5366
|
+
isConcurrencySafe: true,
|
|
4926
5367
|
spec: {
|
|
4927
|
-
name: "
|
|
4928
|
-
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"),
|
|
4929
5375
|
inputSchema: {
|
|
4930
5376
|
type: "object",
|
|
4931
|
-
properties: {
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
type: "integer",
|
|
4938
|
-
description: "Per-call timeout in milliseconds."
|
|
4939
|
-
},
|
|
4940
|
-
maxOutputBytes: {
|
|
4941
|
-
type: "integer",
|
|
4942
|
-
description: "Truncate combined stdout+stderr beyond this many bytes. Default: 32768. Set 0 for unlimited."
|
|
4943
|
-
},
|
|
4944
|
-
metadata: {
|
|
4945
|
-
type: "boolean",
|
|
4946
|
-
description: "Append `(exit N, Nms)` footer and surface non-empty stderr on success. Default: true."
|
|
4947
|
-
}
|
|
4948
|
-
},
|
|
4949
|
-
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
|
|
4950
5383
|
}
|
|
4951
5384
|
},
|
|
4952
|
-
async execute(
|
|
4953
|
-
const
|
|
4954
|
-
if (
|
|
4955
|
-
const
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
if (result.exitCode === 0) {
|
|
4963
|
-
const stdoutTail = truncateTail(result.stdout || "(no output)", cap);
|
|
4964
|
-
if (!wantMetadata) return stdoutTail;
|
|
4965
|
-
const stderrTrimmed = result.stderr.trim();
|
|
4966
|
-
return `${stdoutTail}${stderrTrimmed ? `\n[stderr]\n${truncateTail(stderrTrimmed, Math.min(cap, 2048))}` : ""}\n(exit 0, ${durationMs}ms)`;
|
|
4967
|
-
}
|
|
4968
|
-
if (!semantic.isError) {
|
|
4969
|
-
const tail = truncateTail((result.stdout || result.stderr || "").trim(), cap);
|
|
4970
|
-
const semanticFooter = semantic.message ? `\n(${semantic.message})` : "";
|
|
4971
|
-
const timingFooter = wantMetadata ? `\n(exit ${result.exitCode}, ${durationMs}ms)` : "";
|
|
4972
|
-
return `${tail.length > 0 ? tail : semantic.message ?? "(no output)"}${semanticFooter}${timingFooter}`;
|
|
4973
|
-
}
|
|
4974
|
-
const combined = `${result.stdout}\n${result.stderr}`.trim();
|
|
4975
|
-
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");
|
|
4976
5395
|
}
|
|
4977
5396
|
};
|
|
4978
|
-
function normalizeCap(value) {
|
|
4979
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_MAX_OUTPUT_BYTES;
|
|
4980
|
-
if (value < 0) return DEFAULT_MAX_OUTPUT_BYTES;
|
|
4981
|
-
return Math.floor(value);
|
|
4982
|
-
}
|
|
4983
|
-
/**
|
|
4984
|
-
* Tail-priority byte truncation. When `text` exceeds `cap` bytes, the head is
|
|
4985
|
-
* dropped and replaced with a marker. Always cuts on character boundaries (no
|
|
4986
|
-
* mid-codepoint splits) by walking from the end with `Buffer.byteLength`.
|
|
4987
|
-
*
|
|
4988
|
-
* `cap === 0` disables truncation. `cap` is interpreted as a UTF-8 byte budget
|
|
4989
|
-
* for the tail itself — the marker is added on top and may push the visible
|
|
4990
|
-
* length slightly past `cap`. That tradeoff is intentional: a marker that
|
|
4991
|
-
* always fits inside the budget would shrink the actual content displayed.
|
|
4992
|
-
*/
|
|
4993
|
-
function truncateTail(text, cap) {
|
|
4994
|
-
if (cap === 0) return text;
|
|
4995
|
-
const totalBytes = Buffer.byteLength(text);
|
|
4996
|
-
if (totalBytes <= cap) return text;
|
|
4997
|
-
let bytes = 0;
|
|
4998
|
-
let charIdx = text.length;
|
|
4999
|
-
while (charIdx > 0) {
|
|
5000
|
-
const ch = text[charIdx - 1];
|
|
5001
|
-
const chBytes = Buffer.byteLength(ch);
|
|
5002
|
-
if (bytes + chBytes > cap) break;
|
|
5003
|
-
bytes += chBytes;
|
|
5004
|
-
charIdx--;
|
|
5005
|
-
}
|
|
5006
|
-
const tail = text.slice(charIdx);
|
|
5007
|
-
return `…(${totalBytes - Buffer.byteLength(tail)} bytes truncated from head)…\n${tail}`;
|
|
5008
|
-
}
|
|
5009
5397
|
//#endregion
|
|
5010
5398
|
//#region src/tools/spawn.ts
|
|
5011
5399
|
const BUBBLED_EVENTS = [
|
|
@@ -5018,6 +5406,9 @@ const BUBBLED_EVENTS = [
|
|
|
5018
5406
|
"tool:after",
|
|
5019
5407
|
"tool:error",
|
|
5020
5408
|
"tool:cancelled",
|
|
5409
|
+
"background:start",
|
|
5410
|
+
"background:exit",
|
|
5411
|
+
"background:reassign",
|
|
5021
5412
|
"turn:after"
|
|
5022
5413
|
];
|
|
5023
5414
|
const BUBBLED_MUTABLE_EVENTS = [
|
|
@@ -5035,6 +5426,9 @@ const CHILD_EVENT_NAME = {
|
|
|
5035
5426
|
"tool:after": "child:tool:after",
|
|
5036
5427
|
"tool:error": "child:tool:error",
|
|
5037
5428
|
"tool:cancelled": "child:tool:cancelled",
|
|
5429
|
+
"background:start": "child:background:start",
|
|
5430
|
+
"background:exit": "child:background:exit",
|
|
5431
|
+
"background:reassign": "child:background:reassign",
|
|
5038
5432
|
"turn:after": "child:turn:after"
|
|
5039
5433
|
};
|
|
5040
5434
|
const CHILD_MUTABLE_EVENT_NAME = {
|
|
@@ -5349,6 +5743,24 @@ function createSpawnTool(options = {}) {
|
|
|
5349
5743
|
});
|
|
5350
5744
|
}
|
|
5351
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
|
+
}
|
|
5352
5764
|
try {
|
|
5353
5765
|
await agent.destroy();
|
|
5354
5766
|
} catch (err) {
|
|
@@ -5438,6 +5850,6 @@ const writeFile$1 = {
|
|
|
5438
5850
|
}
|
|
5439
5851
|
};
|
|
5440
5852
|
//#endregion
|
|
5441
|
-
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 };
|
|
5442
5854
|
|
|
5443
|
-
//# sourceMappingURL=tools-
|
|
5855
|
+
//# sourceMappingURL=tools-D_icxa-V.js.map
|