anyclaude-sdk 0.7.2 → 0.7.4

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.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) {
@@ -264,6 +265,14 @@ export async function* runAgent(options) {
264
265
  clientTools.add(t.def.function.name);
265
266
  const defs = toolDefs(tools);
266
267
  const byName = toolByName(tools);
268
+ // Stop streaming visible deltas once tool-call / reasoning markup begins (native
269
+ // dialects, <thinking>, or named-tag tools like <finish>); final text is cleaned.
270
+ const streamSuppressRe = (() => {
271
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
272
+ const names = defs.map((d) => d.function.name).filter(Boolean).map(esc);
273
+ const named = names.length ? `|<(?:${names.join('|')})[\\s/>]` : '';
274
+ return new RegExp(`<tool_call|<function\\s*=|<thinking${named}`, 'i');
275
+ })();
267
276
  let system = options.systemPrompt != null ? options.systemPrompt : defaultSystemPrompt(cwd);
268
277
  if (teamEnabled)
269
278
  system += '\n\n' + coordinatorPrompt();
@@ -639,7 +648,7 @@ export async function* runAgent(options) {
639
648
  // Stop streaming once inline tool-call markup begins; it would
640
649
  // otherwise flood the UI with raw XML / file contents. The cleaned
641
650
  // text arrives with the final assistant message.
642
- if (!inToolMarkup && /<tool_call|<function\s*=/.test(streamedText)) {
651
+ if (!inToolMarkup && streamSuppressRe.test(streamedText)) {
643
652
  inToolMarkup = true;
644
653
  }
645
654
  if (inToolMarkup)
@@ -680,8 +689,17 @@ export async function* runAgent(options) {
680
689
  break;
681
690
  }
682
691
  apiMs += Date.now() - apiStart;
683
- const text = result.text || streamedText;
684
- const calls = result.toolCalls.length ? result.toolCalls : captured;
692
+ let text = result.text || streamedText;
693
+ let calls = result.toolCalls.length ? result.toolCalls : captured;
694
+ // Loop-level safety net: recover inline tool calls (native dialects +
695
+ // named-tag tools like <finish>) a custom LLMClient left as text, and scrub
696
+ // leaked tool/reasoning markup so raw tags never reach the user.
697
+ if (!calls.length) {
698
+ const recovered = parseToolCalls(text, { toolNames: defs.map((d) => d.function.name) });
699
+ if (recovered.calls.length)
700
+ calls = recovered.calls;
701
+ text = recovered.cleanedText;
702
+ }
685
703
  lastText = text || lastText;
686
704
  resultModel = result.model || resultModel;
687
705
  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.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Returns an AsyncGenerator<SDKMessage>. Accepts either a single string prompt
4
4
  // or an async iterable of SDKUserMessage (for multi-turn / interactive use).
5
5
  import { runAgent } from './agent.js';
6
- import { track, telemetryEnabled } from './telemetry.js';
6
+ import { track, telemetryEnabled, tokenBucket } from './telemetry.js';
7
7
  import { profileForModel } from './llm/profiles.js';
8
8
  export function query(options) {
9
9
  const prompt = typeof options.prompt === 'string'
@@ -59,17 +59,18 @@ export function query(options) {
59
59
  settings: options.settings,
60
60
  skills: options.skills,
61
61
  });
62
- gen.interrupt = () => abortController.abort();
63
- // Anonymous, aggregate adoption signal one event per public run. Fire-and-forget,
64
- // never blocks the generator, no-ops unless enabled + a collector is configured.
65
- // Only booleans + a coarse model-family bucket leave the process (see telemetry.ts).
62
+ // Anonymous, aggregate adoption signal. Fire-and-forget, never blocks, no-ops
63
+ // unless enabled + a collector is configured. Only booleans + coarse buckets
64
+ // (model family, token-volume bucket) ever leave the process see telemetry.ts.
66
65
  const telemetry = {
67
66
  disabled: options.disableTelemetry,
68
67
  ...options.telemetry,
69
68
  };
70
- if (telemetryEnabled(telemetry)) {
69
+ const modelFamily = profileForModel(options.model).name;
70
+ const enabled = telemetryEnabled(telemetry);
71
+ if (enabled) {
71
72
  track('run', {
72
- model_family: profileForModel(options.model).name,
73
+ model_family: modelFamily,
73
74
  client_workspace_tools: !!options.clientWorkspaceTools,
74
75
  client_tools: !!options.clientTools?.length,
75
76
  survivor: options.maxDurationMs != null,
@@ -83,7 +84,29 @@ export function query(options) {
83
84
  resumed: !!options.continueRun || !!options.resume,
84
85
  }, telemetry);
85
86
  }
86
- return gen;
87
+ if (!enabled) {
88
+ gen.interrupt = () => abortController.abort();
89
+ return gen;
90
+ }
91
+ // Wrap to emit one `run_end` with a coarse token-volume bucket when the run
92
+ // finishes (tokens aren't known until the `result` message). Pass-through only.
93
+ const wrapped = (async function* () {
94
+ let totalTokens = 0;
95
+ try {
96
+ for await (const m of gen) {
97
+ if (m.type === 'result' && m.usage) {
98
+ const u = m.usage;
99
+ totalTokens = (u.input_tokens || 0) + (u.output_tokens || 0);
100
+ }
101
+ yield m;
102
+ }
103
+ }
104
+ finally {
105
+ track('run_end', { model_family: modelFamily, tokens_bucket: tokenBucket(totalTokens) }, telemetry);
106
+ }
107
+ })();
108
+ wrapped.interrupt = () => abortController.abort();
109
+ return wrapped;
87
110
  }
88
111
  /** Wrap a single text prompt into the async-iterable form runAgent expects. */
89
112
  export async function* singlePrompt(text) {
@@ -6,6 +6,8 @@ export interface TelemetryOptions {
6
6
  /** Collector URL. Defaults to `ANYCLAUDE_TELEMETRY_URL` then the built-in default. */
7
7
  url?: string;
8
8
  }
9
+ /** Coarse token-volume bucket — never an exact count, so a single run isn't fingerprintable. */
10
+ export declare function tokenBucket(total: number): string;
9
11
  /** Resolve whether telemetry may run, honoring every documented opt-out. */
10
12
  export declare function telemetryEnabled(opts?: TelemetryOptions): boolean;
11
13
  /** Coarse runtime bucket — never anything machine-identifying. */
package/dist/telemetry.js CHANGED
@@ -24,7 +24,21 @@ const DEFAULT_TELEMETRY_URL = 'https://anyclaude-telemetry.puter.work';
24
24
  // Only these prop keys are ever transmitted, and only with safe value types.
25
25
  // Booleans pass through; these specific string keys pass through as-is (they are
26
26
  // coarse buckets we set ourselves — never free-form / user data).
27
- const ALLOWED_STRING_KEYS = new Set(['model_family', 'event_detail']);
27
+ const ALLOWED_STRING_KEYS = new Set(['model_family', 'event_detail', 'tokens_bucket']);
28
+ /** Coarse token-volume bucket — never an exact count, so a single run isn't fingerprintable. */
29
+ export function tokenBucket(total) {
30
+ if (!Number.isFinite(total) || total <= 0)
31
+ return '0';
32
+ if (total < 1_000)
33
+ return '<1k';
34
+ if (total < 10_000)
35
+ return '1k-10k';
36
+ if (total < 100_000)
37
+ return '10k-100k';
38
+ if (total < 1_000_000)
39
+ return '100k-1m';
40
+ return '1m+';
41
+ }
28
42
  function readEnv(name) {
29
43
  const p = globalThis.process;
30
44
  return p?.env?.[name];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyclaude-sdk",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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",