@dudko.dev/agent-web 0.0.19 → 0.0.20

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.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ FILES_STORE,
2
3
  IndexedDBVault,
3
4
  KEYS_STORE,
4
5
  SECRETS_STORE,
@@ -7,7 +8,7 @@ import {
7
8
  encryptJSON,
8
9
  getOrCreateVaultKey,
9
10
  openAgentWebDB
10
- } from "./chunk-H7W6MBK2.js";
11
+ } from "./chunk-V7COB2EA.js";
11
12
  import {
12
13
  createWebLLMModel,
13
14
  isWebGPUAvailable,
@@ -15,29 +16,48 @@ import {
15
16
  unloadWebLLMModel
16
17
  } from "./chunk-IT65EW77.js";
17
18
 
18
- // src/config.ts
19
- var resolveConfig = (c) => ({
20
- clientName: c.clientName ?? "agent-web",
21
- sessionId: c.sessionId ?? "default",
22
- toolMode: c.toolMode ?? "auto",
23
- toolSelectionStrategy: c.toolSelectionStrategy ?? "all",
24
- maxIterations: c.maxIterations ?? 8,
25
- maxStepsPerTask: c.maxStepsPerTask ?? 4,
26
- maxRevisions: c.maxRevisions ?? 2,
27
- chatTimeoutMs: c.chatTimeoutMs ?? 12e4,
28
- budgets: {
29
- planner: c.budgets?.planner ?? 800,
30
- executor: c.budgets?.executor ?? 1200,
31
- replanner: c.budgets?.replanner ?? 400,
32
- synthesizer: c.budgets?.synthesizer ?? 400
33
- },
34
- temperature: c.temperature,
35
- replan: c.replan ?? true,
36
- replanAfter: c.replanAfter ?? "failure",
37
- synthesize: c.synthesize ?? true,
38
- compressAfterChars: c.compressAfterChars ?? 12e3,
39
- logLevel: c.logLevel ?? "warn"
19
+ // src/caching.ts
20
+ var resolveCaching = (s) => {
21
+ if (s === false) return { enabled: false };
22
+ if (s === void 0 || s === true) return { enabled: true };
23
+ return { enabled: true, ttl: s.ttl, key: s.key };
24
+ };
25
+ var cachedInstructions = (system, caching) => caching.enabled ? {
26
+ role: "system",
27
+ content: system,
28
+ providerOptions: {
29
+ anthropic: {
30
+ cacheControl: { type: "ephemeral", ...caching.ttl ? { ttl: caching.ttl } : {} }
31
+ }
32
+ }
33
+ } : system;
34
+ var cachingProviderOptions = (caching, clientName, stage) => caching.enabled ? { openai: { promptCacheKey: caching.key ?? `${clientName}:${stage}` } } : void 0;
35
+ var breakpoint = (ttl) => ({
36
+ anthropic: { cacheControl: { type: "ephemeral", ...ttl ? { ttl } : {} } }
40
37
  });
38
+ var withoutBreakpoint = (m) => {
39
+ const own = m.providerOptions;
40
+ if (!own?.anthropic || !("cacheControl" in own.anthropic)) return m;
41
+ const { cacheControl: _drop, ...anthropic } = own.anthropic;
42
+ const { anthropic: _old, ...rest } = own;
43
+ const providerOptions = Object.keys(anthropic).length ? { ...rest, anthropic } : rest;
44
+ const { providerOptions: _po, ...base } = m;
45
+ return Object.keys(providerOptions).length ? { ...base, providerOptions } : base;
46
+ };
47
+ var withRollingBreakpoint = (messages, caching) => {
48
+ if (!caching.enabled || messages.length === 0) return messages;
49
+ const out = messages.map((m, i) => i < messages.length - 1 ? withoutBreakpoint(m) : m);
50
+ const last = out[out.length - 1];
51
+ const own = last.providerOptions ?? {};
52
+ out[out.length - 1] = {
53
+ ...last,
54
+ providerOptions: {
55
+ ...own,
56
+ anthropic: { ...own.anthropic, ...breakpoint(caching.ttl).anthropic }
57
+ }
58
+ };
59
+ return out;
60
+ };
41
61
 
42
62
  // src/llm/generate.ts
43
63
  import {
@@ -55,7 +75,10 @@ var normalizeUsage = (u) => {
55
75
  return {
56
76
  inputTokens,
57
77
  outputTokens,
58
- totalTokens: u?.totalTokens ?? inputTokens + outputTokens
78
+ totalTokens: u?.totalTokens ?? inputTokens + outputTokens,
79
+ reasoningTokens: u?.outputTokenDetails?.reasoningTokens ?? u?.reasoningTokens ?? 0,
80
+ cachedInputTokens: u?.inputTokenDetails?.cacheReadTokens ?? u?.cachedInputTokens ?? 0,
81
+ cacheWriteTokens: u?.inputTokenDetails?.cacheWriteTokens ?? 0
59
82
  };
60
83
  };
61
84
  var timeoutSignal = (signal, ms) => {
@@ -66,53 +89,78 @@ var timeoutSignal = (signal, ms) => {
66
89
  };
67
90
 
68
91
  // src/llm/generate.ts
69
- var generate = (model, opts = {}) => generateText({
70
- model,
71
- system: opts.system,
72
- ...promptOf(opts),
73
- tools: opts.tools,
92
+ var callSettings = (opts) => ({
93
+ ...opts.system !== void 0 ? { instructions: opts.system } : {},
74
94
  maxOutputTokens: opts.maxOutputTokens,
75
95
  temperature: opts.temperature,
96
+ ...opts.reasoning ? { reasoning: opts.reasoning } : {},
97
+ ...opts.providerOptions ? { providerOptions: opts.providerOptions } : {},
76
98
  abortSignal: timeoutSignal(opts.abortSignal, opts.timeoutMs)
77
99
  });
100
+ var generate = (model, opts = {}) => generateText({
101
+ model,
102
+ ...callSettings(opts),
103
+ ...promptOf(opts),
104
+ tools: opts.tools
105
+ });
78
106
  var stream = (model, opts = {}) => streamText({
79
107
  model,
80
- system: opts.system,
108
+ ...callSettings(opts),
81
109
  ...promptOf(opts),
82
- tools: opts.tools,
83
- maxOutputTokens: opts.maxOutputTokens,
84
- temperature: opts.temperature,
85
- abortSignal: timeoutSignal(opts.abortSignal, opts.timeoutMs)
110
+ tools: opts.tools
86
111
  });
87
112
  var generateStructured = (model, schema, opts = {}) => generateObject({
88
113
  model,
89
114
  schema,
90
- system: opts.system,
91
- ...promptOf(opts),
92
- maxOutputTokens: opts.maxOutputTokens,
93
- temperature: opts.temperature,
94
- abortSignal: timeoutSignal(opts.abortSignal, opts.timeoutMs)
115
+ ...callSettings(opts),
116
+ ...promptOf(opts)
95
117
  });
96
118
 
97
119
  // src/memory/compress.ts
98
- var totalChars = (msgs) => msgs.reduce((n, m) => n + m.content.length, 0);
120
+ var resolveCompaction = (c) => {
121
+ const contextWindowTokens = c?.contextWindowTokens ?? 128e3;
122
+ return {
123
+ auto: c?.auto ?? true,
124
+ contextWindowTokens,
125
+ thresholdTokens: c?.thresholdTokens ?? Math.floor(contextWindowTokens / 2),
126
+ keepRecentTurns: c?.keepRecentTurns ?? 4,
127
+ keepRecentSteps: c?.keepRecentSteps ?? 3,
128
+ summaryMaxTokens: c?.summaryMaxTokens ?? 1024,
129
+ maxToolOutputChars: c?.maxToolOutputChars ?? 2e4,
130
+ clearToolResultsAfterTokens: c?.clearToolResultsAfterTokens ?? Math.floor(contextWindowTokens / 4),
131
+ keepToolResults: c?.keepToolResults ?? 3
132
+ };
133
+ };
134
+ var estimateTokens = (text) => Math.ceil((text ?? "").length / 4);
135
+ var messageTokens = (msgs) => msgs.reduce((n, m) => n + estimateTokens(`${m.role}: ${m.content}
136
+ `), 0);
137
+ var SUMMARY_SYSTEM = "Summarize the following conversation into a compact set of durable facts, decisions, identifiers, open tasks and user preferences. Keep names, numbers and IDs verbatim. Output plain text only.";
99
138
  var compressHistory = async (messages, model, opts = {}) => {
100
- const maxChars = opts.maxChars ?? 12e3;
101
139
  const keepRecent = opts.keepRecent ?? 6;
102
- if (maxChars <= 0 || messages.length <= keepRecent + 1 || totalChars(messages) <= maxChars) {
103
- return messages;
140
+ if (messages.length <= keepRecent + 1) return messages;
141
+ if (!opts.force) {
142
+ if (opts.thresholdTokens !== void 0) {
143
+ if (opts.thresholdTokens <= 0 || messageTokens(messages) <= opts.thresholdTokens) {
144
+ return messages;
145
+ }
146
+ } else {
147
+ const maxChars = opts.maxChars ?? 12e3;
148
+ const chars = messages.reduce((n, m) => n + m.content.length, 0);
149
+ if (maxChars <= 0 || chars <= maxChars) return messages;
150
+ }
104
151
  }
105
152
  const head = messages.slice(0, messages.length - keepRecent);
106
153
  const tail = messages.slice(messages.length - keepRecent);
107
- const transcript = head.map((m) => `${m.role}: ${m.content}`).join("\n").slice(0, 8e3);
154
+ const transcript = head.map((m) => `${m.role}: ${m.content}`).join("\n").slice(-48e3);
108
155
  try {
109
156
  const result = await generate(model, {
110
- system: "Summarize the following conversation into a compact set of durable facts and decisions. Output plain text only.",
157
+ system: SUMMARY_SYSTEM,
111
158
  prompt: transcript,
112
- maxOutputTokens: 400,
159
+ maxOutputTokens: opts.summaryMaxTokens ?? 400,
113
160
  timeoutMs: opts.timeoutMs,
114
161
  abortSignal: opts.abortSignal
115
162
  });
163
+ opts.onUsage?.(normalizeUsage(result.usage));
116
164
  const clean = (result.text || "").trim();
117
165
  if (!clean) return messages;
118
166
  const summaryMsg = {
@@ -126,6 +174,93 @@ ${clean}`,
126
174
  return messages;
127
175
  }
128
176
  };
177
+ var compactHistory = compressHistory;
178
+ var TRACE_SYSTEM = "Summarize these completed agent steps into a compact progress note: what was done, what was found (keep names, numbers and IDs verbatim) and what failed. Plain text only.";
179
+ var compactSteps = async (done, model, opts) => {
180
+ const keepRecent = opts.keepRecent ?? 3;
181
+ if (done.length <= keepRecent + 1) return done;
182
+ if (!opts.force && estimateTokens(done.join("\n")) <= opts.thresholdTokens) return done;
183
+ const head = done.slice(0, done.length - keepRecent);
184
+ try {
185
+ const result = await generate(model, {
186
+ system: TRACE_SYSTEM,
187
+ prompt: head.map((d, i) => `${i + 1}. ${d}`).join("\n"),
188
+ maxOutputTokens: opts.summaryMaxTokens ?? 600,
189
+ timeoutMs: opts.timeoutMs,
190
+ abortSignal: opts.abortSignal
191
+ });
192
+ opts.onUsage?.(normalizeUsage(result.usage));
193
+ const clean = (result.text || "").trim();
194
+ if (!clean) return done;
195
+ return [`Summary of ${head.length} earlier step(s): ${clean}`, ...done.slice(head.length)];
196
+ } catch {
197
+ return done;
198
+ }
199
+ };
200
+
201
+ // src/config.ts
202
+ var resolveConfig = (c) => ({
203
+ clientName: c.clientName ?? "agent-web",
204
+ sessionId: c.sessionId ?? "default",
205
+ toolMode: c.toolMode ?? "auto",
206
+ toolSelectionStrategy: c.toolSelectionStrategy ?? "auto",
207
+ toolSearchThreshold: c.toolSearchThreshold ?? 40,
208
+ maxIterations: c.maxIterations ?? 8,
209
+ maxStepsPerTask: c.maxStepsPerTask ?? 4,
210
+ maxRevisions: c.maxRevisions ?? 2,
211
+ maxToolCalls: c.maxToolCalls ?? 0,
212
+ maxPlanSteps: c.maxPlanSteps ?? 8,
213
+ chatTimeoutMs: c.chatTimeoutMs ?? 12e4,
214
+ budgets: {
215
+ planner: c.limits?.perCall?.planner ?? c.budgets?.planner ?? 800,
216
+ executor: c.limits?.perCall?.executor ?? c.budgets?.executor ?? 1200,
217
+ replanner: c.limits?.perCall?.replanner ?? c.budgets?.replanner ?? 400,
218
+ synthesizer: c.limits?.perCall?.synthesizer ?? c.budgets?.synthesizer ?? 400,
219
+ compaction: c.limits?.perCall?.compaction ?? c.compaction?.summaryMaxTokens ?? 1024
220
+ },
221
+ limits: c.limits ?? {},
222
+ temperature: c.temperature,
223
+ replan: c.replan ?? true,
224
+ replanAfter: c.replanAfter ?? "failure",
225
+ synthesize: c.synthesize ?? true,
226
+ compressAfterChars: c.compressAfterChars ?? 12e3,
227
+ compaction: resolveCompaction(c.compaction),
228
+ logLevel: c.logLevel ?? "warn"
229
+ });
230
+
231
+ // src/limits.ts
232
+ var over = (value, cap) => typeof cap === "number" && cap > 0 && (value ?? 0) >= cap;
233
+ var checkLimits = (usage, limits) => {
234
+ if (!limits) return void 0;
235
+ if (over(usage.totalTokens, limits.maxTotalTokens)) {
236
+ return { kind: "total", tokens: usage.totalTokens, cap: limits.maxTotalTokens };
237
+ }
238
+ if (over(usage.inputTokens, limits.maxInputTokens)) {
239
+ return { kind: "input", tokens: usage.inputTokens, cap: limits.maxInputTokens };
240
+ }
241
+ if (over(usage.outputTokens, limits.maxOutputTokens)) {
242
+ return { kind: "output", tokens: usage.outputTokens, cap: limits.maxOutputTokens };
243
+ }
244
+ if (over(usage.reasoningTokens, limits.maxReasoningTokens)) {
245
+ return {
246
+ kind: "reasoning",
247
+ tokens: usage.reasoningTokens ?? 0,
248
+ cap: limits.maxReasoningTokens
249
+ };
250
+ }
251
+ return void 0;
252
+ };
253
+ var ToolBudgetError = class extends Error {
254
+ constructor(cap) {
255
+ super(
256
+ `Tool-call budget exhausted (${cap} per run). Do not call more tools; report what you have.`
257
+ );
258
+ this.name = "ToolBudgetError";
259
+ }
260
+ toJSON() {
261
+ return this.message;
262
+ }
263
+ };
129
264
 
130
265
  // src/providers/types.ts
131
266
  var isDirectModel = (m) => typeof m === "string" || typeof m === "object" && m !== null && "specificationVersion" in m;
@@ -328,54 +463,77 @@ var historyBlock = (history) => {
328
463
  CONVERSATION SO FAR:
329
464
  ${lines.join("\n")}`;
330
465
  };
331
- var PLANNER_NATIVE = `You are the PLANNER of a tool-using agent that changes a workspace step by step.
332
- Produce a brief "thought" and an ordered "steps" list (1\u20136 DISTINCT, self-contained steps) grounded in the current STATE and the available TOOLS.
333
- If the user only greets, thanks, makes small talk, asks a question, or is unclear: return an EMPTY steps list and put a short, friendly answer in "thought".
334
- If a CONVERSATION SO FAR section is present, use it to resolve references to earlier turns.
335
- Never repeat or pad steps.`;
336
- var PLANNER_PROMPTED = `You are the PLANNER of a tool-using agent that changes a workspace step by step.
466
+ var section = (title, body) => body && body.trim() ? `
337
467
 
338
- Reply with a single JSON object, nothing else:
339
- { "reply": string, "plan": string[] }
340
- - Only produce a "plan" when the user CLEARLY asks to build, do, or change something. For a greeting, small talk, thanks, a question, or an unclear/empty request: set "plan": [] and put a short, friendly "reply".
341
- - For a real goal: "reply" is one short sentence; "plan" is 1\u20136 DISTINCT, self-contained steps. Never repeat or pad steps.
468
+ ${title}:
469
+ ${body.trim()}` : "";
470
+ var PLANNER_RULES = `- Act autonomously: whenever the TOOLS can do the work or look up the answer, plan the steps \u2014 a question that needs data, a lookup or a check is a real goal too.
471
+ - Never plan a step that asks the user something. If the request is ambiguous, pick the most reasonable interpretation and say which one you chose.
342
472
  - Plan realistic steps the available TOOLS can perform, grounded in the current STATE.
343
- - If a CONVERSATION SO FAR section is present, use it to resolve references to earlier turns.`;
344
- var EXECUTOR_NATIVE = `You are the EXECUTOR of a tool-using agent. Carry out ONLY the current step by calling the provided tools.
345
- Build on the current STATE \u2014 do not repeat work that is already there.
346
- When the step is done, reply with one short human sentence describing what you did (no JSON).
347
- If you CANNOT complete the step (a needed tool is missing or an input is unavailable), explain why in one sentence and include the token [BLOCKER].`;
348
- var EXECUTOR_PROMPTED = `You are the EXECUTOR of a tool-using agent. Carry out ONLY the current step by emitting tool calls.
473
+ - If a CONVERSATION SO FAR section is present, use it to resolve references to earlier turns.
474
+ - Never repeat or pad steps.`;
475
+ var PLANNER_NATIVE = `You are the PLANNER of an autonomous tool-using agent that works step by step.
476
+ Produce a brief "thought" and an ordered "steps" list (1\u20136 DISTINCT, self-contained steps).
477
+ Return an EMPTY steps list ONLY for a pure greeting, thanks or small talk, or a question you can answer completely from the STATE and general knowledge without any tool \u2014 then put the answer itself in "thought".
478
+ ${PLANNER_RULES}`;
479
+ var PLANNER_PROMPTED = `You are the PLANNER of an autonomous tool-using agent that works step by step.
480
+
481
+ Reply with a single JSON object, nothing else:
482
+ { "reply": string, "plan": string[], "skills": string[] }
483
+ - For a real goal: "reply" is one short sentence; "plan" is 1\u20136 DISTINCT, self-contained steps.
484
+ - Set "plan": [] ONLY for a pure greeting, thanks or small talk, or a question you can answer completely from the STATE and general knowledge without any tool \u2014 then put the answer itself in "reply".
485
+ ${PLANNER_RULES}`;
486
+ var PLANNER_SKILLS = `
487
+ If a SKILLS list is present, list the names of the skills that apply to this goal in "skills" (or leave it empty).`;
488
+ var PLANNER_SEARCH = `
489
+ The TOOLS list may be abbreviated: the executor can search the full catalogue, so describe WHAT to do; name tools only when you see them listed.`;
490
+ var EXECUTOR_RULES = `- Work autonomously: never ask the user questions or for confirmation. Look things up with the tools, choose sensible defaults, and state any assumption in your reply. Permission to run tools is handled by the system, not by you.
491
+ - Build on the current STATE and on the results of earlier steps \u2014 do not repeat work that is already done.`;
492
+ var EXECUTOR_NATIVE = `You are the EXECUTOR of an autonomous tool-using agent. Carry out ONLY the current step by calling the provided tools.
493
+ ${EXECUTOR_RULES}
494
+ - When the step is done, reply with a short factual summary of what you did and found, including the concrete data (names, ids, numbers, values) later steps or the final answer need. No JSON.
495
+ - If you truly CANNOT complete the step (a needed tool is missing or was denied, credentials or data are unavailable), explain why in one sentence and include the token [BLOCKER].`;
496
+ var EXECUTOR_PROMPTED = `You are the EXECUTOR of an autonomous tool-using agent. Carry out ONLY the current step by emitting tool calls.
349
497
 
350
498
  Reply with a single JSON object, nothing else:
351
499
  { "reply": string, "actions": [ { "tool": string, "args": object } ] }
352
500
  - "actions" are the tool calls for THIS step ([] if none are needed). Use ONLY tools from the TOOLS list; "args" must match the tool's parameters.
353
- - Build on the current STATE \u2014 do not repeat work that is already there.
354
- - "reply" is one short human sentence (no JSON) describing what you did.
501
+ ${EXECUTOR_RULES}
502
+ - "reply" is a short factual summary (no JSON) of what you did and found, with the concrete data later steps need.
355
503
  - After your actions run you will see their TOOL RESULTS and may continue the same step; finish with "actions": [] once it is done.
356
- - If you CANNOT complete the step, set "actions": [] and put the token [BLOCKER] in "reply" with a short reason.`;
357
- var REPLANNER_NATIVE = `You are the REPLANNER. After an executed step, decide whether to keep going, revise the remaining steps, or finish.
504
+ - If you truly CANNOT complete the step, set "actions": [] and put the token [BLOCKER] in "reply" with a short reason.`;
505
+ var EXECUTOR_SEARCH = `
506
+ - If a tool you need is not in your current tool list, call find_tools with keywords; the tools it returns become callable right after.`;
507
+ var EXECUTOR_SKILLS = `
508
+ - If a skill from the SKILLS list covers this step, call load_skill to read it first (read_skill_file for its bundled files).`;
509
+ var REPLANNER_RULES = `Judge from the current STATE and the progress vs the goal. Prefer "continue".
510
+ After a failure, prefer revising around it (another tool, another approach) over giving up; never add a step that asks the user something.`;
511
+ var REPLANNER_NATIVE = `You are the REPLANNER of an autonomous agent. After an executed step, decide whether to keep going, revise the remaining steps, or finish.
358
512
  "continue": the remaining steps still fit. "revise": provide a better "plan" for the REMAINING work (never repeat done work). "finish": the goal is already met.
359
- Judge from the current STATE vs the goal. Prefer "continue".`;
360
- var REPLANNER_PROMPTED = `You are the REPLANNER. After each executed step you decide whether to keep going, revise the remaining steps, or finish.
513
+ ${REPLANNER_RULES}`;
514
+ var REPLANNER_PROMPTED = `You are the REPLANNER of an autonomous agent. After each executed step you decide whether to keep going, revise the remaining steps, or finish.
361
515
 
362
516
  Reply with a single JSON object, nothing else:
363
517
  { "decision": "continue" | "revise" | "finish", "reason": string, "plan": string[] }
364
518
  - "continue": remaining steps still fit \u2014 proceed (omit "plan").
365
519
  - "revise": replace the REMAINING steps with a better list in "plan"; never repeat done work.
366
520
  - "finish": the goal is already met \u2014 stop (omit "plan").
367
- Judge from the current STATE vs the goal. Prefer "continue".`;
368
- var SYNTHESIZER_SYSTEM = `You are the SYNTHESIZER. In 1\u20133 short, friendly sentences, tell the user what was done to meet their goal. Be concrete. Output plain text only \u2014 no JSON, no code.`;
521
+ ${REPLANNER_RULES}`;
522
+ var SYNTHESIZER_SYSTEM = `You are the SYNTHESIZER. Answer the user's goal from what the agent did and found.
523
+ - Lead with the result: the concrete data, from the FINDINGS verbatim when accuracy matters (names, numbers, ids), then \u2014 briefly \u2014 what was changed.
524
+ - Mention any assumption the agent made. If something could not be done, say so plainly and why.
525
+ - Do not ask follow-up questions unless the goal genuinely cannot be completed without the user.
526
+ - Be concise and friendly. Plain text (light markdown is fine) \u2014 no JSON, no code.`;
369
527
  var defaultPrompts = {
370
528
  planner: (ctx) => ({
371
- system: ctx.mode === "prompted" ? PLANNER_PROMPTED : PLANNER_NATIVE,
372
- prompt: `GOAL: ${ctx.goal}${historyBlock(ctx.history)}${stateBlock(ctx.state)}
529
+ system: (ctx.mode === "prompted" ? PLANNER_PROMPTED : PLANNER_NATIVE) + (ctx.skills ? PLANNER_SKILLS : "") + (ctx.searchMode ? PLANNER_SEARCH : "") + section("SKILLS", ctx.skills) + `
373
530
 
374
531
  TOOLS:
375
- ${ctx.toolCatalog}`
532
+ ${ctx.toolCatalog}`,
533
+ prompt: `GOAL: ${ctx.goal}${historyBlock(ctx.history)}${stateBlock(ctx.state)}`
376
534
  }),
377
535
  executor: (ctx) => ({
378
- system: ctx.mode === "prompted" ? EXECUTOR_PROMPTED : EXECUTOR_NATIVE,
536
+ system: (ctx.mode === "prompted" ? EXECUTOR_PROMPTED : EXECUTOR_NATIVE) + (ctx.searchMode ? EXECUTOR_SEARCH : "") + (ctx.skills ? EXECUTOR_SKILLS : "") + section("SKILLS", ctx.skills) + section("ACTIVE SKILL INSTRUCTIONS", ctx.activeSkills),
379
537
  prompt: [
380
538
  `GOAL: ${ctx.goal}`,
381
539
  stateBlock(ctx.state).trimStart(),
@@ -387,7 +545,7 @@ ${numbered(ctx.done, "")}` : "",
387
545
  ].filter(Boolean).join("\n\n")
388
546
  }),
389
547
  replanner: (ctx) => ({
390
- system: ctx.mode === "prompted" ? REPLANNER_PROMPTED : REPLANNER_NATIVE,
548
+ system: (ctx.mode === "prompted" ? REPLANNER_PROMPTED : REPLANNER_NATIVE) + section("ACTIVE SKILL INSTRUCTIONS", ctx.activeSkills),
391
549
  prompt: [
392
550
  `GOAL: ${ctx.goal}`,
393
551
  stateBlock(ctx.state).trimStart(),
@@ -399,13 +557,15 @@ ${numbered(ctx.remaining, "(none)")}`,
399
557
  ].filter(Boolean).join("\n\n")
400
558
  }),
401
559
  synthesizer: (ctx) => ({
402
- system: SYNTHESIZER_SYSTEM,
560
+ system: SYNTHESIZER_SYSTEM + section("ACTIVE SKILL INSTRUCTIONS", ctx.activeSkills),
403
561
  prompt: [
404
562
  `GOAL: ${ctx.goal}`,
405
563
  stateBlock(ctx.state).trimStart(),
406
564
  `What was done:
407
565
  ${numbered(ctx.done, "(no changes)")}`,
408
- "Write the final summary for the user."
566
+ ctx.findings?.length ? `FINDINGS (tool results):
567
+ ${ctx.findings.join("\n")}` : "",
568
+ "Write the final answer for the user."
409
569
  ].filter(Boolean).join("\n\n")
410
570
  })
411
571
  };
@@ -413,6 +573,294 @@ var withSystem = (base, extra) => extra && extra.trim() ? `${extra.trim()}
413
573
 
414
574
  ${base}` : base;
415
575
 
576
+ // src/skills.ts
577
+ import { jsonSchema, tool } from "ai";
578
+ var SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
579
+ var SKILL_TOOL_NAMES = ["load_skill", "read_skill_file"];
580
+ var defineSkill = (skill) => {
581
+ if (!skill || typeof skill !== "object") throw new Error("skill must be an object");
582
+ if (typeof skill.name !== "string" || !SKILL_NAME_RE.test(skill.name)) {
583
+ throw new Error(
584
+ `skill name ${JSON.stringify(skill.name)} is invalid: use 1-64 lowercase letters, digits and dashes`
585
+ );
586
+ }
587
+ if (typeof skill.description !== "string" || !skill.description.trim()) {
588
+ throw new Error(`skill "${skill.name}" needs a description`);
589
+ }
590
+ if (typeof skill.content !== "string") {
591
+ throw new Error(`skill "${skill.name}" needs a content string`);
592
+ }
593
+ for (const f of skill.files ?? []) {
594
+ if (typeof f?.path !== "string" || !f.path || f.path.startsWith("/") || f.path.includes("..")) {
595
+ throw new Error(`skill "${skill.name}": invalid file path ${JSON.stringify(f?.path)}`);
596
+ }
597
+ }
598
+ return skill;
599
+ };
600
+ var unquote = (v) => {
601
+ const t = v.trim();
602
+ if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) {
603
+ try {
604
+ return JSON.parse(t);
605
+ } catch {
606
+ return t.slice(1, -1);
607
+ }
608
+ }
609
+ if (t.length >= 2 && t.startsWith("'") && t.endsWith("'")) {
610
+ return t.slice(1, -1).replace(/''/g, "'");
611
+ }
612
+ return t;
613
+ };
614
+ var parseFrontmatter = (block) => {
615
+ const out = {};
616
+ const lines = block.split(/\r?\n/);
617
+ for (let i = 0; i < lines.length; i += 1) {
618
+ const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(lines[i]);
619
+ if (!m) continue;
620
+ const [, key, rest] = m;
621
+ if (rest === ">" || rest === "|" || rest === ">-" || rest === "|-") {
622
+ const body = [];
623
+ while (i + 1 < lines.length && (/^\s+\S/.test(lines[i + 1]) || lines[i + 1] === "")) {
624
+ body.push(lines[i + 1].trim());
625
+ i += 1;
626
+ }
627
+ out[key] = rest.startsWith(">") ? body.filter(Boolean).join(" ") : body.join("\n").trim();
628
+ continue;
629
+ }
630
+ out[key] = unquote(rest);
631
+ }
632
+ return out;
633
+ };
634
+ var parseSkillMarkdown = (markdown, files) => {
635
+ const text = (markdown ?? "").replace(/^/, "");
636
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
637
+ if (!m) throw new Error("SKILL.md must start with a --- frontmatter block (name, description)");
638
+ const meta = parseFrontmatter(m[1]);
639
+ return defineSkill({
640
+ name: meta.name ?? "",
641
+ description: meta.description ?? "",
642
+ content: m[2].trim(),
643
+ ...files?.length ? { files } : {}
644
+ });
645
+ };
646
+ var loadSkillFromUrl = async (url, opts = {}) => {
647
+ const fetchFn = opts.fetch ?? globalThis.fetch;
648
+ const res = await fetchFn(url);
649
+ if (!res.ok) throw new Error(`could not fetch ${url}: HTTP ${res.status}`);
650
+ const markdown = await res.text();
651
+ const files = [];
652
+ for (const path of opts.files ?? []) {
653
+ const r = await fetchFn(new URL(path, url).toString());
654
+ if (!r.ok) throw new Error(`could not fetch skill file ${path}: HTTP ${r.status}`);
655
+ files.push({ path, content: await r.text() });
656
+ }
657
+ return parseSkillMarkdown(markdown, files);
658
+ };
659
+ var renderSkillIndex = (skills) => skills.map((s) => `- ${s.name}: ${s.description.replace(/\s+/g, " ").trim()}`).join("\n");
660
+ var ACTIVE_SKILLS_BUDGET = 24e3;
661
+ var renderActiveSkills = (skills) => {
662
+ let budget = ACTIVE_SKILLS_BUDGET;
663
+ const parts = [];
664
+ for (const s of skills) {
665
+ if (budget <= 0) break;
666
+ const files = s.files?.length ? `
667
+ (bundled files, read with read_skill_file: ${s.files.map((f) => f.path).join(", ")})` : "";
668
+ const block = clip(`### Skill: ${s.name}
669
+ ${s.content}${files}`, budget);
670
+ budget -= block.length;
671
+ parts.push(block);
672
+ }
673
+ return parts.join("\n\n");
674
+ };
675
+ var createSkillTools = (skills, onLoad) => {
676
+ const byName = new Map(skills.map((s) => [s.name, s]));
677
+ const known = skills.map((s) => s.name).join(", ");
678
+ const loadSkill = tool({
679
+ description: "Load the full instructions of a skill from the SKILLS list before doing work it covers.",
680
+ inputSchema: jsonSchema({
681
+ type: "object",
682
+ properties: { name: { type: "string", description: "The skill name" } },
683
+ required: ["name"],
684
+ additionalProperties: false
685
+ }),
686
+ execute: async ({ name }) => {
687
+ const skill = byName.get(name);
688
+ if (!skill) throw new Error(`unknown skill "${name}"; available: ${known}`);
689
+ onLoad?.(skill);
690
+ return {
691
+ name: skill.name,
692
+ content: skill.content,
693
+ files: (skill.files ?? []).map((f) => f.path)
694
+ };
695
+ }
696
+ });
697
+ const readFile = tool({
698
+ description: "Read one of a skill's bundled files (paths are listed by load_skill).",
699
+ inputSchema: jsonSchema({
700
+ type: "object",
701
+ properties: {
702
+ name: { type: "string", description: "The skill name" },
703
+ path: { type: "string", description: "The file path, relative to the skill" }
704
+ },
705
+ required: ["name", "path"],
706
+ additionalProperties: false
707
+ }),
708
+ execute: async ({ name, path }) => {
709
+ const skill = byName.get(name);
710
+ if (!skill) throw new Error(`unknown skill "${name}"; available: ${known}`);
711
+ const file = skill.files?.find((f) => f.path === path);
712
+ if (!file) {
713
+ const listed = (skill.files ?? []).map((f) => f.path).join(", ") || "none";
714
+ throw new Error(`skill "${name}" has no file "${path}"; files: ${listed}`);
715
+ }
716
+ return file.content;
717
+ }
718
+ });
719
+ for (const t of [loadSkill, readFile]) {
720
+ ;
721
+ t.readOnly = true;
722
+ t.promptHint = t === loadSkill ? "{ name: string }" : "{ name: string, path: string }";
723
+ }
724
+ return { load_skill: loadSkill, read_skill_file: readFile };
725
+ };
726
+
727
+ // src/tools/approval.ts
728
+ var TOOL_APPROVAL_MODES = [
729
+ "autopilot",
730
+ "ask-writes",
731
+ "ask-all",
732
+ "read-only"
733
+ ];
734
+ var ToolDeniedError = class extends Error {
735
+ constructor(reason) {
736
+ super(
737
+ `Tool call denied by the user${reason ? `: ${reason}` : ""}. Do not retry it; continue without it, or report what is blocked.`
738
+ );
739
+ this.name = "ToolDeniedError";
740
+ }
741
+ toJSON() {
742
+ return this.message;
743
+ }
744
+ };
745
+ var markReadOnly = (tool6, readOnly = true) => {
746
+ ;
747
+ tool6.readOnly = readOnly;
748
+ return tool6;
749
+ };
750
+ var isReadOnlyTool = (tool6) => tool6?.readOnly === true;
751
+ var globToRegExp = (glob) => new RegExp(
752
+ `^${glob.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`
753
+ );
754
+ var matchRule = (rules, toolName) => {
755
+ if (!rules) return void 0;
756
+ if (Object.hasOwn(rules, toolName)) return rules[toolName];
757
+ let best;
758
+ for (const [pattern, perm] of Object.entries(rules)) {
759
+ if (!pattern.includes("*")) continue;
760
+ if (!globToRegExp(pattern).test(toolName)) continue;
761
+ const len = pattern.replace(/\*/g, "").length;
762
+ if (!best || len > best.len) best = { len, perm };
763
+ }
764
+ return best?.perm;
765
+ };
766
+ var decideToolPermission = (opts) => {
767
+ if (opts.remembered?.has(opts.toolName)) return "allow";
768
+ const rule = matchRule(opts.rules, opts.toolName);
769
+ if (rule) return rule;
770
+ switch (opts.mode) {
771
+ case "autopilot":
772
+ return "allow";
773
+ case "read-only":
774
+ return opts.readOnly ? "allow" : "deny";
775
+ case "ask-writes":
776
+ return opts.readOnly ? "allow" : "ask";
777
+ case "ask-all":
778
+ return "ask";
779
+ }
780
+ };
781
+ var normalizeDecision = (d) => typeof d === "boolean" ? { approved: d } : { approved: Boolean(d?.approved), reason: d?.reason, remember: d?.remember };
782
+ var createApprovalState = (config) => ({
783
+ mode: config?.mode ?? "autopilot",
784
+ config: config ?? {},
785
+ remembered: /* @__PURE__ */ new Set()
786
+ });
787
+ var seq = 0;
788
+ var gateToolCall = async (state, call, events, signal) => {
789
+ const permission = decideToolPermission({
790
+ toolName: call.toolName,
791
+ readOnly: call.readOnly,
792
+ mode: state.mode,
793
+ rules: state.config.rules,
794
+ remembered: state.remembered
795
+ });
796
+ if (permission === "allow") return;
797
+ const id = `approval-${Date.now().toString(36)}-${seq += 1}`;
798
+ if (permission === "deny") {
799
+ const reason = state.mode === "read-only" && !matchRule(state.config.rules, call.toolName) ? "the agent is in read-only mode" : "blocked by policy";
800
+ events.resolved({ id, name: call.toolName, approved: false, reason, automatic: true });
801
+ throw new ToolDeniedError(reason);
802
+ }
803
+ const onRequest = state.config.onRequest;
804
+ if (!onRequest) {
805
+ const reason = "no approval handler configured";
806
+ events.resolved({ id, name: call.toolName, approved: false, reason, automatic: true });
807
+ throw new ToolDeniedError(reason);
808
+ }
809
+ const req = { id, ...call };
810
+ events.requested(req);
811
+ let decision;
812
+ try {
813
+ decision = normalizeDecision(
814
+ await withDeadline(Promise.resolve(onRequest(req)), state.config.timeoutMs, signal)
815
+ );
816
+ } catch (err) {
817
+ const reason = err instanceof Error ? err.message : String(err);
818
+ events.resolved({ id, name: call.toolName, approved: false, reason, automatic: true });
819
+ throw new ToolDeniedError(reason);
820
+ }
821
+ if (decision.approved && decision.remember) state.remembered.add(call.toolName);
822
+ events.resolved({
823
+ id,
824
+ name: call.toolName,
825
+ approved: decision.approved,
826
+ reason: decision.reason,
827
+ automatic: false
828
+ });
829
+ if (!decision.approved) throw new ToolDeniedError(decision.reason);
830
+ };
831
+ var withDeadline = (p, timeoutMs, signal) => {
832
+ if ((!timeoutMs || timeoutMs <= 0) && !signal) return p;
833
+ return new Promise((resolve, reject) => {
834
+ let timer;
835
+ const done = () => {
836
+ if (timer !== void 0) clearTimeout(timer);
837
+ signal?.removeEventListener("abort", onAbort);
838
+ };
839
+ const onAbort = () => {
840
+ done();
841
+ reject(new Error("the run was stopped while waiting for approval"));
842
+ };
843
+ if (signal?.aborted) return onAbort();
844
+ signal?.addEventListener("abort", onAbort, { once: true });
845
+ if (timeoutMs && timeoutMs > 0) {
846
+ timer = setTimeout(() => {
847
+ done();
848
+ reject(new Error(`no approval decision within ${timeoutMs} ms`));
849
+ }, timeoutMs);
850
+ }
851
+ p.then(
852
+ (v) => {
853
+ done();
854
+ resolve(v);
855
+ },
856
+ (e) => {
857
+ done();
858
+ reject(e);
859
+ }
860
+ );
861
+ });
862
+ };
863
+
416
864
  // src/tools/mode.ts
417
865
  var LOCAL_RE = /web-?llm|mlc|browser-ai|transformers|nano|built-?in/i;
418
866
  var selectToolMode = (model, mode = "auto") => {
@@ -423,15 +871,15 @@ var selectToolMode = (model, mode = "auto") => {
423
871
  };
424
872
 
425
873
  // src/tools/types.ts
426
- var promptHintOf = (tool2) => typeof tool2.promptHint === "string" ? tool2.promptHint : void 0;
874
+ var promptHintOf = (tool6) => typeof tool6.promptHint === "string" ? tool6.promptHint : void 0;
427
875
 
428
876
  // src/tools/prompted.ts
429
- var renderCatalog = (tools) => {
877
+ var renderCatalog = (tools, hintOf) => {
430
878
  const names = Object.keys(tools);
431
879
  if (names.length === 0) return "(no tools)";
432
880
  return names.map((name) => {
433
881
  const t = tools[name];
434
- const hint = promptHintOf(t) ?? "";
882
+ const hint = promptHintOf(t) ?? hintOf?.(name) ?? "";
435
883
  const desc = typeof t.description === "string" ? t.description : "";
436
884
  return `- ${name}(${hint}): ${desc}`;
437
885
  }).join("\n");
@@ -481,6 +929,244 @@ var dispatch = async (action, tools, ctx = {}) => {
481
929
  }
482
930
  };
483
931
 
932
+ // src/tools/search.ts
933
+ import { jsonSchema as jsonSchema2, tool as tool2 } from "ai";
934
+ var FIND_TOOLS_NAME = "find_tools";
935
+ var toolCatalogOf = (tools) => Object.entries(tools).map(([name, t]) => {
936
+ const sep = name.indexOf("__");
937
+ return {
938
+ name,
939
+ description: typeof t.description === "string" ? t.description : "",
940
+ ...sep > 0 ? { server: name.slice(0, sep) } : {},
941
+ readOnly: isReadOnlyTool(t)
942
+ };
943
+ });
944
+ var tokenize = (s) => (s ?? "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
945
+ var hits = (term, words) => words.some(
946
+ (w) => w === term || term.length >= 3 && (w.startsWith(term) || term.startsWith(w) && w.length >= 3)
947
+ );
948
+ var searchTools = (catalog, query, opts = {}) => {
949
+ const terms = [...new Set(tokenize(query))];
950
+ const limit = Math.min(Math.max(1, opts.limit ?? 8), 20);
951
+ const server = opts.server?.toLowerCase();
952
+ const scored = [];
953
+ catalog.forEach((entry, index) => {
954
+ if (opts.exclude?.has(entry.name)) return;
955
+ if (server && entry.server?.toLowerCase() !== server) return;
956
+ const nameWords = tokenize(
957
+ entry.server ? entry.name.slice(entry.server.length + 2) : entry.name
958
+ );
959
+ const serverWords = tokenize(entry.server ?? "");
960
+ const descWords = tokenize(entry.description);
961
+ let score = 0;
962
+ for (const term of terms) {
963
+ if (hits(term, nameWords)) score += 3;
964
+ if (hits(term, serverWords)) score += 2;
965
+ if (hits(term, descWords)) score += 1;
966
+ }
967
+ if (terms.length === 0 && server) score = 1;
968
+ if (score > 0) scored.push({ entry, score, index });
969
+ });
970
+ scored.sort((a, b) => b.score - a.score || a.index - b.index);
971
+ return scored.slice(0, limit).map((s) => s.entry);
972
+ };
973
+ var renderSearchCatalog = (catalog, budget = 12e3) => {
974
+ if (catalog.length === 0) return "(no tools)";
975
+ const groups = /* @__PURE__ */ new Map();
976
+ for (const e of catalog) {
977
+ const key = e.server ?? "(host)";
978
+ groups.set(key, [...groups.get(key) ?? [], e]);
979
+ }
980
+ const lines = [];
981
+ let used = 0;
982
+ let shown = 0;
983
+ for (const [server, entries] of groups) {
984
+ const head = `[${server}] ${entries.length} tool(s)`;
985
+ if (used + head.length > budget) break;
986
+ lines.push(head);
987
+ used += head.length + 1;
988
+ for (const e of entries) {
989
+ const desc = e.description.replace(/\s+/g, " ").trim().slice(0, 60);
990
+ const line = `- ${e.name}${desc ? `: ${desc}` : ""}`;
991
+ if (used + line.length > budget) break;
992
+ lines.push(line);
993
+ used += line.length + 1;
994
+ shown += 1;
995
+ }
996
+ }
997
+ const rest = catalog.length - shown;
998
+ if (rest > 0)
999
+ lines.push(`\u2026 ${rest} more tool(s) \u2014 the executor can find them with ${FIND_TOOLS_NAME}`);
1000
+ return lines.join("\n");
1001
+ };
1002
+ var createFindToolsTool = (catalog, onFound, hintOf) => {
1003
+ const t = tool2({
1004
+ description: "Search the full tool catalogue by keywords and make the matching tools callable from the next step on. Use it whenever the tool you need is not in your current tool list.",
1005
+ inputSchema: jsonSchema2({
1006
+ type: "object",
1007
+ properties: {
1008
+ query: { type: "string", description: "Keywords: what the tool should do" },
1009
+ server: { type: "string", description: "Optional: only tools of this MCP server" },
1010
+ limit: { type: "number", description: "Max results (default 8, max 20)" }
1011
+ },
1012
+ required: ["query"],
1013
+ additionalProperties: false
1014
+ }),
1015
+ execute: async ({ query, server, limit }) => {
1016
+ const found = searchTools(catalog(), query, { server, limit });
1017
+ onFound(
1018
+ query,
1019
+ found.map((f) => f.name)
1020
+ );
1021
+ return {
1022
+ tools: found.map((f) => ({
1023
+ name: f.name,
1024
+ description: f.description,
1025
+ ...f.server ? { server: f.server } : {},
1026
+ readOnly: Boolean(f.readOnly),
1027
+ ...hintOf?.(f.name) ? { args: hintOf(f.name) } : {}
1028
+ })),
1029
+ note: found.length ? "These tools are now callable." : "No tool matched; try other keywords or a server name."
1030
+ };
1031
+ }
1032
+ });
1033
+ t.readOnly = true;
1034
+ t.promptHint = "{ query: string, server?: string, limit?: number }";
1035
+ return t;
1036
+ };
1037
+
1038
+ // src/tools/wrap.ts
1039
+ import { asSchema } from "ai";
1040
+ var toolRunContextOf = (options) => options?.agentRun;
1041
+ var truncated = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026 [truncated ${s.length - max} chars]` : s;
1042
+ var toText = (v) => {
1043
+ if (typeof v === "string") return v;
1044
+ try {
1045
+ return JSON.stringify(v) ?? String(v);
1046
+ } catch {
1047
+ return String(v);
1048
+ }
1049
+ };
1050
+ var limitModelOutput = (original, maxChars) => (async (opts) => {
1051
+ const base = original ? await original(opts) : typeof opts.output === "string" ? { type: "text", value: opts.output } : { type: "json", value: opts.output ?? null };
1052
+ if (base.type === "text" || base.type === "error-text") {
1053
+ return { ...base, value: truncated(base.value, maxChars) };
1054
+ }
1055
+ if (base.type === "json" || base.type === "error-json") {
1056
+ const text = toText(base.value);
1057
+ if (text.length <= maxChars) return base;
1058
+ return {
1059
+ type: base.type === "json" ? "text" : "error-text",
1060
+ value: truncated(text, maxChars)
1061
+ };
1062
+ }
1063
+ return base;
1064
+ });
1065
+ var wrapToolsForRun = (tools, o) => {
1066
+ const out = {};
1067
+ const gate = (toolName, input, readOnly, signal) => gateToolCall(
1068
+ o.approval,
1069
+ { toolName, input, readOnly, step: o.step() },
1070
+ {
1071
+ requested: (req) => o.emit({
1072
+ type: "tool.approval-requested",
1073
+ id: req.id,
1074
+ name: req.toolName,
1075
+ input: req.input,
1076
+ readOnly: req.readOnly,
1077
+ step: req.step
1078
+ }),
1079
+ resolved: (r) => o.emit({ type: "tool.approval-resolved", ...r })
1080
+ },
1081
+ signal ?? o.signal
1082
+ );
1083
+ for (const [name, t] of Object.entries(tools)) {
1084
+ const execute = t.execute;
1085
+ const builtin = o.builtins.has(name);
1086
+ const readOnly = builtin || isReadOnlyTool(t);
1087
+ const wrapped = { ...t };
1088
+ if (o.maxToolOutputChars > 0) {
1089
+ wrapped.toModelOutput = limitModelOutput(t.toModelOutput, o.maxToolOutputChars);
1090
+ }
1091
+ if (typeof execute === "function") {
1092
+ wrapped.execute = (input, options) => {
1093
+ const run = () => {
1094
+ if (!builtin) o.countCall();
1095
+ const agentRun = {
1096
+ emit: o.emit,
1097
+ step: o.step(),
1098
+ signal: o.signal,
1099
+ addUsage: o.addUsage,
1100
+ approve: (toolName, toolInput, ro) => gate(toolName, toolInput, ro)
1101
+ };
1102
+ return execute.call(t, input, { ...options, agentRun });
1103
+ };
1104
+ if (builtin) return run();
1105
+ const permission = decideToolPermission({
1106
+ toolName: name,
1107
+ readOnly,
1108
+ mode: o.approval.mode,
1109
+ rules: o.approval.config.rules,
1110
+ remembered: o.approval.remembered
1111
+ });
1112
+ if (permission === "allow") return run();
1113
+ return gate(
1114
+ name,
1115
+ input,
1116
+ readOnly,
1117
+ options?.abortSignal
1118
+ ).then(run);
1119
+ };
1120
+ }
1121
+ out[name] = wrapped;
1122
+ }
1123
+ return out;
1124
+ };
1125
+ var createCallCounter = (cap) => {
1126
+ let calls = 0;
1127
+ return {
1128
+ count: () => {
1129
+ if (cap && cap > 0 && calls >= cap) throw new ToolBudgetError(cap);
1130
+ calls += 1;
1131
+ },
1132
+ get calls() {
1133
+ return calls;
1134
+ },
1135
+ get exhausted() {
1136
+ return Boolean(cap && cap > 0 && calls >= cap);
1137
+ }
1138
+ };
1139
+ };
1140
+ var typeHint = (s, depth) => {
1141
+ if (!s) return "any";
1142
+ if (Array.isArray(s.enum) && s.enum.length > 0 && s.enum.length <= 8) {
1143
+ return s.enum.map((v) => JSON.stringify(v)).join(" | ");
1144
+ }
1145
+ const type = Array.isArray(s.type) ? s.type.find((x) => x !== "null") : s.type;
1146
+ if (type === "array") return `${typeHint(s.items, depth)}[]`;
1147
+ if (type === "object" || s.properties) {
1148
+ if (depth > 1 || !s.properties) return "object";
1149
+ return objectHint(s, depth + 1);
1150
+ }
1151
+ return type ?? "any";
1152
+ };
1153
+ var objectHint = (s, depth = 0) => {
1154
+ const required = new Set(s.required ?? []);
1155
+ const props = Object.entries(s.properties ?? {});
1156
+ if (props.length === 0) return "{}";
1157
+ return `{ ${props.slice(0, 12).map(([k, v]) => `${k}${required.has(k) ? "" : "?"}: ${typeHint(v, depth)}`).join(", ")}${props.length > 12 ? ", \u2026" : ""} }`;
1158
+ };
1159
+ var schemaHintOf = async (t) => {
1160
+ const own = promptHintOf(t);
1161
+ if (own) return own;
1162
+ try {
1163
+ const schema = await asSchema(t.inputSchema).jsonSchema;
1164
+ return schema && typeof schema === "object" ? objectHint(schema) : void 0;
1165
+ } catch {
1166
+ return void 0;
1167
+ }
1168
+ };
1169
+
484
1170
  // src/llm/tool-loop.ts
485
1171
  import {
486
1172
  generateText as generateText2,
@@ -490,15 +1176,26 @@ import {
490
1176
 
491
1177
  // src/agent/loop-types.ts
492
1178
  var BLOCKER = "[BLOCKER]";
493
- var emptyUsage = () => ({ inputTokens: 0, outputTokens: 0, totalTokens: 0 });
1179
+ var emptyUsage = () => ({
1180
+ inputTokens: 0,
1181
+ outputTokens: 0,
1182
+ totalTokens: 0,
1183
+ reasoningTokens: 0,
1184
+ cachedInputTokens: 0,
1185
+ cacheWriteTokens: 0
1186
+ });
494
1187
  var addUsage = (a, b) => ({
495
1188
  inputTokens: a.inputTokens + b.inputTokens,
496
1189
  outputTokens: a.outputTokens + b.outputTokens,
497
- totalTokens: a.totalTokens + b.totalTokens
1190
+ totalTokens: a.totalTokens + b.totalTokens,
1191
+ reasoningTokens: (a.reasoningTokens ?? 0) + (b.reasoningTokens ?? 0),
1192
+ cachedInputTokens: (a.cachedInputTokens ?? 0) + (b.cachedInputTokens ?? 0),
1193
+ cacheWriteTokens: (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0)
498
1194
  });
499
1195
 
500
1196
  // src/parse.ts
501
- var stripFences = (text) => (text || "").replace(/```(?:json)?/gi, "").trim();
1197
+ var stripThinking = (text) => text.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/^[\s\S]*?<\/think>/i, "");
1198
+ var stripFences = (text) => stripThinking(text || "").replace(/```(?:json)?/gi, "").trim();
502
1199
  var extractBalanced = (text, open) => {
503
1200
  const close = open === "{" ? "}" : "]";
504
1201
  const start = text.indexOf(open);
@@ -588,15 +1285,16 @@ var parsePlannerResponse = (raw) => {
588
1285
  const obj = asObject(text);
589
1286
  if (obj) {
590
1287
  const reply = typeof obj.reply === "string" ? obj.reply.trim() : "";
591
- return { reply, plan: normalizeSteps(obj.plan) };
1288
+ return { reply, plan: normalizeSteps(obj.plan), skills: normalizeSteps(obj.skills) };
592
1289
  }
593
1290
  if (/"plan"\s*:/.test(text)) {
594
1291
  return {
595
1292
  reply: salvageString(text, "reply"),
596
- plan: normalizeSteps(salvageStringArray(text, "plan"))
1293
+ plan: normalizeSteps(salvageStringArray(text, "plan")),
1294
+ skills: normalizeSteps(salvageStringArray(text, "skills"))
597
1295
  };
598
1296
  }
599
- return { reply: looksLikeJson(text) ? "" : text.trim(), plan: [] };
1297
+ return { reply: looksLikeJson(text) ? "" : text.trim(), plan: [], skills: [] };
600
1298
  };
601
1299
  var toActions = (value) => {
602
1300
  if (!Array.isArray(value)) return [];
@@ -639,20 +1337,99 @@ var parsePlainText = (raw) => {
639
1337
  return looksLikeJson(text) ? "" : text.trim();
640
1338
  };
641
1339
 
1340
+ // src/llm/context-editing.ts
1341
+ var STUB = (name) => `[${name} result cleared to save context \u2014 call the tool again if you still need it]`;
1342
+ var estimate = (messages) => {
1343
+ try {
1344
+ return estimateTokens(JSON.stringify(messages));
1345
+ } catch {
1346
+ return 0;
1347
+ }
1348
+ };
1349
+ var positionsOf = (messages) => messages.flatMap(
1350
+ (m, i) => m.role === "tool" && Array.isArray(m.content) ? m.content.flatMap(
1351
+ (p, j) => p.type === "tool-result" ? [`${i}:${j}`] : []
1352
+ ) : []
1353
+ );
1354
+ var createToolResultClearer = (opts, onCleared) => {
1355
+ const keep = Math.max(0, opts.keep ?? 3);
1356
+ const cleared = /* @__PURE__ */ new Set();
1357
+ const apply = (messages) => cleared.size === 0 ? messages : messages.map(
1358
+ (m, i) => m.role === "tool" && Array.isArray(m.content) ? {
1359
+ ...m,
1360
+ content: m.content.map(
1361
+ (p, j) => p.type === "tool-result" && cleared.has(`${i}:${j}`) ? { ...p, output: { type: "text", value: STUB(p.toolName) } } : p
1362
+ )
1363
+ } : m
1364
+ );
1365
+ return (messages) => {
1366
+ let edited = apply(messages);
1367
+ const before = estimate(edited);
1368
+ if (before <= opts.triggerTokens) return edited;
1369
+ const positions = positionsOf(messages);
1370
+ let added = 0;
1371
+ for (const pos of positions.slice(0, Math.max(0, positions.length - keep))) {
1372
+ if (!cleared.has(pos)) {
1373
+ cleared.add(pos);
1374
+ added += 1;
1375
+ }
1376
+ }
1377
+ if (added === 0) return edited;
1378
+ edited = apply(messages);
1379
+ onCleared?.({ cleared: added, beforeTokens: before, afterTokens: estimate(edited) });
1380
+ return edited;
1381
+ };
1382
+ };
1383
+
642
1384
  // src/llm/tool-loop.ts
643
1385
  var runToolLoop = (model, opts) => opts.mode === "prompted" ? runPrompted(model, opts) : runNative(model, opts);
1386
+ var activeOf = (opts) => typeof opts.activeTools === "function" ? opts.activeTools() : opts.activeTools;
1387
+ var callExtras = (opts) => ({
1388
+ ...opts.system !== void 0 ? { instructions: opts.system } : {},
1389
+ maxOutputTokens: opts.maxOutputTokens,
1390
+ temperature: opts.temperature,
1391
+ ...opts.reasoning ? { reasoning: opts.reasoning } : {},
1392
+ ...opts.providerOptions ? { providerOptions: opts.providerOptions } : {}
1393
+ });
644
1394
  var runNative = async (model, opts) => {
645
1395
  const toolCalls = [];
646
1396
  const pending = /* @__PURE__ */ new Map();
1397
+ let stoppedByBudget = false;
1398
+ const stopWhen = [stepCountIs(opts.maxSteps ?? 4)];
1399
+ if (opts.shouldStop) {
1400
+ const shouldStop = opts.shouldStop;
1401
+ stopWhen.push(({ steps }) => {
1402
+ const used = steps.reduce((u, s) => addUsage(u, normalizeUsage(s.usage)), emptyUsage());
1403
+ if (!shouldStop(used)) return false;
1404
+ stoppedByBudget = true;
1405
+ return true;
1406
+ });
1407
+ }
1408
+ const initialActive = activeOf(opts);
1409
+ const dynamic = typeof opts.activeTools === "function";
1410
+ const caching = opts.caching?.enabled ? opts.caching : void 0;
1411
+ const clear = opts.clearToolResults ? createToolResultClearer(opts.clearToolResults, opts.callbacks?.onToolResultsCleared) : void 0;
1412
+ const editMessages = caching || clear;
647
1413
  const result = streamText2({
648
1414
  model,
649
1415
  tools: opts.tools,
650
- ...opts.activeTools ? { activeTools: opts.activeTools } : {},
651
- stopWhen: stepCountIs(opts.maxSteps ?? 4),
652
- system: opts.system,
1416
+ ...initialActive ? { activeTools: initialActive } : {},
1417
+ // Before every round: re-read the active set so tools activated mid-call
1418
+ // (find_tools) become callable; clear stale tool results; move the cache
1419
+ // breakpoint to the newest message.
1420
+ ...dynamic || editMessages ? {
1421
+ prepareStep: ({ messages }) => {
1422
+ let next = clear ? clear(messages) : messages;
1423
+ if (caching) next = withRollingBreakpoint(next, caching);
1424
+ return {
1425
+ ...dynamic ? { activeTools: activeOf(opts) } : {},
1426
+ ...editMessages ? { messages: next } : {}
1427
+ };
1428
+ }
1429
+ } : {},
1430
+ stopWhen,
1431
+ ...callExtras(opts),
653
1432
  ...promptOf(opts),
654
- maxOutputTokens: opts.maxOutputTokens,
655
- temperature: opts.temperature,
656
1433
  abortSignal: timeoutSignal(opts.abortSignal, opts.timeoutMs)
657
1434
  });
658
1435
  for await (const part of result.fullStream) {
@@ -660,6 +1437,9 @@ var runNative = async (model, opts) => {
660
1437
  case "text-delta":
661
1438
  if (part.text) opts.callbacks?.onTextDelta?.(part.text);
662
1439
  break;
1440
+ case "reasoning-delta":
1441
+ if (part.text) opts.callbacks?.onReasoningDelta?.(part.text);
1442
+ break;
663
1443
  case "tool-call":
664
1444
  pending.set(part.toolCallId, { name: part.toolName, input: part.input });
665
1445
  opts.callbacks?.onToolCall?.(part.toolName, part.input);
@@ -693,7 +1473,7 @@ var runNative = async (model, opts) => {
693
1473
  }
694
1474
  }
695
1475
  const [text, usage] = await Promise.all([result.text, result.usage]);
696
- return { text: text.trim(), toolCalls, usage: normalizeUsage(usage) };
1476
+ return { text: text.trim(), toolCalls, usage: normalizeUsage(usage), stoppedByBudget };
697
1477
  };
698
1478
  var asJson = (value) => {
699
1479
  try {
@@ -702,13 +1482,16 @@ var asJson = (value) => {
702
1482
  return String(value);
703
1483
  }
704
1484
  };
705
- var toolResultsPrompt = (results) => {
1485
+ var toolResultsPrompt = (results, newTools) => {
706
1486
  const lines = results.map(
707
1487
  (r) => `- ${r.name} ${clip(asJson(r.input), 160)} \u2192 ${r.ok ? "ok" : "FAILED"}: ${clip(asJson(r.output), 400)}`
708
1488
  );
709
1489
  return `TOOL RESULTS:
710
1490
  ${lines.join("\n")}
711
-
1491
+ ${newTools ? `
1492
+ TOOLS NOW AVAILABLE (call them by these exact names):
1493
+ ${newTools}
1494
+ ` : ""}
712
1495
  Continue THIS step using the results above. Reply with a single JSON object, nothing else:
713
1496
  { "reply": string, "actions": [ { "tool": string, "args": object } ] }
714
1497
  - If the step is now complete: "actions": [] and a short outcome in "reply".
@@ -716,22 +1499,28 @@ Continue THIS step using the results above. Reply with a single JSON object, not
716
1499
  };
717
1500
  var runPrompted = async (model, opts) => {
718
1501
  const maxRounds = Math.max(1, opts.maxSteps ?? 4);
719
- const active = opts.activeTools;
720
- const tools = active ? Object.fromEntries(Object.entries(opts.tools).filter(([name]) => active.includes(name))) : opts.tools;
1502
+ const callable = () => {
1503
+ const active = activeOf(opts);
1504
+ return active ? Object.fromEntries(Object.entries(opts.tools).filter(([name]) => active.includes(name))) : opts.tools;
1505
+ };
721
1506
  const signal = timeoutSignal(opts.abortSignal, opts.timeoutMs);
722
1507
  const messages = opts.messages ? [...opts.messages] : [{ role: "user", content: opts.prompt ?? "" }];
723
1508
  const toolCalls = [];
724
1509
  const seen = /* @__PURE__ */ new Set();
725
1510
  let usage = emptyUsage();
726
1511
  let text = "";
1512
+ let stoppedByBudget = false;
727
1513
  for (let round = 1; round <= maxRounds; round += 1) {
728
1514
  if (signal?.aborted) break;
1515
+ if (opts.shouldStop?.(usage)) {
1516
+ stoppedByBudget = true;
1517
+ break;
1518
+ }
1519
+ const before = new Set(Object.keys(callable()));
729
1520
  const result = await generateText2({
730
1521
  model,
731
- system: opts.system,
732
- messages,
733
- maxOutputTokens: opts.maxOutputTokens,
734
- temperature: opts.temperature,
1522
+ ...callExtras(opts),
1523
+ messages: opts.caching ? withRollingBreakpoint(messages, opts.caching) : messages,
735
1524
  abortSignal: signal
736
1525
  });
737
1526
  usage = addUsage(usage, normalizeUsage(result.usage));
@@ -746,21 +1535,192 @@ var runPrompted = async (model, opts) => {
746
1535
  for (const action of parsed.actions) {
747
1536
  if (signal?.aborted) break;
748
1537
  opts.callbacks?.onToolCall?.(action.tool, action.args);
749
- const rec = await dispatch(action, tools, { abortSignal: signal });
1538
+ const rec = await dispatch(action, callable(), { abortSignal: signal });
750
1539
  toolCalls.push(rec);
751
1540
  results.push(rec);
752
1541
  opts.callbacks?.onToolResult?.(rec.name, rec.output, rec.ok);
753
1542
  }
754
1543
  if (round === maxRounds || signal?.aborted) break;
1544
+ const added = Object.keys(callable()).filter((n) => !before.has(n));
755
1545
  messages.push({ role: "assistant", content: result.text });
756
- messages.push({ role: "user", content: toolResultsPrompt(results) });
1546
+ messages.push({
1547
+ role: "user",
1548
+ content: toolResultsPrompt(
1549
+ results,
1550
+ added.length && opts.describeTools ? opts.describeTools(added) : ""
1551
+ )
1552
+ });
757
1553
  }
758
1554
  if (text) opts.callbacks?.onTextDelta?.(text);
759
- return { text, toolCalls, usage };
1555
+ return { text, toolCalls, usage, stoppedByBudget };
760
1556
  };
761
1557
 
1558
+ // src/thinking.ts
1559
+ var normalize = (setting) => {
1560
+ if (setting === void 0 || setting === false) return void 0;
1561
+ if (setting === true) return { level: "medium" };
1562
+ if (typeof setting === "string") return { level: setting };
1563
+ return setting;
1564
+ };
1565
+ var resolveThinking = (setting) => {
1566
+ const cfg = normalize(setting);
1567
+ if (!cfg) return {};
1568
+ const level = cfg.level ?? "medium";
1569
+ if (level === "none") return { reasoning: "none" };
1570
+ const includeThoughts = cfg.includeThoughts !== false;
1571
+ const budget = cfg.budgetTokens;
1572
+ if (typeof budget === "number" && budget > 0) {
1573
+ return {
1574
+ reasoning: level,
1575
+ providerOptions: {
1576
+ anthropic: { thinking: { type: "enabled", budgetTokens: budget } },
1577
+ google: { thinkingConfig: { thinkingBudget: budget, includeThoughts } }
1578
+ }
1579
+ };
1580
+ }
1581
+ if (!includeThoughts) return { reasoning: level };
1582
+ return {
1583
+ reasoning: level,
1584
+ providerOptions: {
1585
+ google: { thinkingConfig: { includeThoughts: true } },
1586
+ openai: { reasoningSummary: "auto" }
1587
+ }
1588
+ };
1589
+ };
1590
+ var thinkingFor = (stage, top, perStage) => perStage && stage in perStage ? perStage[stage] : top;
1591
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1592
+ var deepMerge = (a, b) => {
1593
+ const out = { ...a };
1594
+ for (const [k, v] of Object.entries(b)) {
1595
+ out[k] = isPlainObject(out[k]) && isPlainObject(v) ? deepMerge(out[k], v) : v;
1596
+ }
1597
+ return out;
1598
+ };
1599
+ var mergeProviderOptions = (...maps) => {
1600
+ let out;
1601
+ for (const m of maps) {
1602
+ if (!m || Object.keys(m).length === 0) continue;
1603
+ out = out ? deepMerge(out, m) : { ...m };
1604
+ }
1605
+ return out;
1606
+ };
1607
+
1608
+ // src/images.ts
1609
+ var DATA_URL_RE = /^data:([^;,]+)?;base64,(.*)$/s;
1610
+ var HTTP_RE = /^https?:\/\//i;
1611
+ var mediaTypeOf = (f) => {
1612
+ if (f.mediaType) return f.mediaType;
1613
+ if (typeof f.data === "string") {
1614
+ const m = DATA_URL_RE.exec(f.data);
1615
+ if (m?.[1]) return m[1];
1616
+ }
1617
+ const url = f.data instanceof URL ? f.data.href : typeof f.data === "string" ? f.data : "";
1618
+ if (HTTP_RE.test(url) || f.name) {
1619
+ const ext = (f.name ?? new URL(url, "http://x").pathname).split(".").pop()?.toLowerCase();
1620
+ if (ext === "pdf") return "application/pdf";
1621
+ if (ext && ["png", "jpg", "jpeg", "gif", "webp"].includes(ext)) {
1622
+ return `image/${ext === "jpg" ? "jpeg" : ext}`;
1623
+ }
1624
+ }
1625
+ return void 0;
1626
+ };
1627
+ var attachmentKind = (f) => {
1628
+ const mt = mediaTypeOf(f) ?? "";
1629
+ if (mt.startsWith("image/")) return "image";
1630
+ if (mt === "application/pdf") return "pdf";
1631
+ return "file";
1632
+ };
1633
+ var KIND_LABEL = {
1634
+ image: "images",
1635
+ pdf: "PDF files",
1636
+ file: "file attachments"
1637
+ };
1638
+ var KIND_HINT = {
1639
+ image: "Remove the image, or switch to a vision-capable model such as Gemini, Claude or GPT-4o/GPT-5.",
1640
+ pdf: "Convert the PDF to text first (e.g. PDF \u2192 Markdown), or switch to a model that reads PDFs such as Gemini, Claude or GPT-4o/GPT-5.",
1641
+ file: "Paste its text instead, or switch to a model that accepts this file type."
1642
+ };
1643
+ var AttachmentsNotSupportedError = class extends Error {
1644
+ kind;
1645
+ constructor(model, kind, detail) {
1646
+ super(
1647
+ `The model "${model}" can't take ${KIND_LABEL[kind]}${detail ? ` (${detail})` : ""}. ${KIND_HINT[kind]}`
1648
+ );
1649
+ this.name = "AttachmentsNotSupportedError";
1650
+ this.kind = kind;
1651
+ }
1652
+ toJSON() {
1653
+ return this.message;
1654
+ }
1655
+ };
1656
+ var ImagesNotSupportedError = class extends AttachmentsNotSupportedError {
1657
+ constructor(model, detail) {
1658
+ super(model, "image", detail);
1659
+ this.name = "ImagesNotSupportedError";
1660
+ }
1661
+ };
1662
+ var unsupportedAttachment = (model, kind, detail) => kind === "image" ? new ImagesNotSupportedError(model, detail) : new AttachmentsNotSupportedError(model, kind, detail);
1663
+ var dataOf = (f) => {
1664
+ if (typeof f.data === "string") {
1665
+ const m = DATA_URL_RE.exec(f.data);
1666
+ if (m) return m[2];
1667
+ if (HTTP_RE.test(f.data)) return new URL(f.data);
1668
+ }
1669
+ return f.data;
1670
+ };
1671
+ var toFilePart = (f) => {
1672
+ const kind = attachmentKind(f);
1673
+ const mediaType = mediaTypeOf(f) ?? (kind === "image" ? "image" : "application/octet-stream");
1674
+ return {
1675
+ type: "file",
1676
+ data: dataOf(f),
1677
+ mediaType,
1678
+ ...f.name ? { filename: f.name } : {}
1679
+ };
1680
+ };
1681
+ var toImagePart = toFilePart;
1682
+ var modelLabel = (model) => {
1683
+ if (typeof model === "string") return model;
1684
+ const m = model;
1685
+ return m?.modelId ? `${m.modelId}${m.provider ? ` (${m.provider})` : ""}` : "this model";
1686
+ };
1687
+ var REFUSAL_RE = /image|vision|multimodal|multi-modal|media type|mediatype|pdf|document|unsupported (file|content|part|mime)|does not support (file|content)|invalid content type|image_url|inline_?data|file_?data/i;
1688
+ var isAttachmentRefusal = (err) => REFUSAL_RE.test(err instanceof Error ? `${err.name} ${err.message}` : String(err));
1689
+ var isImageRefusal = isAttachmentRefusal;
1690
+
762
1691
  // src/agent/internal.ts
1692
+ var promptFor = (ctx, prompt) => ctx.images?.length ? {
1693
+ messages: [
1694
+ {
1695
+ role: "user",
1696
+ content: [{ type: "text", text: prompt }, ...ctx.images.map(toFilePart)]
1697
+ }
1698
+ ]
1699
+ } : { prompt };
1700
+ var imageRefusal = (ctx, err, model) => {
1701
+ if (!ctx.images?.length || err instanceof AttachmentsNotSupportedError || !isAttachmentRefusal(err)) {
1702
+ return void 0;
1703
+ }
1704
+ const kinds = new Set(ctx.images.map(attachmentKind));
1705
+ const kind = kinds.size === 1 ? [...kinds][0] : "file";
1706
+ const detail = err instanceof Error ? err.message.slice(0, 160) : void 0;
1707
+ return unsupportedAttachment(modelLabel(model), kind, detail && `the provider said: ${detail}`);
1708
+ };
763
1709
  var systemFor = (ctx, base) => withSystem(base, ctx.raw.systemPrompt);
1710
+ var stageCall = (ctx, stage, baseSystem) => {
1711
+ const system = systemFor(ctx, baseSystem);
1712
+ const thinking = resolveThinking(thinkingFor(stage, ctx.raw.thinking, ctx.raw.stageThinking));
1713
+ const caching = ctx.caching ?? { enabled: false };
1714
+ const providerOptions = mergeProviderOptions(
1715
+ cachingProviderOptions(caching, ctx.config.clientName, stage),
1716
+ thinking.providerOptions
1717
+ );
1718
+ return {
1719
+ system: cachedInstructions(system, caching),
1720
+ ...thinking.reasoning ? { reasoning: thinking.reasoning } : {},
1721
+ ...providerOptions ? { providerOptions } : {}
1722
+ };
1723
+ };
764
1724
 
765
1725
  // src/agent/executor.ts
766
1726
  var splitBlocker = (raw) => {
@@ -768,20 +1728,33 @@ var splitBlocker = (raw) => {
768
1728
  return { summary: raw.split(BLOCKER).join("").trim(), blocked: true };
769
1729
  };
770
1730
  var activeToolNames = (ctx, step) => {
771
- if (ctx.config.toolSelectionStrategy !== "plan-narrowed") return void 0;
1731
+ const strategy = ctx.strategy ?? ctx.config.toolSelectionStrategy;
1732
+ const builtins = [...ctx.builtinTools ?? []].filter((n) => ctx.tools[n]);
772
1733
  const suggested = step.suggestedTools ?? [];
773
1734
  const known = suggested.filter((n) => ctx.tools[n]);
1735
+ if (strategy === "search") {
1736
+ const discovered = (ctx.discovered?.() ?? []).slice(-16);
1737
+ return [.../* @__PURE__ */ new Set([...builtins, ...known, ...discovered])];
1738
+ }
1739
+ if (strategy !== "plan-narrowed") return void 0;
774
1740
  if (known.length === 0 && suggested.length > 0) return Object.keys(ctx.tools);
775
- return known;
1741
+ return [.../* @__PURE__ */ new Set([...known, ...builtins])];
776
1742
  };
777
1743
  var summaryOf = (text, toolCallCount) => {
778
1744
  if (text.length > 0) return text;
779
1745
  return toolCallCount > 0 ? `Executed ${toolCallCount} tool call(s).` : "Step produced no output.";
780
1746
  };
1747
+ var catalogFor = (ctx, names) => renderCatalog(
1748
+ Object.fromEntries(names.filter((n) => ctx.tools[n]).map((n) => [n, ctx.tools[n]])),
1749
+ ctx.toolHint
1750
+ );
781
1751
  var executeStep = async (ctx, goal, step, index, total, done) => {
782
1752
  const state = await ctx.state();
783
- const active = activeToolNames(ctx, step);
784
- const toolCatalog = active === void 0 ? ctx.toolCatalog : renderCatalog(Object.fromEntries(active.map((name) => [name, ctx.tools[name]])));
1753
+ ctx.setCurrentStep?.(step);
1754
+ const initial = activeToolNames(ctx, step);
1755
+ const searchMode = (ctx.strategy ?? ctx.config.toolSelectionStrategy) === "search";
1756
+ const toolCatalog = catalogFor(ctx, initial ?? Object.keys(ctx.tools));
1757
+ const active = ctx.activeSkills?.() ?? [];
785
1758
  const parts = ctx.prompts.executor({
786
1759
  goal,
787
1760
  state,
@@ -790,21 +1763,44 @@ var executeStep = async (ctx, goal, step, index, total, done) => {
790
1763
  total,
791
1764
  toolCatalog,
792
1765
  done,
793
- mode: ctx.executorMode
1766
+ mode: ctx.executorMode,
1767
+ skills: ctx.skills?.length ? renderSkillIndex(ctx.skills) : void 0,
1768
+ activeSkills: active.length ? renderActiveSkills(active) : void 0,
1769
+ searchMode: searchMode && Boolean(ctx.tools[FIND_TOOLS_NAME])
794
1770
  });
795
1771
  const loop = await runToolLoop(ctx.executorModel, {
796
1772
  mode: ctx.executorMode,
797
- system: systemFor(ctx, parts.system),
798
- prompt: parts.prompt,
1773
+ ...stageCall(ctx, "executor", parts.system),
1774
+ ...promptFor(ctx, parts.prompt),
799
1775
  tools: ctx.tools,
800
- activeTools: active,
1776
+ // In search mode the active set grows while the step runs.
1777
+ activeTools: searchMode ? () => [.../* @__PURE__ */ new Set([...initial ?? [], ...ctx.discovered?.() ?? []])] : initial,
1778
+ describeTools: (names) => catalogFor(ctx, names),
801
1779
  maxSteps: ctx.config.maxStepsPerTask,
1780
+ shouldStop: ctx.overBudget,
802
1781
  maxOutputTokens: ctx.config.budgets.executor,
803
1782
  temperature: ctx.config.temperature,
804
1783
  abortSignal: ctx.signal,
805
1784
  timeoutMs: ctx.config.chatTimeoutMs,
1785
+ caching: ctx.caching,
1786
+ ...ctx.config.compaction.clearToolResultsAfterTokens > 0 ? {
1787
+ clearToolResults: {
1788
+ triggerTokens: ctx.config.compaction.clearToolResultsAfterTokens,
1789
+ keep: ctx.config.compaction.keepToolResults
1790
+ }
1791
+ } : {},
806
1792
  callbacks: {
1793
+ onToolResultsCleared: (info) => {
1794
+ ctx.log.info(`cleared ${info.cleared} stale tool result(s) from the step's context`);
1795
+ ctx.emit({
1796
+ type: "context.compacted",
1797
+ scope: "tool-results",
1798
+ beforeTokens: info.beforeTokens,
1799
+ afterTokens: info.afterTokens
1800
+ });
1801
+ },
807
1802
  onTextDelta: (delta) => ctx.emit({ type: "step.text-delta", step, delta }),
1803
+ onReasoningDelta: (delta) => ctx.emit({ type: "step.reasoning-delta", step, delta }),
808
1804
  onToolCall: (name, input) => {
809
1805
  ctx.log.info(`tool call: ${name}`, input);
810
1806
  ctx.emit({ type: "step.tool-call", step, name, input });
@@ -814,6 +1810,8 @@ var executeStep = async (ctx, goal, step, index, total, done) => {
814
1810
  ctx.emit({ type: "step.tool-result", step, name, output, ok });
815
1811
  }
816
1812
  }
1813
+ }).catch((err) => {
1814
+ throw imageRefusal(ctx, err, ctx.executorModel) ?? err;
817
1815
  });
818
1816
  ctx.log.debug("executor text:", loop.text);
819
1817
  const { summary, blocked } = splitBlocker(loop.text);
@@ -824,7 +1822,8 @@ var executeStep = async (ctx, goal, step, index, total, done) => {
824
1822
  toolCalls: loop.toolCalls,
825
1823
  blocked
826
1824
  },
827
- usage: loop.usage
1825
+ usage: loop.usage,
1826
+ stoppedByBudget: loop.stoppedByBudget
828
1827
  };
829
1828
  };
830
1829
  var shouldReplan = (result) => {
@@ -865,9 +1864,11 @@ var PlanStepSchema = z.object({
865
1864
  suggestedTools: z.array(z.string()).optional()
866
1865
  });
867
1866
  var PlanSchema = z.object({
868
- /** One short sentence. For a greeting/question/unclear request, holds the answer and steps is empty. */
1867
+ /** One short sentence. For a greeting/small talk, holds the answer and steps is empty. */
869
1868
  thought: z.string(),
870
- steps: z.array(PlanStepSchema)
1869
+ steps: z.array(PlanStepSchema),
1870
+ /** Names of the configured skills that apply to this goal. */
1871
+ skills: z.array(z.string()).optional()
871
1872
  });
872
1873
  var ReplanSchema = z.object({
873
1874
  decision: z.enum(["continue", "revise", "finish"]),
@@ -877,7 +1878,7 @@ var ReplanSchema = z.object({
877
1878
  });
878
1879
 
879
1880
  // src/agent/planner.ts
880
- var toSteps = (raw) => raw.filter((s) => s.description && s.description.trim()).map((s, i) => ({
1881
+ var toSteps = (raw, cap) => raw.filter((s) => s.description && s.description.trim()).slice(0, Math.max(1, cap)).map((s, i) => ({
881
1882
  id: `s${i + 1}`,
882
1883
  description: s.description.trim(),
883
1884
  expectedOutcome: s.expectedOutcome,
@@ -888,46 +1889,72 @@ var createPlan = async (ctx, goal, nudge) => {
888
1889
  const effectiveGoal = nudge ? `${goal}
889
1890
 
890
1891
  ${nudge}` : goal;
1892
+ const skillIndex = ctx.skills?.length ? renderSkillIndex(ctx.skills) : void 0;
1893
+ const cap = ctx.config.maxPlanSteps;
891
1894
  const commonFor = (mode) => {
892
1895
  const parts = ctx.prompts.planner({
893
1896
  goal: effectiveGoal,
894
1897
  state,
895
1898
  toolCatalog: ctx.toolCatalog,
896
1899
  mode,
897
- history: ctx.history
1900
+ history: ctx.history,
1901
+ skills: skillIndex,
1902
+ searchMode: ctx.strategy === "search"
898
1903
  });
899
1904
  return {
900
- system: systemFor(ctx, parts.system),
901
- prompt: parts.prompt,
1905
+ ...stageCall(ctx, "planner", parts.system),
1906
+ ...promptFor(ctx, parts.prompt),
902
1907
  maxOutputTokens: ctx.config.budgets.planner,
903
1908
  temperature: ctx.config.temperature,
904
1909
  abortSignal: ctx.signal,
905
1910
  timeoutMs: ctx.config.chatTimeoutMs
906
1911
  };
907
1912
  };
1913
+ const validSkills = (names) => {
1914
+ const known = new Set((ctx.skills ?? []).map((s) => s.name));
1915
+ const picked = [...new Set(names ?? [])];
1916
+ const kept = picked.filter((n) => known.has(n));
1917
+ if (kept.length < picked.length) {
1918
+ ctx.log.warn(
1919
+ "planner picked unknown skills, dropped:",
1920
+ picked.filter((n) => !known.has(n))
1921
+ );
1922
+ }
1923
+ return kept;
1924
+ };
908
1925
  if (ctx.plannerMode === "native") {
909
1926
  try {
910
1927
  const result2 = await generateStructured(ctx.plannerModel, PlanSchema, commonFor("native"));
911
1928
  ctx.log.debug("planner (native):", result2.object);
912
1929
  return {
913
- plan: { thought: result2.object.thought, steps: toSteps(result2.object.steps) },
914
- usage: normalizeUsage(result2.usage)
1930
+ plan: { thought: result2.object.thought, steps: toSteps(result2.object.steps, cap) },
1931
+ usage: normalizeUsage(result2.usage),
1932
+ skills: validSkills(result2.object.skills)
915
1933
  };
916
1934
  } catch (err) {
1935
+ if (ctx.signal?.aborted) throw err;
1936
+ const refused = imageRefusal(ctx, err, ctx.plannerModel);
1937
+ if (refused) throw refused;
917
1938
  ctx.log.warn("planner: native structured output failed, salvaging:", asMessage(err));
918
1939
  ctx.emit({ type: "retry", phase: "plan", attempt: 1, error: asMessage(err) });
919
1940
  }
920
1941
  }
921
- const result = await generate(ctx.plannerModel, commonFor("prompted"));
1942
+ const result = await generate(ctx.plannerModel, commonFor("prompted")).catch((err) => {
1943
+ throw imageRefusal(ctx, err, ctx.plannerModel) ?? err;
1944
+ });
922
1945
  const parsed = parsePlannerResponse(result.text);
923
1946
  ctx.log.debug("planner (prompted) raw:", result.text);
924
1947
  ctx.log.debug("planner (prompted) parsed:", parsed);
925
1948
  return {
926
1949
  plan: {
927
1950
  thought: parsed.reply,
928
- steps: toSteps(parsed.plan.map((description) => ({ description })))
1951
+ steps: toSteps(
1952
+ parsed.plan.map((description) => ({ description })),
1953
+ cap
1954
+ )
929
1955
  },
930
- usage: normalizeUsage(result.usage)
1956
+ usage: normalizeUsage(result.usage),
1957
+ skills: validSkills(parsed.skills)
931
1958
  };
932
1959
  };
933
1960
  var asMessage = (err) => err instanceof Error ? err.message : String(err);
@@ -935,10 +1962,18 @@ var asMessage = (err) => err instanceof Error ? err.message : String(err);
935
1962
  // src/agent/replanner.ts
936
1963
  var decideReplan = async (ctx, goal, done, remaining) => {
937
1964
  const state = await ctx.state();
1965
+ const active = ctx.activeSkills?.() ?? [];
938
1966
  const commonFor = (mode) => {
939
- const parts = ctx.prompts.replanner({ goal, state, done, remaining, mode });
1967
+ const parts = ctx.prompts.replanner({
1968
+ goal,
1969
+ state,
1970
+ done,
1971
+ remaining,
1972
+ mode,
1973
+ activeSkills: active.length ? renderActiveSkills(active) : void 0
1974
+ });
940
1975
  return {
941
- system: systemFor(ctx, parts.system),
1976
+ ...stageCall(ctx, "replanner", parts.system),
942
1977
  prompt: parts.prompt,
943
1978
  maxOutputTokens: ctx.config.budgets.replanner,
944
1979
  temperature: ctx.config.temperature,
@@ -973,12 +2008,19 @@ var decideReplan = async (ctx, goal, done, remaining) => {
973
2008
  };
974
2009
 
975
2010
  // src/agent/synthesizer.ts
976
- var synthesizeAnswer = async (ctx, goal, done) => {
2011
+ var synthesizeAnswer = async (ctx, goal, done, findings = []) => {
977
2012
  const state = await ctx.state();
978
- const parts = ctx.prompts.synthesizer({ goal, state, done });
2013
+ const active = ctx.activeSkills?.() ?? [];
2014
+ const parts = ctx.prompts.synthesizer({
2015
+ goal,
2016
+ state,
2017
+ done,
2018
+ findings,
2019
+ activeSkills: active.length ? renderActiveSkills(active) : void 0
2020
+ });
979
2021
  const result = stream(ctx.synthesizerModel, {
980
- system: systemFor(ctx, parts.system),
981
- prompt: parts.prompt,
2022
+ ...stageCall(ctx, "synthesizer", parts.system),
2023
+ ...promptFor(ctx, parts.prompt),
982
2024
  maxOutputTokens: ctx.config.budgets.synthesizer,
983
2025
  temperature: ctx.config.temperature,
984
2026
  abortSignal: ctx.signal,
@@ -986,13 +2028,27 @@ var synthesizeAnswer = async (ctx, goal, done) => {
986
2028
  });
987
2029
  let text = "";
988
2030
  let verdict = "unknown";
989
- for await (const delta of result.textStream) {
2031
+ for await (const part of result.fullStream) {
2032
+ if (part.type === "reasoning-delta") {
2033
+ if (part.text) ctx.emit({ type: "final.reasoning-delta", delta: part.text });
2034
+ continue;
2035
+ }
2036
+ if (part.type === "error") {
2037
+ const err = part.error instanceof Error ? part.error : new Error(String(part.error));
2038
+ throw imageRefusal(ctx, err, ctx.synthesizerModel) ?? err;
2039
+ }
2040
+ if (part.type !== "text-delta") continue;
2041
+ const delta = part.text;
990
2042
  text += delta;
991
2043
  if (verdict === "unknown") {
992
2044
  const lead = text.trimStart();
993
2045
  if (!lead) continue;
2046
+ if (lead.startsWith("<think>") && !text.includes("</think>")) continue;
994
2047
  verdict = looksLikeJson(lead) ? "suppress" : "emit";
995
- if (verdict === "emit") ctx.emit({ type: "final.text-delta", delta: text });
2048
+ if (verdict === "emit") {
2049
+ const visible = text.includes("</think>") ? text.slice(text.lastIndexOf("</think>") + "</think>".length).trimStart() : text;
2050
+ if (visible) ctx.emit({ type: "final.text-delta", delta: visible });
2051
+ }
996
2052
  } else if (verdict === "emit") {
997
2053
  ctx.emit({ type: "final.text-delta", delta });
998
2054
  }
@@ -1025,6 +2081,43 @@ var createLogger = (level = "warn", sink = console) => {
1025
2081
  };
1026
2082
  };
1027
2083
 
2084
+ // src/providers/capabilities.ts
2085
+ var CLOUD = /* @__PURE__ */ new Set([
2086
+ "openai",
2087
+ "anthropic",
2088
+ "google",
2089
+ "openai-compatible",
2090
+ "xai",
2091
+ "deepseek",
2092
+ "gateway"
2093
+ ]);
2094
+ var supportsNativeTools = (p) => CLOUD.has(p);
2095
+ var supportsStructuredOutput = (p) => CLOUD.has(p);
2096
+ var directBrowserOk = (p) => p === "google" || p === "gateway" || p === "openai-compatible" || p === "anthropic";
2097
+ var LOCAL_RE2 = /web-?llm|mlc|browser-ai|transformers|built-?in/i;
2098
+ var TEXT_ONLY_MODEL_RE = /gpt-3\.5|o1-mini|o3-mini|deepseek|text-(davinci|embedding)|embedding|babbage|davinci|codestral/i;
2099
+ var VISION_PROVIDER_RE = /^(google|anthropic|openai|azure|xai|vertex|bedrock|gemini)/i;
2100
+ var PDF_PROVIDER_RE = /^(google|anthropic|openai|azure|vertex|bedrock|gemini)/i;
2101
+ var supportsImages = (model) => {
2102
+ if (typeof model === "string") {
2103
+ const [provider2 = "", id2 = model] = model.includes("/") ? model.split("/", 2) : ["", model];
2104
+ if (TEXT_ONLY_MODEL_RE.test(id2) || /deepseek/i.test(provider2)) return false;
2105
+ return VISION_PROVIDER_RE.test(provider2) ? true : void 0;
2106
+ }
2107
+ if (!model || typeof model !== "object") return void 0;
2108
+ const provider = String(model.provider ?? "");
2109
+ const id = String(model.modelId ?? "");
2110
+ if (LOCAL_RE2.test(provider)) return false;
2111
+ if (/deepseek/i.test(provider) || TEXT_ONLY_MODEL_RE.test(id)) return false;
2112
+ return VISION_PROVIDER_RE.test(provider) ? true : void 0;
2113
+ };
2114
+ var supportsPdf = (model) => {
2115
+ const vision = supportsImages(model);
2116
+ if (vision === false) return false;
2117
+ const provider = typeof model === "string" ? model.split("/")[0] ?? "" : String(model?.provider ?? "");
2118
+ return PDF_PROVIDER_RE.test(provider) ? true : void 0;
2119
+ };
2120
+
1028
2121
  // src/agent/runner.ts
1029
2122
  var errMessage = (err) => err instanceof Error ? err.message : String(err);
1030
2123
  var filterTools = (all, available, excluded) => {
@@ -1033,6 +2126,16 @@ var filterTools = (all, available, excluded) => {
1033
2126
  if (excluded && excluded.length > 0) entries = entries.filter(([n]) => !excluded.includes(n));
1034
2127
  return Object.fromEntries(entries);
1035
2128
  };
2129
+ var asJson2 = (v) => {
2130
+ if (typeof v === "string") return v;
2131
+ try {
2132
+ return JSON.stringify(v) ?? String(v);
2133
+ } catch {
2134
+ return String(v);
2135
+ }
2136
+ };
2137
+ var FINDING_CHARS = 600;
2138
+ var FINDINGS_BUDGET = 6e3;
1036
2139
  var createAgent = async (config) => {
1037
2140
  const cfg = resolveConfig(config);
1038
2141
  const buildStage = (override, stageName) => buildModelFromStage(
@@ -1048,16 +2151,52 @@ var createAgent = async (config) => {
1048
2151
  ]);
1049
2152
  const plannerMode = selectToolMode(plannerModel, cfg.toolMode);
1050
2153
  const executorMode = selectToolMode(executorModel, cfg.toolMode);
1051
- const tools = filterTools(
2154
+ const baseTools = filterTools(
1052
2155
  config.tools ?? {},
1053
2156
  config.availableTools,
1054
2157
  config.excludedTools
1055
2158
  );
1056
- const toolCatalog = renderCatalog(tools);
2159
+ const skills = (config.skills ?? []).map(defineSkill);
2160
+ const skillNames = /* @__PURE__ */ new Set();
2161
+ for (const s of skills) {
2162
+ if (skillNames.has(s.name)) throw new Error(`duplicate skill name "${s.name}"`);
2163
+ skillNames.add(s.name);
2164
+ }
2165
+ const catalog = toolCatalogOf(baseTools);
2166
+ const strategy = cfg.toolSelectionStrategy === "auto" ? catalog.length > cfg.toolSearchThreshold ? "search" : "all" : cfg.toolSelectionStrategy;
2167
+ const reserved = [
2168
+ ...skills.length ? SKILL_TOOL_NAMES : [],
2169
+ ...strategy === "search" ? [FIND_TOOLS_NAME] : []
2170
+ ];
2171
+ for (const name of reserved) {
2172
+ if (Object.hasOwn(baseTools, name)) {
2173
+ throw new Error(
2174
+ `tool name "${name}" is reserved by the agent's built-in tools; rename your tool`
2175
+ );
2176
+ }
2177
+ }
2178
+ const builtinTools = new Set(reserved);
2179
+ const hints = /* @__PURE__ */ new Map();
2180
+ await Promise.all(
2181
+ Object.entries(baseTools).map(async ([name, t]) => {
2182
+ const hint = await schemaHintOf(t);
2183
+ if (hint) hints.set(name, hint);
2184
+ })
2185
+ );
2186
+ const toolHint = (name) => hints.get(name);
2187
+ const plannerCatalog = strategy === "search" ? renderSearchCatalog(catalog) : renderCatalog(baseTools, toolHint);
1057
2188
  const prompts = { ...defaultPrompts, ...config.prompts };
2189
+ const caching = resolveCaching(config.promptCaching);
2190
+ const textOnly = executorMode === "prompted";
2191
+ const accepts = {
2192
+ image: config.inputs?.images ?? config.vision ?? (textOnly ? false : supportsImages(executorModel)),
2193
+ pdf: config.inputs?.pdf ?? (textOnly ? false : supportsPdf(executorModel)),
2194
+ file: config.inputs?.files ?? (textOnly ? false : void 0)
2195
+ };
2196
+ const approval = createApprovalState(config.toolApproval);
1058
2197
  const log = createLogger(cfg.logLevel, config.logger);
1059
2198
  log.info(
1060
- `agent ready \u2014 tools: [${Object.keys(tools).join(", ") || "none"}], planner mode: ${plannerMode}, executor mode: ${executorMode}`
2199
+ `agent ready \u2014 ${catalog.length} tool(s) (${strategy}), skills: [${[...skillNames].join(", ") || "none"}], planner mode: ${plannerMode}, executor mode: ${executorMode}, approval: ${approval.mode}`
1061
2200
  );
1062
2201
  const run = async (goal, opts = {}) => {
1063
2202
  const emit = (event) => {
@@ -1077,7 +2216,8 @@ var createAgent = async (config) => {
1077
2216
  steps: 0,
1078
2217
  applied: 0,
1079
2218
  stopped: false,
1080
- usage
2219
+ usage,
2220
+ skills: []
1081
2221
  };
1082
2222
  if (!text) return result;
1083
2223
  const state = async () => config.describeState ? config.describeState() : void 0;
@@ -1094,7 +2234,72 @@ var createAgent = async (config) => {
1094
2234
  } catch {
1095
2235
  }
1096
2236
  };
2237
+ const activeSkillNames = /* @__PURE__ */ new Set();
2238
+ const activateSkill = (name, by) => {
2239
+ if (activeSkillNames.has(name) || !skillNames.has(name)) return;
2240
+ activeSkillNames.add(name);
2241
+ result.skills = [...activeSkillNames];
2242
+ emit({ type: "skill.activated", name, by });
2243
+ };
2244
+ const discovered = [];
2245
+ let currentStep;
2246
+ const counter = createCallCounter(cfg.maxToolCalls);
2247
+ const builtins = {};
2248
+ if (skills.length) {
2249
+ Object.assign(
2250
+ builtins,
2251
+ createSkillTools(skills, (s) => activateSkill(s.name, "tool"))
2252
+ );
2253
+ }
2254
+ if (strategy === "search") {
2255
+ builtins[FIND_TOOLS_NAME] = createFindToolsTool(
2256
+ () => catalog,
2257
+ (query, names) => {
2258
+ for (const n of names) {
2259
+ const i = discovered.indexOf(n);
2260
+ if (i >= 0) discovered.splice(i, 1);
2261
+ discovered.push(n);
2262
+ }
2263
+ emit({ type: "tools.discovered", step: currentStep, query, names });
2264
+ },
2265
+ toolHint
2266
+ );
2267
+ }
2268
+ const tools = wrapToolsForRun(
2269
+ { ...baseTools, ...builtins },
2270
+ {
2271
+ approval,
2272
+ builtins: builtinTools,
2273
+ emit,
2274
+ step: () => currentStep,
2275
+ signal: opts.signal,
2276
+ addUsage: (u) => bumpUsage(u, "subagent"),
2277
+ countCall: counter.count,
2278
+ maxToolOutputChars: cfg.compaction.maxToolOutputChars
2279
+ }
2280
+ );
2281
+ let budgetExceeded;
2282
+ const breach = (extra) => {
2283
+ const b = checkLimits(extra ? addUsage(usage, extra) : usage, cfg.limits);
2284
+ if (b) return b;
2285
+ if (counter.exhausted)
2286
+ return { kind: "tool-calls", tokens: counter.calls, cap: cfg.maxToolCalls };
2287
+ return void 0;
2288
+ };
1097
2289
  emit({ type: "run.start", goal: text });
2290
+ const images = [...opts.images ?? [], ...opts.files ?? []];
2291
+ const refused = images.map(attachmentKind).find((kind) => accepts[kind] === false);
2292
+ if (refused) {
2293
+ const err = unsupportedAttachment(
2294
+ modelLabel(executorModel),
2295
+ refused,
2296
+ textOnly ? "it runs in the text-only prompted tool mode" : void 0
2297
+ );
2298
+ log.warn(err.message);
2299
+ emit({ type: "error", phase: "run", error: err.message });
2300
+ result.final = err.message;
2301
+ return result;
2302
+ }
1098
2303
  let history = [];
1099
2304
  if (config.memory) {
1100
2305
  try {
@@ -1102,6 +2307,30 @@ var createAgent = async (config) => {
1102
2307
  } catch {
1103
2308
  }
1104
2309
  }
2310
+ if (config.memory && config.compaction && cfg.compaction.auto && history.length > 0) {
2311
+ const before = messageTokens(history);
2312
+ if (before > cfg.compaction.thresholdTokens) {
2313
+ const compacted = await compressHistory(history, synthesizerModel, {
2314
+ thresholdTokens: cfg.compaction.thresholdTokens,
2315
+ keepRecent: cfg.compaction.keepRecentTurns,
2316
+ summaryMaxTokens: cfg.budgets.compaction,
2317
+ timeoutMs: cfg.chatTimeoutMs,
2318
+ abortSignal: opts.signal,
2319
+ onUsage: (u) => bumpUsage(u, "compact")
2320
+ });
2321
+ if (compacted !== history) {
2322
+ history = compacted;
2323
+ await config.memory.replace(sessionId, compacted).catch(() => {
2324
+ });
2325
+ emit({
2326
+ type: "context.compacted",
2327
+ scope: "history",
2328
+ beforeTokens: before,
2329
+ afterTokens: messageTokens(compacted)
2330
+ });
2331
+ }
2332
+ }
2333
+ }
1105
2334
  const ctx = {
1106
2335
  config: cfg,
1107
2336
  raw: config,
@@ -1111,24 +2340,42 @@ var createAgent = async (config) => {
1111
2340
  plannerMode,
1112
2341
  executorMode,
1113
2342
  tools,
1114
- toolCatalog,
2343
+ toolCatalog: plannerCatalog,
1115
2344
  prompts,
1116
2345
  emit,
1117
2346
  log,
1118
2347
  signal: opts.signal,
1119
2348
  state,
1120
- history
2349
+ history,
2350
+ strategy,
2351
+ caching,
2352
+ skills,
2353
+ activeSkills: () => skills.filter((s) => activeSkillNames.has(s.name)),
2354
+ discovered: () => discovered,
2355
+ builtinTools,
2356
+ toolHint,
2357
+ usageSoFar: () => usage,
2358
+ overBudget: (extra) => breach(extra) !== void 0,
2359
+ setCurrentStep: (step) => {
2360
+ currentStep = step;
2361
+ },
2362
+ images: images.length ? images : void 0
2363
+ };
2364
+ await remember({
2365
+ role: "user",
2366
+ content: images.length ? `${text}
2367
+ [attached: ${images.map((f, i) => f.name ?? `${attachmentKind(f)} ${i + 1}`).join(", ")}]` : text
2368
+ });
2369
+ log.info("run:", text, images.length ? `(+${images.length} image(s))` : "");
2370
+ const stop = () => {
2371
+ emit({ type: "stopped" });
2372
+ result.stopped = true;
2373
+ return result;
1121
2374
  };
1122
- await remember({ role: "user", content: text });
1123
- log.info("run:", text);
1124
2375
  try {
1125
2376
  let planned = await createPlan(ctx, text);
1126
2377
  bumpUsage(planned.usage, "plan");
1127
- if (isAborted()) {
1128
- emit({ type: "stopped" });
1129
- result.stopped = true;
1130
- return result;
1131
- }
2378
+ if (isAborted()) return stop();
1132
2379
  if (planned.plan.steps.length === 0 && text.split(/\s+/).length > 2) {
1133
2380
  log.warn(
1134
2381
  "planner returned no steps for a multi-word goal \u2014 retrying with a nudge. thought:",
@@ -1137,17 +2384,13 @@ var createAgent = async (config) => {
1137
2384
  const retried = await createPlan(
1138
2385
  ctx,
1139
2386
  text,
1140
- "NOTE: If the message above asks to build, add, change, clear or delete ANYTHING, you MUST output 1-6 concrete steps. Output an empty plan ONLY for a pure greeting or question."
2387
+ "NOTE: If the message above asks to build, add, change, clear, delete, find, check or look up ANYTHING the tools can do, you MUST output 1-6 concrete steps. Output an empty plan ONLY for a pure greeting or small talk."
1141
2388
  );
1142
2389
  bumpUsage(retried.usage, "plan");
1143
2390
  if (retried.plan.steps.length > 0) planned = retried;
1144
2391
  }
1145
2392
  result.plan = planned.plan;
1146
- if (isAborted()) {
1147
- emit({ type: "stopped" });
1148
- result.stopped = true;
1149
- return result;
1150
- }
2393
+ if (isAborted()) return stop();
1151
2394
  if (planned.plan.steps.length === 0) {
1152
2395
  log.info("no plan \u2014 answering directly (no tools will run)");
1153
2396
  const answer = planned.plan.thought.trim() || "Tell me what you'd like to do.";
@@ -1162,19 +2405,25 @@ var createAgent = async (config) => {
1162
2405
  );
1163
2406
  emit({ type: "plan.created", plan: planned.plan });
1164
2407
  planned.plan.steps.forEach((step, index) => emit({ type: "plan.step-added", step, index }));
1165
- const done = [];
2408
+ for (const name of planned.skills) activateSkill(name, "plan");
2409
+ let done = [];
2410
+ const findings = [];
1166
2411
  let remaining = [...planned.plan.steps];
1167
2412
  let iter = 0;
1168
2413
  let revisions = 0;
1169
2414
  while (remaining.length > 0 && iter < cfg.maxIterations) {
1170
- if (isAborted()) {
1171
- emit({ type: "stopped" });
1172
- result.stopped = true;
1173
- return result;
2415
+ if (isAborted()) return stop();
2416
+ const hit = breach();
2417
+ if (hit) {
2418
+ budgetExceeded = hit;
2419
+ result.budgetExceeded = hit;
2420
+ log.warn(`${hit.kind} limit reached (${hit.tokens}/${hit.cap}) \u2014 writing the answer now`);
2421
+ emit({ type: "budget.exceeded", ...hit });
2422
+ break;
1174
2423
  }
1175
2424
  iter += 1;
1176
2425
  const step = remaining.shift();
1177
- const stepNo = done.length + 1;
2426
+ const stepNo = result.trace.length + 1;
1178
2427
  const total = stepNo + remaining.length;
1179
2428
  emit({ type: "step.start", step, index: stepNo, total });
1180
2429
  let stepResult;
@@ -1183,13 +2432,12 @@ var createAgent = async (config) => {
1183
2432
  bumpUsage(out.usage, "execute");
1184
2433
  stepResult = out.result;
1185
2434
  } catch (err) {
1186
- if (isAborted()) {
1187
- emit({ type: "stopped" });
1188
- result.stopped = true;
1189
- return result;
1190
- }
2435
+ if (isAborted()) return stop();
2436
+ if (err instanceof AttachmentsNotSupportedError) throw err;
1191
2437
  emit({ type: "error", phase: "execute", error: errMessage(err) });
1192
2438
  stepResult = { step, summary: errMessage(err), toolCalls: [], blocked: true };
2439
+ } finally {
2440
+ currentStep = void 0;
1193
2441
  }
1194
2442
  result.trace.push(stepResult);
1195
2443
  const applied = stepResult.toolCalls.filter((c) => c.ok).length;
@@ -1205,10 +2453,38 @@ var createAgent = async (config) => {
1205
2453
  stepResult.toolCalls.filter((c) => !c.ok).forEach((c) => log.warn(`tool ${c.name} failed:`, c.output));
1206
2454
  log.debug(`step ${stepNo} detail:`, stepResult);
1207
2455
  emit({ type: "step.complete", step, result: stepResult });
2456
+ const counts = stepResult.toolCalls.length ? ` [${applied} tool call(s) ok${failed ? `, ${failed} failed` : ""}]` : "";
1208
2457
  done.push(
1209
- `${step.description} \u2014 ${applied} applied${failed ? `, ${failed} failed` : ""}${stepResult.blocked ? ", blocked" : ""}`
2458
+ `${step.description} \u2014 ${clip(stepResult.summary, FINDING_CHARS)}${counts}${stepResult.blocked ? " [blocked]" : ""}`
1210
2459
  );
1211
- const wantReplan = cfg.replan && iter < cfg.maxIterations && revisions < cfg.maxRevisions && !isAborted() && await replanWanted(cfg.replanAfter, stepResult, {
2460
+ for (const c of stepResult.toolCalls) {
2461
+ if (!c.ok || builtinTools.has(c.name)) continue;
2462
+ findings.push(`- ${c.name}: ${clip(asJson2(c.output), FINDING_CHARS)}`);
2463
+ }
2464
+ while (findings.join("\n").length > FINDINGS_BUDGET && findings.length > 1) findings.shift();
2465
+ if (cfg.compaction.auto) {
2466
+ const before = estimateTokens(done.join("\n"));
2467
+ if (before > cfg.compaction.thresholdTokens) {
2468
+ const compacted = await compactSteps(done, synthesizerModel, {
2469
+ thresholdTokens: cfg.compaction.thresholdTokens,
2470
+ keepRecent: cfg.compaction.keepRecentSteps,
2471
+ summaryMaxTokens: cfg.budgets.compaction,
2472
+ timeoutMs: cfg.chatTimeoutMs,
2473
+ abortSignal: opts.signal,
2474
+ onUsage: (u) => bumpUsage(u, "compact")
2475
+ });
2476
+ if (compacted !== done) {
2477
+ done = compacted;
2478
+ emit({
2479
+ type: "context.compacted",
2480
+ scope: "trace",
2481
+ beforeTokens: before,
2482
+ afterTokens: estimateTokens(done.join("\n"))
2483
+ });
2484
+ }
2485
+ }
2486
+ }
2487
+ const wantReplan = cfg.replan && iter < cfg.maxIterations && revisions < cfg.maxRevisions && !isAborted() && !breach() && await replanWanted(cfg.replanAfter, stepResult, {
1212
2488
  signal: ctx.signal,
1213
2489
  timeoutMs: cfg.chatTimeoutMs,
1214
2490
  onError: (err) => log.warn("replanAfter predicate threw \u2014 using the failure rule", err)
@@ -1225,7 +2501,7 @@ var createAgent = async (config) => {
1225
2501
  if (decision.decision === "finish") break;
1226
2502
  if (decision.decision === "revise" && decision.plan.length > 0) {
1227
2503
  revisions += 1;
1228
- remaining = decision.plan.slice(0, cfg.maxIterations - iter).map((description, i) => ({ id: `r${iter}-${i + 1}`, description }));
2504
+ remaining = decision.plan.slice(0, Math.min(cfg.maxPlanSteps, cfg.maxIterations - iter)).map((description, i) => ({ id: `r${iter}-${i + 1}`, description }));
1229
2505
  emit({
1230
2506
  type: "plan.revised",
1231
2507
  plan: { thought: planned.plan.thought, steps: remaining },
@@ -1234,62 +2510,352 @@ var createAgent = async (config) => {
1234
2510
  }
1235
2511
  }
1236
2512
  }
1237
- if (isAborted()) {
1238
- emit({ type: "stopped" });
1239
- result.stopped = true;
1240
- return result;
2513
+ if (!budgetExceeded && remaining.length > 0) {
2514
+ const hit = breach();
2515
+ if (hit) {
2516
+ budgetExceeded = hit;
2517
+ result.budgetExceeded = hit;
2518
+ emit({ type: "budget.exceeded", ...hit });
2519
+ }
1241
2520
  }
2521
+ if (isAborted()) return stop();
1242
2522
  let summary = "Done \u2014 the changes have been applied.";
1243
2523
  if (cfg.synthesize) {
1244
2524
  try {
1245
- const synth = await synthesizeAnswer(ctx, text, done);
2525
+ const synth = await synthesizeAnswer(ctx, text, done, findings);
1246
2526
  bumpUsage(synth.usage, "synthesize");
1247
2527
  if (synth.text) summary = synth.text;
1248
- } catch {
2528
+ } catch (err) {
2529
+ if (isAborted()) return stop();
2530
+ if (err instanceof AttachmentsNotSupportedError) throw err;
1249
2531
  }
1250
2532
  }
1251
2533
  log.info(`done \u2014 ${result.applied} tool call(s) applied over ${result.steps} step(s)`);
1252
2534
  emit({ type: "final", text: summary });
1253
2535
  await remember({ role: "assistant", content: summary });
1254
2536
  result.final = summary;
1255
- if (config.memory && cfg.compressAfterChars > 0) {
2537
+ if (config.memory && (config.compaction ? cfg.compaction.auto : cfg.compressAfterChars > 0)) {
1256
2538
  try {
1257
2539
  const transcript = await config.memory.load(sessionId);
2540
+ const before = messageTokens(transcript);
1258
2541
  const compacted = await compressHistory(transcript, synthesizerModel, {
1259
- maxChars: cfg.compressAfterChars,
2542
+ ...config.compaction ? {
2543
+ thresholdTokens: cfg.compaction.thresholdTokens,
2544
+ keepRecent: cfg.compaction.keepRecentTurns,
2545
+ summaryMaxTokens: cfg.budgets.compaction
2546
+ } : { maxChars: cfg.compressAfterChars },
1260
2547
  timeoutMs: cfg.chatTimeoutMs,
1261
- abortSignal: opts.signal
2548
+ abortSignal: opts.signal,
2549
+ onUsage: (u) => bumpUsage(u, "compact")
1262
2550
  });
1263
- if (compacted !== transcript) await config.memory.replace(sessionId, compacted);
2551
+ if (compacted !== transcript) {
2552
+ await config.memory.replace(sessionId, compacted);
2553
+ emit({
2554
+ type: "context.compacted",
2555
+ scope: "history",
2556
+ beforeTokens: before,
2557
+ afterTokens: messageTokens(compacted)
2558
+ });
2559
+ }
1264
2560
  } catch {
1265
2561
  }
1266
2562
  }
1267
2563
  return result;
1268
2564
  } catch (err) {
2565
+ if (isAborted()) return stop();
1269
2566
  emit({ type: "error", phase: "run", error: errMessage(err) });
1270
2567
  result.final = errMessage(err);
1271
2568
  return result;
1272
2569
  }
1273
2570
  };
2571
+ const compact = async (o = {}) => {
2572
+ const empty = {
2573
+ compacted: false,
2574
+ beforeTokens: 0,
2575
+ afterTokens: 0,
2576
+ usage: emptyUsage()
2577
+ };
2578
+ if (!config.memory) return empty;
2579
+ const sessionId = o.sessionId ?? cfg.sessionId;
2580
+ const transcript = await config.memory.load(sessionId);
2581
+ const beforeTokens = messageTokens(transcript);
2582
+ let usage = emptyUsage();
2583
+ const compacted = await compressHistory(transcript, synthesizerModel, {
2584
+ thresholdTokens: cfg.compaction.thresholdTokens,
2585
+ keepRecent: cfg.compaction.keepRecentTurns,
2586
+ summaryMaxTokens: cfg.budgets.compaction,
2587
+ force: o.force ?? true,
2588
+ timeoutMs: cfg.chatTimeoutMs,
2589
+ abortSignal: o.signal,
2590
+ onUsage: (u) => {
2591
+ usage = addUsage(usage, u);
2592
+ }
2593
+ });
2594
+ if (compacted === transcript)
2595
+ return { ...empty, beforeTokens, afterTokens: beforeTokens, usage };
2596
+ await config.memory.replace(sessionId, compacted);
2597
+ return { compacted: true, beforeTokens, afterTokens: messageTokens(compacted), usage };
2598
+ };
1274
2599
  return {
1275
2600
  run,
2601
+ compact,
2602
+ setToolApprovalMode: (mode) => {
2603
+ if (!TOOL_APPROVAL_MODES.includes(mode)) {
2604
+ throw new Error(`unknown tool approval mode "${mode}" (${TOOL_APPROVAL_MODES.join(" | ")})`);
2605
+ }
2606
+ approval.mode = mode;
2607
+ log.info(`tool approval mode \u2192 ${mode}`);
2608
+ },
2609
+ get toolApprovalMode() {
2610
+ return approval.mode;
2611
+ },
2612
+ listTools: () => catalog.map((e) => ({ ...e })),
2613
+ toolStrategy: strategy,
2614
+ skills: skills.map((s) => ({ name: s.name, description: s.description })),
2615
+ capabilities: { images: accepts.image, pdf: accepts.pdf, files: accepts.file },
1276
2616
  models: { planner: plannerModel, executor: executorModel, synthesizer: synthesizerModel }
1277
2617
  };
1278
2618
  };
1279
2619
 
1280
- // src/providers/capabilities.ts
1281
- var CLOUD = /* @__PURE__ */ new Set([
1282
- "openai",
1283
- "anthropic",
1284
- "google",
1285
- "openai-compatible",
1286
- "xai",
1287
- "deepseek",
1288
- "gateway"
1289
- ]);
1290
- var supportsNativeTools = (p) => CLOUD.has(p);
1291
- var supportsStructuredOutput = (p) => CLOUD.has(p);
1292
- var directBrowserOk = (p) => p === "google" || p === "gateway" || p === "openai-compatible" || p === "anthropic";
2620
+ // src/subagent/tool.ts
2621
+ import { asSchema as asSchema2, jsonSchema as jsonSchema3, tool as tool3 } from "ai";
2622
+
2623
+ // src/subagent/protocol.ts
2624
+ var toCloneable = (value) => {
2625
+ try {
2626
+ return JSON.parse(
2627
+ JSON.stringify(value, (_k, v) => v instanceof Error ? v.message : v) ?? "null"
2628
+ );
2629
+ } catch {
2630
+ return String(value);
2631
+ }
2632
+ };
2633
+ var safePost = (target, message) => {
2634
+ try {
2635
+ target.postMessage(message);
2636
+ } catch {
2637
+ target.postMessage(toCloneable(message));
2638
+ }
2639
+ };
2640
+
2641
+ // src/subagent/tool.ts
2642
+ var abortError = () => {
2643
+ const e = new Error("The subagent was aborted");
2644
+ e.name = "AbortError";
2645
+ return e;
2646
+ };
2647
+ var createSemaphore = (size) => {
2648
+ let active = 0;
2649
+ const queue = [];
2650
+ return async (signal) => {
2651
+ if (active >= size) {
2652
+ await new Promise((resolve, reject) => {
2653
+ const go = () => {
2654
+ signal?.removeEventListener("abort", onAbort);
2655
+ resolve();
2656
+ };
2657
+ const onAbort = () => {
2658
+ const i = queue.indexOf(go);
2659
+ if (i >= 0) queue.splice(i, 1);
2660
+ reject(abortError());
2661
+ };
2662
+ queue.push(go);
2663
+ signal?.addEventListener("abort", onAbort, { once: true });
2664
+ });
2665
+ }
2666
+ active += 1;
2667
+ let released = false;
2668
+ return () => {
2669
+ if (released) return;
2670
+ released = true;
2671
+ active -= 1;
2672
+ queue.shift()?.();
2673
+ };
2674
+ };
2675
+ };
2676
+ var seq2 = 0;
2677
+ var createSubagentTool = (opts) => {
2678
+ if (!opts.config === !opts.worker) {
2679
+ throw new Error(
2680
+ `subagent "${opts.name}": pass exactly one of \`config\` (in-process) or \`worker\``
2681
+ );
2682
+ }
2683
+ if (opts.worker && !opts.workerConfig) {
2684
+ throw new Error(`subagent "${opts.name}": a worker subagent needs \`workerConfig\``);
2685
+ }
2686
+ const acquire = createSemaphore(Math.max(1, opts.maxConcurrent ?? 4));
2687
+ const outputMax = opts.outputMaxChars ?? 8e3;
2688
+ const gatedHostTools = (approve) => Object.fromEntries(
2689
+ Object.entries(opts.tools ?? {}).map(([name, t2]) => {
2690
+ const execute = t2.execute;
2691
+ if (typeof execute !== "function" || !approve) return [name, t2];
2692
+ return [
2693
+ name,
2694
+ {
2695
+ ...t2,
2696
+ execute: async (input, o) => {
2697
+ await approve(name, input, isReadOnlyTool(t2));
2698
+ return execute.call(t2, input, o);
2699
+ }
2700
+ }
2701
+ ];
2702
+ })
2703
+ );
2704
+ const runInProcess = async (task, signal, forward, hostTools) => {
2705
+ const config = opts.config;
2706
+ const agent = await createAgent({ ...config, tools: { ...config.tools, ...hostTools } });
2707
+ let runError;
2708
+ const result = await agent.run(task, {
2709
+ signal,
2710
+ onEvent: (e) => {
2711
+ if (e.type === "error" && e.phase === "run") runError = e.error;
2712
+ forward(e);
2713
+ }
2714
+ });
2715
+ if (result.stopped) throw abortError();
2716
+ if (runError) throw new Error(runError);
2717
+ return { text: result.final, usage: result.usage };
2718
+ };
2719
+ const proxiedSpecs = async () => Promise.all(
2720
+ Object.entries(opts.tools ?? {}).map(async ([name, t2]) => ({
2721
+ name,
2722
+ description: typeof t2.description === "string" ? t2.description : "",
2723
+ inputSchema: toCloneable(await asSchema2(t2.inputSchema).jsonSchema),
2724
+ readOnly: isReadOnlyTool(t2)
2725
+ }))
2726
+ );
2727
+ const workerConfigFor = async () => {
2728
+ const cfg = opts.workerConfig;
2729
+ let model = { ...cfg.model };
2730
+ if (model.credentialRef && !model.apiKey && opts.credentials) {
2731
+ const apiKey = await opts.credentials.getApiKey(model.credentialRef);
2732
+ if (!apiKey)
2733
+ throw new Error(`no API key stored under "${model.credentialRef}" for the subagent`);
2734
+ model = { ...model, apiKey };
2735
+ }
2736
+ return toCloneable({ ...cfg, model });
2737
+ };
2738
+ const runInWorker = async (task, signal, forward, hostTools) => {
2739
+ if (signal?.aborted) throw abortError();
2740
+ const [config, proxied] = await Promise.all([workerConfigFor(), proxiedSpecs()]);
2741
+ const worker = opts.worker();
2742
+ return new Promise((resolve, reject) => {
2743
+ let settled = false;
2744
+ const finish = (fn) => {
2745
+ if (settled) return;
2746
+ settled = true;
2747
+ signal?.removeEventListener("abort", onAbort);
2748
+ worker.removeEventListener?.("message", onMessage);
2749
+ worker.removeEventListener?.("error", onError);
2750
+ worker.terminate?.();
2751
+ fn();
2752
+ };
2753
+ const onAbort = () => {
2754
+ safePost(worker, { type: "abort" });
2755
+ finish(() => reject(abortError()));
2756
+ };
2757
+ const onError = (ev) => finish(() => reject(new Error(`subagent worker failed: ${ev?.message ?? "unknown error"}`)));
2758
+ const callHostTool = async (callId, name, input) => {
2759
+ const t2 = hostTools[name];
2760
+ try {
2761
+ if (!t2 || typeof t2.execute !== "function") throw new Error(`unknown host tool "${name}"`);
2762
+ const output = await t2.execute(input, {
2763
+ toolCallId: callId,
2764
+ messages: [],
2765
+ abortSignal: signal
2766
+ });
2767
+ if (!settled) safePost(worker, { type: "tool-result", callId, ok: true, output });
2768
+ } catch (err) {
2769
+ const output = err instanceof Error ? err.message : String(err);
2770
+ if (!settled) safePost(worker, { type: "tool-result", callId, ok: false, output });
2771
+ }
2772
+ };
2773
+ const onMessage = (ev) => {
2774
+ const msg = ev.data;
2775
+ switch (msg?.type) {
2776
+ case "event":
2777
+ forward(msg.event);
2778
+ break;
2779
+ case "tool-call":
2780
+ void callHostTool(msg.callId, msg.name, msg.input);
2781
+ break;
2782
+ case "done":
2783
+ finish(() => resolve({ text: msg.text, usage: msg.usage }));
2784
+ break;
2785
+ case "error":
2786
+ finish(() => reject(new Error(msg.error)));
2787
+ break;
2788
+ }
2789
+ };
2790
+ worker.addEventListener("message", onMessage);
2791
+ worker.addEventListener("error", onError);
2792
+ signal?.addEventListener("abort", onAbort, { once: true });
2793
+ safePost(worker, { type: "run", task, config, proxied });
2794
+ });
2795
+ };
2796
+ const t = tool3({
2797
+ description: opts.description,
2798
+ inputSchema: jsonSchema3({
2799
+ type: "object",
2800
+ properties: {
2801
+ task: {
2802
+ type: "string",
2803
+ description: "A complete, self-contained task: the subagent sees nothing of your context except this text."
2804
+ }
2805
+ },
2806
+ required: ["task"],
2807
+ additionalProperties: false
2808
+ }),
2809
+ execute: async ({ task }, options) => {
2810
+ const runCtx = toolRunContextOf(options);
2811
+ const parentSignal = options?.abortSignal ?? runCtx?.signal;
2812
+ const id = `${opts.name}-${seq2 += 1}`;
2813
+ const release = await acquire(parentSignal);
2814
+ const emit = (e) => runCtx?.emit(e);
2815
+ const controller = new AbortController();
2816
+ const onParentAbort = () => controller.abort();
2817
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
2818
+ if (parentSignal?.aborted) controller.abort();
2819
+ let timedOut = false;
2820
+ const timer = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
2821
+ timedOut = true;
2822
+ controller.abort();
2823
+ }, opts.timeoutMs) : void 0;
2824
+ emit({ type: "subagent.start", id, name: opts.name, task });
2825
+ try {
2826
+ const forward = (event) => emit({ type: "subagent.event", id, name: opts.name, event });
2827
+ const hostTools = gatedHostTools(runCtx?.approve);
2828
+ const run = opts.worker ? runInWorker : runInProcess;
2829
+ const out = await run(task, controller.signal, forward, hostTools).catch((err) => {
2830
+ if (timedOut)
2831
+ throw new Error(`subagent "${opts.name}" timed out after ${opts.timeoutMs} ms`);
2832
+ throw err;
2833
+ });
2834
+ runCtx?.addUsage(out.usage);
2835
+ emit({ type: "subagent.complete", id, name: opts.name, text: out.text, usage: out.usage });
2836
+ return clip(out.text, outputMax);
2837
+ } catch (err) {
2838
+ emit({
2839
+ type: "subagent.error",
2840
+ id,
2841
+ name: opts.name,
2842
+ error: err instanceof Error ? err.message : String(err)
2843
+ });
2844
+ throw err;
2845
+ } finally {
2846
+ if (timer !== void 0) clearTimeout(timer);
2847
+ parentSignal?.removeEventListener("abort", onParentAbort);
2848
+ release();
2849
+ }
2850
+ }
2851
+ });
2852
+ t.promptHint = "{ task: string }";
2853
+ if (opts.readOnly) t.readOnly = true;
2854
+ return t;
2855
+ };
2856
+
2857
+ // src/subagent/worker.ts
2858
+ import { dynamicTool, jsonSchema as jsonSchema4 } from "ai";
1293
2859
 
1294
2860
  // src/secrets/store.ts
1295
2861
  var VaultCredentialStore = class {
@@ -1323,12 +2889,333 @@ var MemoryCredentialStore = class {
1323
2889
  }
1324
2890
  };
1325
2891
 
2892
+ // src/subagent/worker.ts
2893
+ var SUBAGENT_REF = "__subagent__";
2894
+ var defaultResolveModel = (spec) => {
2895
+ const { apiKey, ...rest } = spec;
2896
+ const credentials = apiKey ? new MemoryCredentialStore({ [SUBAGENT_REF]: apiKey }) : void 0;
2897
+ return buildModelFromStage(
2898
+ resolveStage(apiKey ? { ...rest, credentialRef: SUBAGENT_REF } : rest, void 0, "subagent"),
2899
+ credentials
2900
+ );
2901
+ };
2902
+ var serveSubagentWorker = (opts = {}) => {
2903
+ const scope = opts.scope ?? globalThis;
2904
+ let controller;
2905
+ const pending = /* @__PURE__ */ new Map();
2906
+ let callSeq = 0;
2907
+ const proxy = (spec) => {
2908
+ const t = dynamicTool({
2909
+ description: spec.description,
2910
+ inputSchema: jsonSchema4(spec.inputSchema),
2911
+ execute: (input, options) => new Promise((resolve, reject) => {
2912
+ const callId = `call-${callSeq += 1}`;
2913
+ const signal = options?.abortSignal;
2914
+ if (signal?.aborted) return reject(new Error("aborted"));
2915
+ pending.set(callId, { resolve, reject });
2916
+ signal?.addEventListener(
2917
+ "abort",
2918
+ () => {
2919
+ if (pending.delete(callId)) reject(new Error("aborted"));
2920
+ },
2921
+ { once: true }
2922
+ );
2923
+ safePost(scope, { type: "tool-call", callId, name: spec.name, input });
2924
+ })
2925
+ });
2926
+ return spec.readOnly ? markReadOnly(t) : t;
2927
+ };
2928
+ const run = async (msg) => {
2929
+ controller = new AbortController();
2930
+ let runError;
2931
+ try {
2932
+ const { model: spec, ...rest } = msg.config;
2933
+ const model = await (opts.resolveModel ?? defaultResolveModel)(spec);
2934
+ const proxied = Object.fromEntries(msg.proxied.map((p) => [p.name, proxy(p)]));
2935
+ const agent = await createAgent({
2936
+ ...rest,
2937
+ model,
2938
+ tools: { ...opts.tools, ...proxied }
2939
+ });
2940
+ const result = await agent.run(msg.task, {
2941
+ signal: controller.signal,
2942
+ onEvent: (event) => {
2943
+ if (event.type === "error" && event.phase === "run") runError = event.error;
2944
+ safePost(scope, { type: "event", event });
2945
+ }
2946
+ });
2947
+ if (result.stopped) safePost(scope, { type: "error", error: "aborted" });
2948
+ else if (runError) safePost(scope, { type: "error", error: runError });
2949
+ else
2950
+ safePost(scope, {
2951
+ type: "done",
2952
+ text: result.final,
2953
+ usage: result.usage,
2954
+ steps: result.steps
2955
+ });
2956
+ } catch (err) {
2957
+ safePost(scope, { type: "error", error: err instanceof Error ? err.message : String(err) });
2958
+ }
2959
+ };
2960
+ scope.addEventListener("message", (ev) => {
2961
+ const msg = ev.data;
2962
+ switch (msg?.type) {
2963
+ case "run":
2964
+ void run(msg);
2965
+ break;
2966
+ case "abort":
2967
+ controller?.abort();
2968
+ for (const [id, p] of pending) {
2969
+ pending.delete(id);
2970
+ p.reject(new Error("aborted"));
2971
+ }
2972
+ break;
2973
+ case "tool-result": {
2974
+ const p = pending.get(msg.callId);
2975
+ if (!p) break;
2976
+ pending.delete(msg.callId);
2977
+ if (msg.ok) p.resolve(msg.output);
2978
+ else
2979
+ p.reject(
2980
+ new Error(typeof msg.output === "string" ? msg.output : JSON.stringify(msg.output))
2981
+ );
2982
+ break;
2983
+ }
2984
+ }
2985
+ });
2986
+ };
2987
+
2988
+ // src/files/vfs.ts
2989
+ var MIME = {
2990
+ txt: "text/plain",
2991
+ md: "text/markdown",
2992
+ json: "application/json",
2993
+ csv: "text/csv",
2994
+ html: "text/html",
2995
+ css: "text/css",
2996
+ js: "text/javascript",
2997
+ ts: "text/typescript",
2998
+ svg: "image/svg+xml",
2999
+ png: "image/png",
3000
+ jpg: "image/jpeg",
3001
+ jpeg: "image/jpeg",
3002
+ gif: "image/gif",
3003
+ webp: "image/webp",
3004
+ pdf: "application/pdf",
3005
+ xml: "application/xml",
3006
+ yaml: "text/yaml",
3007
+ yml: "text/yaml"
3008
+ };
3009
+ var mimeFromPath = (path) => MIME[path.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream";
3010
+ var isTextMime = (mime) => /^text\/|json|xml|yaml|javascript|typescript|svg/.test(mime);
3011
+ var normalizePath = (input) => {
3012
+ const parts = [];
3013
+ for (const seg of String(input ?? "").replace(/\\/g, "/").split("/")) {
3014
+ if (!seg || seg === ".") continue;
3015
+ if (seg === "..") {
3016
+ if (parts.length === 0) throw new Error(`path "${input}" escapes the root`);
3017
+ parts.pop();
3018
+ continue;
3019
+ }
3020
+ parts.push(seg);
3021
+ }
3022
+ if (parts.length === 0) throw new Error("a file path is required");
3023
+ return `/${parts.join("/")}`;
3024
+ };
3025
+ var byteLength = (content, encoding) => encoding === "base64" ? Math.floor(content.replace(/=+$/, "").length * 3 / 4) : new TextEncoder().encode(content).length;
3026
+ var bytesToBase64 = (bytes) => {
3027
+ let bin = "";
3028
+ for (let i = 0; i < bytes.length; i += 32768) {
3029
+ bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
3030
+ }
3031
+ return btoa(bin);
3032
+ };
3033
+ var VirtualFileSystem = class {
3034
+ memory;
3035
+ dbName;
3036
+ ns;
3037
+ maxFileBytes;
3038
+ listeners = /* @__PURE__ */ new Set();
3039
+ constructor(opts = {}) {
3040
+ if (opts.memory) this.memory = /* @__PURE__ */ new Map();
3041
+ this.dbName = opts.dbName;
3042
+ this.ns = opts.namespace ?? "default";
3043
+ this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
3044
+ }
3045
+ key(path) {
3046
+ return `${this.ns}:${path}`;
3047
+ }
3048
+ /** Subscribe to writes and deletes (e.g. to refresh a file list). Returns an unsubscribe. */
3049
+ onChange(listener) {
3050
+ this.listeners.add(listener);
3051
+ return () => this.listeners.delete(listener);
3052
+ }
3053
+ notify(type, path) {
3054
+ for (const l of this.listeners) {
3055
+ try {
3056
+ l({ type, path });
3057
+ } catch {
3058
+ }
3059
+ }
3060
+ }
3061
+ async write(path, content, opts = {}) {
3062
+ const p = normalizePath(path);
3063
+ const binary = content instanceof Uint8Array;
3064
+ const encoding = binary ? "base64" : opts.encoding ?? "utf8";
3065
+ const text = binary ? bytesToBase64(content) : content;
3066
+ const size = binary ? content.length : byteLength(text, encoding);
3067
+ if (size > this.maxFileBytes) {
3068
+ throw new Error(`"${p}" is ${size} bytes; the limit is ${this.maxFileBytes}`);
3069
+ }
3070
+ const file = {
3071
+ path: p,
3072
+ content: text,
3073
+ encoding,
3074
+ mimeType: opts.mimeType ?? mimeFromPath(p),
3075
+ size,
3076
+ updatedAt: Date.now()
3077
+ };
3078
+ if (this.memory) this.memory.set(p, file);
3079
+ else {
3080
+ const db = await openAgentWebDB({ dbName: this.dbName });
3081
+ await db.put(FILES_STORE, file, this.key(p));
3082
+ }
3083
+ this.notify("write", p);
3084
+ const { content: _drop, ...info } = file;
3085
+ return info;
3086
+ }
3087
+ /** Store a data URL ("data:image/png;base64,…") as a file. */
3088
+ async writeDataUrl(path, dataUrl) {
3089
+ const m = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(dataUrl);
3090
+ if (!m) throw new Error("not a data URL");
3091
+ const mimeType = m[1] ?? "application/octet-stream";
3092
+ return m[2] ? this.write(path, m[3], { mimeType, encoding: "base64" }) : this.write(path, decodeURIComponent(m[3]), { mimeType });
3093
+ }
3094
+ async read(path) {
3095
+ const p = normalizePath(path);
3096
+ if (this.memory) return this.memory.get(p);
3097
+ const db = await openAgentWebDB({ dbName: this.dbName });
3098
+ return await db.get(FILES_STORE, this.key(p));
3099
+ }
3100
+ /** The file as a data URL (for <img src>, downloads). */
3101
+ async readDataUrl(path) {
3102
+ const f = await this.read(path);
3103
+ if (!f) return void 0;
3104
+ return f.encoding === "base64" ? `data:${f.mimeType};base64,${f.content}` : `data:${f.mimeType};charset=utf-8,${encodeURIComponent(f.content)}`;
3105
+ }
3106
+ async delete(path) {
3107
+ const p = normalizePath(path);
3108
+ let existed;
3109
+ if (this.memory) existed = this.memory.delete(p);
3110
+ else {
3111
+ const db = await openAgentWebDB({ dbName: this.dbName });
3112
+ existed = await db.get(FILES_STORE, this.key(p)) !== void 0;
3113
+ await db.delete(FILES_STORE, this.key(p));
3114
+ }
3115
+ if (existed) this.notify("delete", p);
3116
+ return existed;
3117
+ }
3118
+ /** Files under a prefix ("/" = all), sorted by path. */
3119
+ async list(prefix = "/") {
3120
+ const dir = prefix === "/" ? "/" : `${normalizePath(prefix)}/`;
3121
+ let files;
3122
+ if (this.memory) files = [...this.memory.values()];
3123
+ else {
3124
+ const db = await openAgentWebDB({ dbName: this.dbName });
3125
+ const range = IDBKeyRange.bound(`${this.ns}:`, `${this.ns}:\uFFFF`);
3126
+ files = await db.getAll(FILES_STORE, range);
3127
+ }
3128
+ return files.filter((f) => dir === "/" || f.path.startsWith(dir)).map(({ content: _drop, ...info }) => info).sort((a, b) => a.path.localeCompare(b.path));
3129
+ }
3130
+ /** Remove every file (of this namespace). */
3131
+ async clear() {
3132
+ for (const f of await this.list()) await this.delete(f.path);
3133
+ }
3134
+ };
3135
+
3136
+ // src/files/tools.ts
3137
+ import { jsonSchema as jsonSchema5, tool as tool4 } from "ai";
3138
+ var createFileTools = (vfs, opts = {}) => {
3139
+ const p = opts.prefix ?? "fs_";
3140
+ const maxRead = opts.maxReadChars ?? 5e4;
3141
+ const tools = {
3142
+ [`${p}list`]: markReadOnly(
3143
+ tool4({
3144
+ description: "List the files in the workspace (attachments the user added, files you wrote). Optionally only under a directory prefix.",
3145
+ inputSchema: jsonSchema5({
3146
+ type: "object",
3147
+ properties: { prefix: { type: "string", description: 'Directory prefix, e.g. "/docs"' } },
3148
+ additionalProperties: false
3149
+ }),
3150
+ execute: async ({ prefix }) => (await vfs.list(prefix || "/")).map((f) => ({
3151
+ path: f.path,
3152
+ mimeType: f.mimeType,
3153
+ size: f.size
3154
+ }))
3155
+ })
3156
+ ),
3157
+ [`${p}read`]: markReadOnly(
3158
+ tool4({
3159
+ description: "Read a text file from the workspace by its path.",
3160
+ inputSchema: jsonSchema5({
3161
+ type: "object",
3162
+ properties: { path: { type: "string", description: 'Absolute path, e.g. "/notes.md"' } },
3163
+ required: ["path"],
3164
+ additionalProperties: false
3165
+ }),
3166
+ execute: async ({ path }) => {
3167
+ const f = await vfs.read(path);
3168
+ if (!f) {
3169
+ const known = (await vfs.list()).map((x) => x.path).slice(0, 30);
3170
+ throw new Error(`no file "${path}"; files: ${known.join(", ") || "none"}`);
3171
+ }
3172
+ if (f.encoding === "base64" && !isTextMime(f.mimeType)) {
3173
+ return `"${f.path}" is a binary file (${f.mimeType}, ${f.size} bytes) and cannot be read as text.`;
3174
+ }
3175
+ const text = f.encoding === "base64" ? atob(f.content) : f.content;
3176
+ return text.length > maxRead ? `${text.slice(0, maxRead)}\u2026 [truncated ${text.length - maxRead} chars]` : text;
3177
+ }
3178
+ })
3179
+ )
3180
+ };
3181
+ if (opts.readOnly) return tools;
3182
+ tools[`${p}write`] = tool4({
3183
+ description: "Write a text file to the workspace (creates or replaces it). Use it for reports, drafts, data the user should be able to open.",
3184
+ inputSchema: jsonSchema5({
3185
+ type: "object",
3186
+ properties: {
3187
+ path: { type: "string", description: 'Absolute path, e.g. "/reports/summary.md"' },
3188
+ content: { type: "string", description: "The full file content" },
3189
+ mimeType: { type: "string", description: "Optional MIME type (guessed from the name)" }
3190
+ },
3191
+ required: ["path", "content"],
3192
+ additionalProperties: false
3193
+ }),
3194
+ execute: async ({ path, content, mimeType }) => {
3195
+ const info = await vfs.write(path, content, mimeType ? { mimeType } : {});
3196
+ return { written: info.path, size: info.size };
3197
+ }
3198
+ });
3199
+ tools[`${p}delete`] = tool4({
3200
+ description: "Delete a file from the workspace.",
3201
+ inputSchema: jsonSchema5({
3202
+ type: "object",
3203
+ properties: { path: { type: "string" } },
3204
+ required: ["path"],
3205
+ additionalProperties: false
3206
+ }),
3207
+ execute: async ({ path }) => ({ deleted: await vfs.delete(path) })
3208
+ });
3209
+ return tools;
3210
+ };
3211
+
1326
3212
  // src/tools/define.ts
1327
- import { tool } from "ai";
3213
+ import { tool as tool5 } from "ai";
1328
3214
  var defineTool = (config) => {
1329
- const { promptHint, ...rest } = config;
1330
- const t = tool(rest);
3215
+ const { promptHint, readOnly, ...rest } = config;
3216
+ const t = tool5(rest);
1331
3217
  if (promptHint) t.promptHint = promptHint;
3218
+ if (readOnly) t.readOnly = true;
1332
3219
  return t;
1333
3220
  };
1334
3221
 
@@ -1382,6 +3269,10 @@ var IndexedDBStore = class {
1382
3269
  }
1383
3270
  };
1384
3271
  export {
3272
+ AttachmentsNotSupportedError,
3273
+ FILES_STORE,
3274
+ FIND_TOOLS_NAME,
3275
+ ImagesNotSupportedError,
1385
3276
  IndexedDBStore,
1386
3277
  IndexedDBVault,
1387
3278
  KEYS_STORE,
@@ -1392,28 +3283,59 @@ export {
1392
3283
  ReplanSchema,
1393
3284
  SECRETS_STORE,
1394
3285
  SESSIONS_STORE,
3286
+ SKILL_TOOL_NAMES,
3287
+ TOOL_APPROVAL_MODES,
3288
+ ToolBudgetError,
3289
+ ToolDeniedError,
1395
3290
  VaultCredentialStore,
3291
+ VirtualFileSystem,
3292
+ attachmentKind,
1396
3293
  buildModelFromStage,
3294
+ bytesToBase64,
3295
+ checkLimits,
3296
+ compactHistory,
3297
+ compactSteps,
1397
3298
  compressHistory,
1398
3299
  createAgent,
3300
+ createFileTools,
3301
+ createFindToolsTool,
1399
3302
  createLogger,
1400
3303
  createPlan,
3304
+ createSkillTools,
3305
+ createSubagentTool,
3306
+ createToolResultClearer,
1401
3307
  createWebLLMModel,
1402
3308
  decideReplan,
3309
+ decideToolPermission,
1403
3310
  decryptJSON,
1404
3311
  defaultPrompts,
3312
+ defineSkill,
1405
3313
  defineTool,
1406
3314
  directBrowserOk,
1407
3315
  dispatch,
1408
3316
  encryptJSON,
3317
+ messageTokens as estimateMessagesTokens,
3318
+ estimateTokens,
1409
3319
  executeStep,
1410
3320
  generate,
1411
3321
  generateStructured,
1412
3322
  getOrCreateVaultKey,
3323
+ isAttachmentRefusal,
1413
3324
  isDirectModel,
3325
+ isImageRefusal,
1414
3326
  isProviderSpec,
3327
+ isReadOnlyTool,
3328
+ isTextMime,
1415
3329
  isWebGPUAvailable,
3330
+ limitModelOutput,
3331
+ loadSkillFromUrl,
1416
3332
  looksLikeJson,
3333
+ markReadOnly,
3334
+ matchRule,
3335
+ mediaTypeOf,
3336
+ mergeProviderOptions,
3337
+ mimeFromPath,
3338
+ normalizePath,
1417
3339
  normalizeSteps,
1418
3340
  normalizeUsage,
1419
3341
  openAgentWebDB,
@@ -1421,22 +3343,41 @@ export {
1421
3343
  parsePlainText,
1422
3344
  parsePlannerResponse,
1423
3345
  parseReplannerResponse,
3346
+ parseSkillMarkdown,
1424
3347
  preloadWebLLMModel,
1425
3348
  promptHintOf,
3349
+ renderActiveSkills,
1426
3350
  renderCatalog,
3351
+ renderSearchCatalog,
3352
+ renderSkillIndex,
1427
3353
  replanWanted,
3354
+ resolveCaching,
3355
+ resolveCompaction,
1428
3356
  resolveConfig,
1429
3357
  resolveModel,
1430
3358
  resolveStage,
3359
+ resolveThinking,
1431
3360
  runToolLoop,
3361
+ schemaHintOf,
3362
+ searchTools,
1432
3363
  selectToolMode,
3364
+ serveSubagentWorker,
1433
3365
  shouldReplan,
1434
3366
  splitBlocker,
1435
3367
  stream,
3368
+ supportsImages,
1436
3369
  supportsNativeTools,
3370
+ supportsPdf,
1437
3371
  supportsStructuredOutput,
1438
3372
  synthesizeAnswer,
3373
+ thinkingFor,
3374
+ toFilePart,
3375
+ toImagePart,
3376
+ tokenize,
3377
+ toolCatalogOf,
3378
+ toolRunContextOf,
1439
3379
  unloadWebLLMModel,
3380
+ withRollingBreakpoint,
1440
3381
  withSystem
1441
3382
  };
1442
3383
  //# sourceMappingURL=index.js.map