agent.libx.js 0.93.29 → 0.93.31

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as AgentOptions, H as Hooks, h as RunResult, A as Agent } from './Agent-kWrJvtZM.js';
2
- export { C as ChatFragment, D as DEFAULT_MUTATING, b as Decision, P as PermissionOptions, c as PermissionPolicy, d as PermissionRule, e as PreToolUseDecision, R as ReasoningEffort, f as RecordingHooks, g as RecordingLifecycle, T as ToolUse, i as ToolUseMeta, j as composeHooks, p as planMode, r as reasoningToChatFragment } from './Agent-kWrJvtZM.js';
1
+ import { a as AgentOptions, H as Hooks, h as RunResult, A as Agent } from './Agent-uWtu_WFY.js';
2
+ export { C as ChatFragment, D as DEFAULT_MUTATING, b as Decision, P as PermissionOptions, c as PermissionPolicy, d as PermissionRule, e as PreToolUseDecision, R as ReasoningEffort, f as RecordingHooks, g as RecordingLifecycle, T as ToolUse, i as ToolUseMeta, j as composeHooks, p as planMode, r as reasoningToChatFragment } from './Agent-uWtu_WFY.js';
3
3
  import { IFilesystem, FileMetadata } from '@livx.cc/wcli/core';
4
4
  export { CommandExecutor, FileMetadata, IFilesystem, IndexedDbFilesystem, MemFilesystem, registerHeadlessCommands } from '@livx.cc/wcli/core';
5
5
  import { BodDB } from '@bod.ee/db';
@@ -314,7 +314,7 @@ declare const grepTool: AgentTool;
314
314
  * Compact map of a VFS — code signatures and/or doc outlines. Edge-safe (pure IFilesystem walk).
315
315
  * `mode`: "code" (default) = top-level signatures; "docs" = heading outlines; "all" = both.
316
316
  */
317
- declare function repoIndex(fs: IFilesystem, glob?: string, mode?: 'code' | 'docs' | 'all'): Promise<string>;
317
+ declare function repoIndex(fs: IFilesystem, glob?: string, mode?: 'code' | 'docs' | 'all', signal?: AbortSignal): Promise<string>;
318
318
  /** Compact map of the codebase or document workspace — orient in ONE call, not many. */
319
319
  declare const repoMapTool: AgentTool;
320
320
  /** Create or overwrite a file, creating parent directories as needed (mkdir -p). */
@@ -418,6 +418,16 @@ declare class Scratch {
418
418
  capture(tool: AgentTool): AgentTool;
419
419
  /** Wrap many tools at once. */
420
420
  captureAll(tools: AgentTool[]): AgentTool[];
421
+ /**
422
+ * Spill an oversized tool result to a scratch file and return PAGE 1 + a recoverable, paginated stub.
423
+ * Drop-in for `Agent.capToolResult`: the agent sees usable content immediately and knows how to get
424
+ * the rest (refine the query, Read the file in pages with offset/limit, or Ask to extract specifics).
425
+ * Lossless — unlike a plain crop, the full output stays available on the scratch FS.
426
+ */
427
+ spill(full: string, info: {
428
+ tool: string;
429
+ args: any;
430
+ }, pageBytes?: number): Promise<string>;
421
431
  }
