anyclaude-sdk 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.d.ts CHANGED
@@ -26,6 +26,11 @@ export interface AgentOptions {
26
26
  allowedTools?: string[];
27
27
  /** Denylist of tool names, applied after allowedTools. */
28
28
  disallowedTools?: string[];
29
+ /** Tool names to DEFER out of the per-turn payload — still discoverable via
30
+ * `tool_search` and executable, but their schema isn't sent (saving tokens)
31
+ * until the model searches and the loop arms them. For large pools of
32
+ * rarely-used integration tools. (Per-tool `defer: true` works too.) */
33
+ deferredTools?: string[];
29
34
  maxTurns?: number;
30
35
  /** Wall-clock budget (ms). At a turn boundary past this, the loop pauses: it
31
36
  * persists to sessionStore and emits a `paused` system message instead of
package/dist/agent.js CHANGED
@@ -25,6 +25,7 @@ import { DEFAULT_MAX_RESULT_CHARS, maybePersistLargeResult } from './persist.js'
25
25
  import { computeCostUSD, contextWindowFor } from './util/pricing.js';
26
26
  import { estimateTokens, summarizeHistory } from './compact.js';
27
27
  import { validateToolArguments } from './llm/repair.js';
28
+ import { parseToolCalls } from './llm/dialects.js';
28
29
  import { uuid } from './util/ids.js';
29
30
  /** Wrap a single text prompt into the async-iterable form runAgent expects. */
30
31
  async function* singleUserPrompt(text) {
@@ -262,8 +263,25 @@ export async function* runAgent(options) {
262
263
  for (const t of tools)
263
264
  if (!t.run)
264
265
  clientTools.add(t.def.function.name);
265
- const defs = toolDefs(tools);
266
+ const defs = toolDefs(tools); // FULL set — for the search index, suppression, and call recovery
266
267
  const byName = toolByName(tools);
268
+ // Deferred tools: kept OUT of the per-turn payload (token savings) but still
269
+ // discoverable via tool_search and executable. `tool_search` surfaces them and
270
+ // arms them (adds their schema to subsequent turns). tool_search itself is
271
+ // never deferred, or discovery breaks.
272
+ const deferredSet = new Set([...(options.deferredTools ?? []), ...tools.filter((t) => t.defer).map((t) => t.def.function.name)].filter((n) => n !== 'tool_search'));
273
+ const armed = new Set();
274
+ const sentDefs = () => deferredSet.size
275
+ ? toolDefs(tools.filter((t) => !deferredSet.has(t.def.function.name) || armed.has(t.def.function.name)))
276
+ : defs;
277
+ // Stop streaming visible deltas once tool-call / reasoning markup begins (native
278
+ // dialects, <thinking>, or named-tag tools like <finish>); final text is cleaned.
279
+ const streamSuppressRe = (() => {
280
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
281
+ const names = defs.map((d) => d.function.name).filter(Boolean).map(esc);
282
+ const named = names.length ? `|<(?:${names.join('|')})[\\s/>]` : '';
283
+ return new RegExp(`<tool_call|<function\\s*=|<thinking${named}`, 'i');
284
+ })();
267
285
  let system = options.systemPrompt != null ? options.systemPrompt : defaultSystemPrompt(cwd);
268
286
  if (teamEnabled)
269
287
  system += '\n\n' + coordinatorPrompt();
@@ -293,6 +311,13 @@ export async function* runAgent(options) {
293
311
  memory,
294
312
  skills,
295
313
  planMode,
314
+ armTools: deferredSet.size
315
+ ? (names) => {
316
+ for (const n of names)
317
+ if (deferredSet.has(n))
318
+ armed.add(n);
319
+ }
320
+ : undefined,
296
321
  };
297
322
  const skillCommands = skillsToCommands(skills);
298
323
  const allCommands = [...(options.commands ?? []), ...skillCommands];
@@ -403,7 +428,7 @@ export async function* runAgent(options) {
403
428
  subtype: 'init',
404
429
  apiKeySource: 'none',
405
430
  cwd,
406
- tools: defs.map((d) => d.function.name),
431
+ tools: sentDefs().map((d) => d.function.name),
407
432
  mcp_servers: mcpStatuses,
408
433
  model: model ?? 'unknown',
409
434
  permissionMode,
@@ -632,14 +657,14 @@ export async function* runAgent(options) {
632
657
  let inToolMarkup = false;
633
658
  const sp = llm.streamChat(history, {
634
659
  model,
635
- tools: defs,
660
+ tools: sentDefs(),
636
661
  signal,
637
662
  onToken: (delta) => {
638
663
  streamedText += delta;
639
664
  // Stop streaming once inline tool-call markup begins; it would
640
665
  // otherwise flood the UI with raw XML / file contents. The cleaned
641
666
  // text arrives with the final assistant message.
642
- if (!inToolMarkup && /<tool_call|<function\s*=/.test(streamedText)) {
667
+ if (!inToolMarkup && streamSuppressRe.test(streamedText)) {
643
668
  inToolMarkup = true;
644
669
  }
645
670
  if (inToolMarkup)
@@ -664,7 +689,7 @@ export async function* runAgent(options) {
664
689
  else {
665
690
  result = await llm.streamChat(history, {
666
691
  model,
667
- tools: defs,
692
+ tools: sentDefs(),
668
693
  signal,
669
694
  onToken: (delta) => {
670
695
  streamedText += delta;
@@ -680,8 +705,17 @@ export async function* runAgent(options) {
680
705
  break;
681
706
  }
682
707
  apiMs += Date.now() - apiStart;
683
- const text = result.text || streamedText;
684
- const calls = result.toolCalls.length ? result.toolCalls : captured;
708
+ let text = result.text || streamedText;
709
+ let calls = result.toolCalls.length ? result.toolCalls : captured;
710
+ // Loop-level safety net: recover inline tool calls (native dialects +
711
+ // named-tag tools like <finish>) a custom LLMClient left as text, and scrub
712
+ // leaked tool/reasoning markup so raw tags never reach the user.
713
+ if (!calls.length) {
714
+ const recovered = parseToolCalls(text, { toolNames: defs.map((d) => d.function.name) });
715
+ if (recovered.calls.length)
716
+ calls = recovered.calls;
717
+ text = recovered.cleanedText;
718
+ }
685
719
  lastText = text || lastText;
686
720
  resultModel = result.model || resultModel;
687
721
  addUsageInto(usageTotal, result.usage);
@@ -16,6 +16,21 @@ export interface ToolDialect {
16
16
  export declare const xmlFunctionDialect: ToolDialect;
17
17
  export declare const hermesDialect: ToolDialect;
18
18
  export declare const jsonFenceDialect: ToolDialect;
19
+ /**
20
+ * Named-tag tool calls (the Cline/Roo/Aider convention): a tool invoked as
21
+ * `<tool_name><param>value</param></tool_name>` (or `<tool_name/>`). Scoped to
22
+ * the KNOWN tool names so ordinary markup the model writes isn't misread. This
23
+ * is what leaks as raw `<finish>…</finish>` text when a model emulates a custom
24
+ * tool format and the SDK doesn't recognize it.
25
+ */
26
+ export declare function parseNamedTagToolCalls(text: string, toolNames: string[], idBase?: number): ParsedToolCalls;
27
+ /**
28
+ * Remove leaked reasoning / tool-wrapper markup from user-visible text:
29
+ * `<thinking>…</thinking>` blocks and orphan `<tool_call>` / `<function…>` /
30
+ * `<parameter…>` tags that a model emitted as prose. Conservative — only these
31
+ * well-known control tags, which essentially never appear in legitimate output.
32
+ */
33
+ export declare function stripControlTags(text: string): string;
19
34
  /** All built-in dialects, keyed by name. */
20
35
  export declare const dialects: Record<string, ToolDialect>;
21
36
  /** Default attempt order — xml-function first preserves original behavior. */
@@ -27,6 +42,7 @@ export declare const DEFAULT_DIALECTS: string[];
27
42
  export declare function parseToolCalls(text: string, opts?: {
28
43
  dialects?: string[];
29
44
  idBase?: number;
45
+ toolNames?: string[];
30
46
  }): ParsedToolCalls;
31
47
  /** True if ANY of the given dialects (default: all) detects tool-call markup. */
32
48
  export declare function hasToolCalls(text: string, order?: string[]): boolean;
@@ -117,6 +117,69 @@ export const jsonFenceDialect = {
117
117
  return { calls, cleanedText };
118
118
  },
119
119
  };
120
+ function escapeRe(s) {
121
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
122
+ }
123
+ /** Extract params from a tool-tag body: both `<parameter=key>v</parameter>` and direct `<key>v</key>` children. */
124
+ function parseTagParams(body) {
125
+ const args = {};
126
+ const pRe = /<parameter\s*=\s*([^>\s]+)\s*>([\s\S]*?)(?:<\/parameter>|<parameter\s*=|$)/gi;
127
+ let m;
128
+ while ((m = pRe.exec(body)) !== null)
129
+ args[m[1].trim()] = trimEdges(m[2]);
130
+ const tRe = /<([a-zA-Z_][\w-]*)\s*>([\s\S]*?)<\/\1>/g;
131
+ while ((m = tRe.exec(body)) !== null) {
132
+ const k = m[1];
133
+ if (k === 'parameter' || k in args)
134
+ continue;
135
+ args[k] = trimEdges(m[2]);
136
+ }
137
+ return args;
138
+ }
139
+ /**
140
+ * Named-tag tool calls (the Cline/Roo/Aider convention): a tool invoked as
141
+ * `<tool_name><param>value</param></tool_name>` (or `<tool_name/>`). Scoped to
142
+ * the KNOWN tool names so ordinary markup the model writes isn't misread. This
143
+ * is what leaks as raw `<finish>…</finish>` text when a model emulates a custom
144
+ * tool format and the SDK doesn't recognize it.
145
+ */
146
+ export function parseNamedTagToolCalls(text, toolNames, idBase = 0) {
147
+ if (!text || !toolNames?.length)
148
+ return { calls: [], cleanedText: text };
149
+ let best = { idx: -1, name: '', after: -1 };
150
+ for (const name of toolNames) {
151
+ const re = new RegExp('<' + escapeRe(name) + '(?:\\s[^>]*)?/?>', 'i');
152
+ const m = re.exec(text);
153
+ if (m && (best.idx < 0 || m.index < best.idx))
154
+ best = { idx: m.index, name, after: m.index + m[0].length };
155
+ }
156
+ if (best.idx < 0)
157
+ return { calls: [], cleanedText: text };
158
+ const closer = new RegExp('</' + escapeRe(best.name) + '>', 'i');
159
+ const rest = text.slice(best.after);
160
+ const cm = closer.exec(rest);
161
+ const body = cm ? rest.slice(0, cm.index) : rest;
162
+ const args = parseTagParams(body);
163
+ return {
164
+ calls: [{ id: `call_inline_${idBase}`, type: 'function', function: { name: best.name, arguments: JSON.stringify(args) } }],
165
+ cleanedText: text.slice(0, best.idx).trim(),
166
+ };
167
+ }
168
+ /**
169
+ * Remove leaked reasoning / tool-wrapper markup from user-visible text:
170
+ * `<thinking>…</thinking>` blocks and orphan `<tool_call>` / `<function…>` /
171
+ * `<parameter…>` tags that a model emitted as prose. Conservative — only these
172
+ * well-known control tags, which essentially never appear in legitimate output.
173
+ */
174
+ export function stripControlTags(text) {
175
+ if (!text || text.indexOf('<') < 0)
176
+ return text;
177
+ return text
178
+ .replace(/<thinking\s*>[\s\S]*?<\/thinking\s*>/gi, '')
179
+ .replace(/<\/?(?:thinking|tool_call|function|parameter|antml:[a-z_]+)(?:\s[^>]*|=[^>]*)?\/?>/gi, '')
180
+ .replace(/[ \t]+(\r?\n)/g, '$1')
181
+ .trim();
182
+ }
120
183
  /** All built-in dialects, keyed by name. */
121
184
  export const dialects = {
122
185
  'xml-function': xmlFunctionDialect,
@@ -139,9 +202,17 @@ export function parseToolCalls(text, opts = {}) {
139
202
  continue;
140
203
  const parsed = d.parse(text, opts.idBase ?? 0);
141
204
  if (parsed.calls.length)
142
- return parsed;
205
+ return { calls: parsed.calls, cleanedText: stripControlTags(parsed.cleanedText) };
206
+ }
207
+ // Named-tag fallback (e.g. `<finish>…</finish>`) — scoped to known tool names.
208
+ if (opts.toolNames?.length) {
209
+ const named = parseNamedTagToolCalls(text, opts.toolNames, opts.idBase ?? 0);
210
+ if (named.calls.length)
211
+ return { calls: named.calls, cleanedText: stripControlTags(named.cleanedText) };
143
212
  }
144
- return { calls: [], cleanedText: text };
213
+ // No tool call recognized still scrub any leaked control/reasoning markup so
214
+ // raw tags never render to the user.
215
+ return { calls: [], cleanedText: stripControlTags(text) };
145
216
  }
146
217
  /** True if ANY of the given dialects (default: all) detects tool-call markup. */
147
218
  export function hasToolCalls(text, order = DEFAULT_DIALECTS) {
@@ -113,11 +113,9 @@ export function createOpenAIClient(options = {}) {
113
113
  // visible text. (Empty `dialects` — e.g. for native GPT/Claude — skips this.)
114
114
  let finalText = text;
115
115
  if (!toolCalls.length && (!dialects || dialects.length)) {
116
- const inline = parseToolCalls(text, { dialects });
117
- if (inline.calls.length) {
118
- toolCalls.push(...inline.calls);
119
- finalText = inline.cleanedText;
120
- }
116
+ const inline = parseToolCalls(text, { dialects, toolNames: opts.tools?.map((t) => t.function.name) });
117
+ toolCalls.push(...inline.calls);
118
+ finalText = inline.cleanedText; // also scrubs leaked control tags even when no call is found
121
119
  }
122
120
  if (toolCalls.length && opts.onTool)
123
121
  opts.onTool(toolCalls);
package/dist/loop.js CHANGED
@@ -1,6 +1,14 @@
1
1
  import { toolByName, toolDefs } from './tools/index.js';
2
2
  import { validateToolArguments } from './llm/repair.js';
3
+ import { parseToolCalls } from './llm/dialects.js';
3
4
  import { uuid } from './util/ids.js';
5
+ /** Regex that matches the onset of tool-call / reasoning markup in streamed text. */
6
+ function buildSuppressRe(toolNames) {
7
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8
+ const names = toolNames.filter(Boolean).map(esc);
9
+ const named = names.length ? `|<(?:${names.join('|')})[\\s/>]` : '';
10
+ return new RegExp(`<tool_call|<function\\s*=|<thinking${named}`, 'i');
11
+ }
4
12
  const emptyUsage = () => ({ input_tokens: 0, output_tokens: 0 });
5
13
  function addUsage(t, b) {
6
14
  if (!b)
@@ -85,6 +93,10 @@ export async function* runToolLoop(opts) {
85
93
  const emitPartial = !!opts.includePartialMessages;
86
94
  const byName = toolByName(tools);
87
95
  const defs = toolDefs(tools);
96
+ // Stop streaming visible deltas once tool-call / reasoning markup begins — the
97
+ // final cleaned text comes from the parsed result. Covers native dialects,
98
+ // <thinking>, and named-tag tools (e.g. <finish>) so they never flicker to the UI.
99
+ const suppressRe = buildSuppressRe(defs.map((d) => d.function.name));
88
100
  const startedAt = Date.now();
89
101
  let apiMs = 0;
90
102
  let turns = 0;
@@ -115,7 +127,7 @@ export async function* runToolLoop(opts) {
115
127
  signal,
116
128
  onToken: (delta) => {
117
129
  streamedText += delta;
118
- if (!inToolMarkup && /<tool_call|<function\s*=/.test(streamedText))
130
+ if (!inToolMarkup && suppressRe.test(streamedText))
119
131
  inToolMarkup = true;
120
132
  if (inToolMarkup)
121
133
  return;
@@ -155,8 +167,17 @@ export async function* runToolLoop(opts) {
155
167
  break;
156
168
  }
157
169
  apiMs += Date.now() - apiStart;
158
- const text = result.text || streamedText;
159
- const calls = result.toolCalls.length ? result.toolCalls : captured;
170
+ let text = result.text || streamedText;
171
+ let calls = result.toolCalls.length ? result.toolCalls : captured;
172
+ // Loop-level safety net: recover tool calls a (possibly custom) LLMClient left
173
+ // as inline text — native dialects + named-tag tools — and scrub leaked
174
+ // tool/reasoning markup so it never renders. Runs for ANY client, not just ours.
175
+ if (!calls.length) {
176
+ const recovered = parseToolCalls(text, { toolNames: defs.map((d) => d.function.name) });
177
+ if (recovered.calls.length)
178
+ calls = recovered.calls;
179
+ text = recovered.cleanedText;
180
+ }
160
181
  lastText = text || lastText;
161
182
  resultModel = result.model || resultModel;
162
183
  addUsage(usageTotal, result.usage);
package/dist/query.d.ts CHANGED
@@ -22,6 +22,9 @@ export interface QueryOptions {
22
22
  appendSystemPrompt?: string;
23
23
  allowedTools?: string[];
24
24
  disallowedTools?: string[];
25
+ /** Tool names to defer out of the per-turn payload — discoverable via `tool_search`
26
+ * and armed on demand. Saves tokens on large tool pools (also per-tool `defer: true`). */
27
+ deferredTools?: string[];
25
28
  maxTurns?: number;
26
29
  /** Wall-clock budget (ms): pause at a turn boundary past this + emit `paused` (survivor). */
27
30
  maxDurationMs?: number;
package/dist/query.js CHANGED
@@ -21,6 +21,7 @@ export function query(options) {
21
21
  appendSystemPrompt: options.appendSystemPrompt,
22
22
  allowedTools: options.allowedTools,
23
23
  disallowedTools: options.disallowedTools,
24
+ deferredTools: options.deferredTools,
24
25
  maxTurns: options.maxTurns,
25
26
  maxDurationMs: options.maxDurationMs,
26
27
  continueRun: options.continueRun,
@@ -15,6 +15,9 @@ export interface DefineToolSpec {
15
15
  run?: (input: Record<string, unknown>, ctx: ToolContext) => Promise<ToolResult> | ToolResult;
16
16
  /** Optional: spill threshold for large outputs (see Tool.maxResultChars). */
17
17
  maxResultChars?: number;
18
+ /** Defer out of the per-turn payload — discoverable via `tool_search`, armed on
19
+ * demand. For large pools of rarely-used tools (see Tool.defer). */
20
+ defer?: boolean;
18
21
  }
19
22
  /** Build a `Tool` from a friendly spec. */
20
23
  export declare function defineTool(spec: DefineToolSpec): Tool;
@@ -19,5 +19,7 @@ export function defineTool(spec) {
19
19
  tool.run = async (input, ctx) => spec.run(input, ctx);
20
20
  if (spec.maxResultChars !== undefined)
21
21
  tool.maxResultChars = spec.maxResultChars;
22
+ if (spec.defer)
23
+ tool.defer = true;
22
24
  return tool;
23
25
  }
@@ -41,8 +41,11 @@ export const toolSearch = {
41
41
  .slice(0, limit);
42
42
  if (!scored.length)
43
43
  return { content: `No tools matched "${q}".` };
44
+ // Arm any deferred tools we surfaced so their full schema is sent next turn
45
+ // and the model can call them directly.
46
+ ctx.armTools?.(scored.map(({ t }) => t.name));
44
47
  return {
45
- content: `Matching tools for "${q}":\n` +
48
+ content: `Matching tools for "${q}" (now available to call):\n` +
46
49
  scored.map(({ t }) => ` ${t.name} — ${t.description.split('\n')[0]}`).join('\n'),
47
50
  };
48
51
  },
@@ -72,6 +72,9 @@ export interface ToolContext {
72
72
  planMode?: {
73
73
  active: boolean;
74
74
  };
75
+ /** Arm deferred tools by name so their full schema is sent on subsequent turns.
76
+ * Provided by the loop; `tool_search` calls it for the deferred tools it surfaces. */
77
+ armTools?: (names: string[]) => void;
75
78
  }
76
79
  /** Result returned by a tool run. */
77
80
  export interface ToolResult {
@@ -92,4 +95,12 @@ export interface Tool {
92
95
  * When omitted, the loop uses its global default threshold.
93
96
  */
94
97
  maxResultChars?: number;
98
+ /**
99
+ * DEFER this tool out of the per-turn payload sent to the LLM. It stays
100
+ * discoverable via `tool_search` and executable when called, but its schema
101
+ * isn't sent (saving tokens every turn) until `tool_search` surfaces it — at
102
+ * which point the loop "arms" it for subsequent turns. Use for large pools of
103
+ * rarely-used integration tools. (Also settable via `query({ deferredTools })`.)
104
+ */
105
+ defer?: boolean;
95
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyclaude-sdk",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Standalone, browser-compatible SDK providing Claude Code agent capabilities (tools, tool loop, multi-turn, MCP, sub-agents, sessions) against any OpenAI/Anthropic-compatible LLM endpoint. Runs in the browser (WebContainer), Node, and Bun — no backend required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",