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 +21 -3
- package/dist/llm/dialects.d.ts +16 -0
- package/dist/llm/dialects.js +73 -2
- package/dist/llm/openai.js +3 -5
- package/dist/loop.js +24 -3
- package/dist/query.js +31 -8
- package/dist/telemetry.d.ts +2 -0
- package/dist/telemetry.js +15 -1
- package/package.json +1 -1
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 &&
|
|
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
|
-
|
|
684
|
-
|
|
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);
|
package/dist/llm/dialects.d.ts
CHANGED
|
@@ -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;
|
package/dist/llm/dialects.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/llm/openai.js
CHANGED
|
@@ -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
|
-
|
|
118
|
-
|
|
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 &&
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
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
|
-
|
|
69
|
+
const modelFamily = profileForModel(options.model).name;
|
|
70
|
+
const enabled = telemetryEnabled(telemetry);
|
|
71
|
+
if (enabled) {
|
|
71
72
|
track('run', {
|
|
72
|
-
model_family:
|
|
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
|
-
|
|
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) {
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -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.
|
|
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",
|