422
432
  interface AskOptions {
423
433
  /** The scratch filesystem to peek into (dedicated VFS holding only scratch files). */
@@ -683,6 +693,11 @@ declare class DuplexAgentOptions {
683
693
  actModel: string;
684
694
  /** Premium reasoning model. Set to `false` to disable the Think tier entirely. */
685
695
  thinkModel: string | false;
696
+ /** Per-worker providerOptions, derived from the worker's actual model at spawn time (IoC — keeps duplex
697
+ * provider-agnostic). Workers override the reflex/main model, so provider-specific options (e.g. cursor's
698
+ * cwd/cursorSession) must be recomputed for the worker's model, never inherited from the main template —
699
+ * leaking cursor options to an anthropic worker is a hard 400. Returns undefined → no providerOptions. */
700
+ providerOptionsFor?: (model: string) => Record<string, unknown> | undefined;
686
701
  /** Escape hatches merged over the derived per-agent options. */
687
702
  reflexOptions?: Partial<AgentOptions>;
688
703
  actOptions?: Partial<AgentOptions>;
package/dist/index.js CHANGED
@@ -25,7 +25,10 @@ var init_redact = __esm({
25
25
  });
26
26
 
27
27
  // src/tools.structured.ts
28
- async function walkFiles(fs, dir, out = []) {
28
+ function ckAbort(signal) {
29
+ if (signal?.aborted) throw new Error("aborted");
30
+ }
31
+ async function walkFiles(fs, dir, signal, out = []) {
29
32
  let entries;
30
33
  try {
31
34
  entries = await fs.readDir(dir);
@@ -33,8 +36,9 @@ async function walkFiles(fs, dir, out = []) {
33
36
  return out;
34
37
  }
35
38
  for (const name of entries.sort()) {
39
+ ckAbort(signal);
36
40
  const p = dir === "/" ? `/${name}` : `${dir}/${name}`;
37
- if (await fs.isDirectory(p)) await walkFiles(fs, p, out);
41
+ if (await fs.isDirectory(p)) await walkFiles(fs, p, signal, out);
38
42
  else out.push(p);
39
43
  }
40
44
  return out;
@@ -91,13 +95,14 @@ function signaturesOf(content, cap = 40) {
91
95
  }
92
96
  return out;
93
97
  }
94
- async function repoIndex(fs, glob, mode = "code") {
98
+ async function repoIndex(fs, glob, mode = "code", signal) {
95
99
  const scope = glob ? anchoredGlob(fs, String(glob)) : null;
96
100
  const filter = mode === "code" ? isCode : mode === "docs" ? isDoc : (p) => isCode(p) || isDoc(p);
97
- const files = (await walkFiles(fs, fsCwd(fs))).filter((p) => scope ? scope.test(p) : filter(p));
101
+ const files = (await walkFiles(fs, fsCwd(fs), signal)).filter((p) => scope ? scope.test(p) : filter(p));
98
102
  const blocks = [];
99
103
  let shown = 0;
100
104
  for (const path of files) {
105
+ ckAbort(signal);
101
106
  let content;
102
107
  try {
103
108
  content = await fs.readFile(path);
@@ -217,7 +222,7 @@ var init_tools_structured = __esm({
217
222
  const include = pats.filter((p) => !p.startsWith("!")).map((p) => anchoredGlob(ctx.fs, p));
218
223
  const exclude = pats.filter((p) => p.startsWith("!")).map((p) => anchoredGlob(ctx.fs, p.slice(1)));
219
224
  const includes = include.length ? include : [anchoredGlob(ctx.fs, "**")];
220
- const hits = (await walkFiles(ctx.fs, fsCwd(ctx.fs))).filter(
225
+ const hits = (await walkFiles(ctx.fs, fsCwd(ctx.fs), ctx.signal)).filter(
221
226
  (p) => includes.some((re) => re.test(p)) && !exclude.some((re) => re.test(p))
222
227
  );
223
228
  return hits.length ? hits.join("\n") : "(no matches)";
@@ -244,11 +249,12 @@ var init_tools_structured = __esm({
244
249
  throw new Error(`invalid regex: ${String(e)}`);
245
250
  }
246
251
  const scope = glob ? anchoredGlob(ctx.fs, String(glob)) : null;
247
- const files = (await walkFiles(ctx.fs, fsCwd(ctx.fs))).filter((p) => !scope || scope.test(p));
252
+ const files = (await walkFiles(ctx.fs, fsCwd(ctx.fs), ctx.signal)).filter((p) => !scope || scope.test(p));
248
253
  const ctxN = Math.max(0, Number(context ?? 0));
249
254
  const out = [];
250
255
  const matched = [];
251
256
  for (const path of files) {
257
+ ckAbort(ctx.signal);
252
258
  let content;
253
259
  try {
254
260
  content = await ctx.fs.readFile(path);
@@ -284,7 +290,7 @@ var init_tools_structured = __esm({
284
290
  scope: { type: "string", enum: ["code", "docs", "all"], description: 'what to map: "code" (default), "docs", or "all"' }
285
291
  }
286
292
  },
287
- run: ({ glob, scope }, ctx) => repoIndex(ctx.fs, glob, scope || "code")
293
+ run: ({ glob, scope }, ctx) => repoIndex(ctx.fs, glob, scope || "code", ctx.signal)
288
294
  };
289
295
  writeTool = {
290
296
  name: "Write",
@@ -2682,6 +2688,14 @@ var AgentOptions = class {
2682
2688
  /** Token-aware backstop (~4 chars/token estimate). After note-taking, drop oldest messages from the
2683
2689
  * sent context until the estimate is under this ceiling (pairing-safe). 0 = off. */
2684
2690
  maxContextTokens = 0;
2691
+ /** Pagination ceiling for a SINGLE tool result (bytes). A result over this is cropped to page 1 with
2692
+ * a marker telling the model it was cropped (refine the query, or page further). Guards against one
2693
+ * Grep/Read/MCP call blowing the whole context window. 0 = off. Default 60k (~15k tokens). */
2694
+ maxToolResultBytes = 6e4;
2695
+ /** Hook to handle an oversized tool result instead of the default lossy crop: receives the FULL output
2696
+ * and returns the (cropped) string to put in context — e.g. spill to scratch and return a recoverable,
2697
+ * paginated stub. Called only when a result exceeds `maxToolResultBytes`. */
2698
+ capToolResult;
2685
2699
  /** VFS dir(s) of skills (`<dir>/<id>/SKILL.md`). If set: inject a catalog + add the `Skill` tool. Multiple dirs are merged (first wins on name collisions). */
2686
2700
  skillsDir;
2687
2701
  /** VFS dir(s) of slash-command templates (`<dir>/<name>.md`). If set: inject a catalog + add the `SlashCommand` tool. Multiple dirs are merged (first wins). */
@@ -3004,6 +3018,7 @@ var Agent = class _Agent {
3004
3018
  toolCallsTotal += toolCalls.length;
3005
3019
  if (o.maxToolCalls && toolCallsTotal > o.maxToolCalls) return kill("max_tool_calls");
3006
3020
  for (const tc of toolCalls) {
3021
+ if (o.signal?.aborted) return kill("aborted");
3007
3022
  const raw = await this.dispatch(tc);
3008
3023
  let content;
3009
3024
  if (typeof raw === "string") {
@@ -3096,6 +3111,11 @@ var Agent = class _Agent {
3096
3111
  this.ctx.emit = void 0;
3097
3112
  }
3098
3113
  if (!threw) result = await this.maybeAutoTest(tc.function.name, result);
3114
+ const cap = this.options.maxToolResultBytes ?? 0;
3115
+ if (!threw && cap > 0 && result.length > cap) {
3116
+ const info = { tool: tc.function.name, args };
3117
+ result = this.options.capToolResult ? await this.options.capToolResult(result, info) : cropResult(result, cap);
3118
+ }
3099
3119
  await hooks?.postToolUse?.(call, result, meta);
3100
3120
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: result, isError: threw });
3101
3121
  if (images?.length) {
@@ -3149,6 +3169,15 @@ function estimateTokens(m) {
3149
3169
  for (const x of m) chars += contentText(x.content).length + (x.tool_calls ? JSON.stringify(x.tool_calls).length : 0);
3150
3170
  return Math.ceil(chars / 4);
3151
3171
  }
3172
+ function cropResult(result, cap) {
3173
+ const head = result.slice(0, cap);
3174
+ const nl = head.lastIndexOf("\n");
3175
+ const page = nl > cap * 0.5 ? head.slice(0, nl) : head;
3176
+ const omitted = result.length - page.length;
3177
+ return `${page}
3178
+
3179
+ [output cropped \u2014 showing ${page.length} of ${result.length} bytes; ${omitted} omitted. This is page 1. Refine your query/command to narrow it, or call the tool again with a tighter scope to see more.]`;
3180
+ }
3152
3181
  function stubOldToolResults(messages, keep) {
3153
3182
  const meta = /* @__PURE__ */ new Map();
3154
3183
  for (const msg of messages)
@@ -3626,6 +3655,34 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
3626
3655
  captureAll(tools) {
3627
3656
  return tools.map((t) => this.capture(t));
3628
3657
  }
3658
+ /**
3659
+ * Spill an oversized tool result to a scratch file and return PAGE 1 + a recoverable, paginated stub.
3660
+ * Drop-in for `Agent.capToolResult`: the agent sees usable content immediately and knows how to get
3661
+ * the rest (refine the query, Read the file in pages with offset/limit, or Ask to extract specifics).
3662
+ * Lossless — unlike a plain crop, the full output stays available on the scratch FS.
3663
+ */
3664
+ async spill(full, info, pageBytes = 8e3) {
3665
+ const { dir } = this.options;
3666
+ const id = "a" + ++this.seq;
3667
+ const path = `${dir}/${id}-${slug(info.tool)}.txt`;
3668
+ const header = `# ${info.tool}(${shortArgs(info.args)}) \u2014 ${full.length} bytes
3669
+ `;
3670
+ try {
3671
+ await (this.dirReady ??= mkdirp(this.fs, dir));
3672
+ await this.fs.writeFile(path, header + full);
3673
+ } catch (e) {
3674
+ log5.debug("scratch spill failed; cropping lossy", e);
3675
+ return full.slice(0, pageBytes) + `
3676
+
3677
+ [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
3678
+ }
3679
+ const head = full.slice(0, pageBytes);
3680
+ const nl = head.lastIndexOf("\n");
3681
+ const page = nl > pageBytes * 0.5 ? head.slice(0, nl) : head;
3682
+ return `${page}
3683
+
3684
+ [output cropped \u2014 page 1 (${page.length} of ${full.length} bytes). Full output saved to ${path}. To see more: refine your query/command to narrow it, or Read ${path} with offset/limit to page through it, or Ask({ question: "\u2026", over: "${path}" }) to extract specifics.]`;
3685
+ }
3629
3686
  };
3630
3687
  var ASK_PROMPT = "You are a retrieval-extraction step with Read, Grep and Glob over a scratch filesystem holding raw outputs from earlier tools. Find the information that answers the question and return it concisely, quoting values/facts verbatim. Do NOT add analysis or anything not grounded in the files. If the answer is not present, say so plainly.";
3631
3688
  function makeAskTool(o) {
@@ -3759,10 +3816,18 @@ var DuplexAgentOptions = class {
3759
3816
  ai;
3760
3817
  /** The WORKER's filesystem (act + think). If omitted the worker keeps Agent's jailed-disk-at-cwd default. */
3761
3818
  fs;
3762
- reflexModel = "groq/openai/gpt-oss-20b";
3819
+ // The reflex IS the voice. 120b (not 20b) for channel discipline + instruction-following: the 20b
3820
+ // mislabels gpt-oss harmony channels under load, leaking raw analysis into the spoken `final` channel
3821
+ // (and misfiring Hold). 120b is the same price tier (~$0.15/$0.60) — the quality/cost trade is free.
3822
+ reflexModel = "groq/openai/gpt-oss-120b";
3763
3823
  actModel = "anthropic/claude-sonnet-4-6";
3764
3824
  /** Premium reasoning model. Set to `false` to disable the Think tier entirely. */
3765
3825
  thinkModel = "anthropic/claude-opus-4-8";
3826
+ /** Per-worker providerOptions, derived from the worker's actual model at spawn time (IoC — keeps duplex
3827
+ * provider-agnostic). Workers override the reflex/main model, so provider-specific options (e.g. cursor's
3828
+ * cwd/cursorSession) must be recomputed for the worker's model, never inherited from the main template —
3829
+ * leaking cursor options to an anthropic worker is a hard 400. Returns undefined → no providerOptions. */
3830
+ providerOptionsFor;
3766
3831
  /** Escape hatches merged over the derived per-agent options. */
3767
3832
  reflexOptions;
3768
3833
  actOptions;
@@ -3841,7 +3906,12 @@ var DuplexAgent = class {
3841
3906
  const canSearch = workerToolNames.some((n) => /WebSearch/i.test(n));
3842
3907
  const canFetch = workerToolNames.some((n) => /WebFetch/i.test(n));
3843
3908
  const workerWeb = canSearch ? `, and it CAN search the web and read web pages \u2014 so when the user gives you something specific to look up ("search for X", "find me\u2026", "what's the latest on\u2026"), route it to Act. But a bare capability QUESTION like "can you search the web?" just gets a short spoken "yes, I can" \u2014 do NOT dispatch and NEVER invent a query the user did not give you` : canFetch ? ", and it can fetch a specific web page URL (but cannot search the web)" : "";
3844
- const prompt = VOICE_SYSTEM_PROMPT.replace("{{MEMORY_SLOT}}", memSlot).replace("{{THINK_SLOT}}", thinkSlot).replace("{{WORKER_WEB}}", workerWeb) + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + `
3909
+ const mcpNames = [
3910
+ ...Object.keys(o.actOptions?.providerOptions?.mcpServers ?? {}),
3911
+ ...new Set(workerToolNames.filter((n) => n.startsWith("mcp__")).map((n) => n.slice(5).split("__")[0]))
3912
+ ];
3913
+ const workerMcp = mcpNames.length ? `, and it can use these MCP servers: ${[...new Set(mcpNames)].join(", ")}` + (mcpNames.some((n) => /browser/i.test(n)) ? ' \u2014 including driving a REAL browser (open tabs, navigate, click, screenshot), so answer "yes" if asked whether you can control/drive a browser and route an actual browse to Act' : "") : "";
3914
+ const prompt = VOICE_SYSTEM_PROMPT.replace("{{MEMORY_SLOT}}", memSlot).replace("{{THINK_SLOT}}", thinkSlot).replace("{{WORKER_WEB}}", workerWeb + workerMcp) + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + `
3845
3915
  Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
3846
3916
  const tools = [
3847
3917
  ...o.reflexOptions?.tools ?? [],
@@ -4025,6 +4095,9 @@ ${recent}` : brief) + verify;
4025
4095
  model: tierModel,
4026
4096
  ...tier === "think" ? { reasoning: tierOpts?.reasoning ?? "high" } : {},
4027
4097
  ...tierOpts,
4098
+ // Recompute providerOptions for THIS worker's model (after tierOpts so it wins over any inherited
4099
+ // main-template value) — prevents cursor-only cwd/cursorSession leaking onto an anthropic worker.
4100
+ providerOptions: o.providerOptionsFor?.(tierModel),
4028
4101
  ...workerHost ? { host: workerHost } : {},
4029
4102
  ...hooks ? { hooks } : {},
4030
4103
  signal: controller.signal
@@ -4273,8 +4346,10 @@ Another agent just implemented the above. Independently check the CURRENT state
4273
4346
  case "capabilities": {
4274
4347
  const actTools = this.options.actOptions?.tools ?? [];
4275
4348
  const names = actTools.map((t) => t.name);
4349
+ const mcpServers = Object.keys(this.options.actOptions?.providerOptions?.mcpServers ?? {});
4350
+ const mcpNote = mcpServers.length ? ` Plus MCP servers your worker can use: ${mcpServers.join(", ")} (e.g. browser-bridge \u2192 drive a real browser: open tabs, navigate, click, screenshot).` : "";
4276
4351
  if (!names.length)
4277
- return "Your worker uses Act's default local toolset (reading/editing files, running shell commands). No extra tools (e.g. web/internet) are configured; if a request is not a basic file or shell operation, assume you can't do it and say so.";
4352
+ return "Your worker uses Act's default local toolset (reading/editing files, running shell commands). No extra tools (e.g. web/internet) are configured; if a request is not a basic file or shell operation, assume you can't do it and say so." + mcpNote;
4278
4353
  const hasFetch = names.some((n) => /WebFetch/i.test(n));
4279
4354
  const hasBrowser = names.some((n) => /browser.*(navigate|click|page|type)/i.test(n));
4280
4355
  const hasSearch = names.some((n) => /(^|_)WebSearch$|search/i.test(n) && !/WebFetch|browser/i.test(n));
@@ -4283,7 +4358,7 @@ Another agent just implemented the above. Independently check the CURRENT state
4283
4358
  if (hasBrowser) notes.push("The browser tools drive a real browser: you CAN open a site and, if needed, navigate to a search engine and search there \u2014 but it is manual and takes a moment, not an instant lookup.");
4284
4359
  else if (!hasSearch && hasFetch) notes.push('You have no general web-search tool, so for an instant "search the web" you can only fetch a URL they provide.');
4285
4360
  const webNote = notes.length ? " NOTE: " + notes.join(" ") : "";
4286
- return `Tools your background worker (Act) can actually use: ${names.join(", ")}. Read each name literally and match the request to a SPECIFIC tool; if none fits, you do NOT have that ability \u2014 say so honestly.` + webNote;
4361
+ return `Tools your background worker (Act) can actually use: ${names.join(", ")}. Read each name literally and match the request to a SPECIFIC tool; if none fits, you do NOT have that ability \u2014 say so honestly.` + webNote + mcpNote;
4287
4362
  }
4288
4363
  case "time":
4289
4364
  return (/* @__PURE__ */ new Date()).toString();