anyclaude-sdk 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -25,3 +25,4 @@ export { uuid } from './util/ids.js';
25
25
  export * as paths from './util/paths.js';
26
26
  export { priceFor, computeCostUSD, contextWindowFor, type Pricing } from './util/pricing.js';
27
27
  export { estimateTokens, summarizeHistory, compactWithWindow } from './compact.js';
28
+ export { runToolLoop, type RunToolLoopOptions } from './loop.js';
package/dist/index.js CHANGED
@@ -27,4 +27,5 @@ export { uuid } from './util/ids.js';
27
27
  export * as paths from './util/paths.js';
28
28
  export { priceFor, computeCostUSD, contextWindowFor } from './util/pricing.js';
29
29
  export { estimateTokens, summarizeHistory, compactWithWindow } from './compact.js';
30
+ export { runToolLoop } from './loop.js';
30
31
  // (createResponsesClient is exported via ./llm/index.js)
package/dist/loop.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { CanUseTool, ChatMsg, LLMClient, SDKMessage } from './types/index.js';
2
+ import type { Tool, ToolContext } from './tools/types.js';
3
+ export interface RunToolLoopOptions {
4
+ /** Conversation so far: `[systemMsg, ...]`. Mutated in place as the loop appends turns. */
5
+ history: ChatMsg[];
6
+ /** Tools the model may call (executed via `ctx`). */
7
+ tools: Tool[];
8
+ /** Any OpenAI/Anthropic-compatible client. */
9
+ llm: LLMClient;
10
+ model?: string;
11
+ /** Tool-execution context (fs / exec / cwd / readFiles / limits …). */
12
+ ctx: ToolContext;
13
+ /** Max LLM turns before stopping with `error_max_turns`. Default 50. */
14
+ maxTurns?: number;
15
+ signal?: AbortSignal;
16
+ /** Optional permission gate; a `deny` result turns into an error tool_result. */
17
+ canUseTool?: CanUseTool;
18
+ /** Emit `stream_event` text deltas as the assistant streams. */
19
+ includePartialMessages?: boolean;
20
+ /** Correlation id stamped on every emitted SDKMessage. */
21
+ sessionId?: string;
22
+ }
23
+ /**
24
+ * Run the bare tool loop, yielding SDKMessages until the model stops or maxTurns.
25
+ *
26
+ * const ctx = { fs, exec, cwd: '/work', readFiles: new Set() } as ToolContext
27
+ * for await (const m of runToolLoop({ history, tools, llm, model, ctx })) render(m)
28
+ */
29
+ export declare function runToolLoop(opts: RunToolLoopOptions): AsyncGenerator<SDKMessage>;
package/dist/loop.js ADDED
@@ -0,0 +1,284 @@
1
+ import { toolByName, toolDefs } from './tools/index.js';
2
+ import { uuid } from './util/ids.js';
3
+ const emptyUsage = () => ({ input_tokens: 0, output_tokens: 0 });
4
+ function addUsage(t, b) {
5
+ if (!b)
6
+ return;
7
+ t.input_tokens += b.input_tokens || 0;
8
+ t.output_tokens += b.output_tokens || 0;
9
+ t.cache_read_input_tokens = (t.cache_read_input_tokens || 0) + (b.cache_read_input_tokens || 0);
10
+ t.cache_creation_input_tokens = (t.cache_creation_input_tokens || 0) + (b.cache_creation_input_tokens || 0);
11
+ }
12
+ function safeParse(json) {
13
+ if (!json || !json.trim())
14
+ return {};
15
+ try {
16
+ const v = JSON.parse(json);
17
+ return v && typeof v === 'object' ? v : { value: v };
18
+ }
19
+ catch {
20
+ return { _raw: json };
21
+ }
22
+ }
23
+ function toolUseBlocks(calls) {
24
+ return calls.map((c) => ({ type: 'tool_use', id: c.id, name: c.function.name, input: safeParse(c.function.arguments) }));
25
+ }
26
+ function resultToText(content) {
27
+ if (typeof content === 'string')
28
+ return content;
29
+ return content
30
+ .map((b) => (b.type === 'text' ? b.text : b.type === 'image' ? '[image]' : b.type === 'document' ? '[document]' : `[${b.type}]`))
31
+ .join('\n');
32
+ }
33
+ function toToolResultContent(content) {
34
+ if (typeof content === 'string')
35
+ return content;
36
+ return content.filter((b) => b.type === 'text' || b.type === 'image' || b.type === 'document');
37
+ }
38
+ function createPushQueue() {
39
+ const items = [];
40
+ let resolveNext = null;
41
+ let closed = false;
42
+ return {
43
+ push(v) {
44
+ if (resolveNext) {
45
+ resolveNext({ value: v, done: false });
46
+ resolveNext = null;
47
+ }
48
+ else
49
+ items.push(v);
50
+ },
51
+ close() {
52
+ closed = true;
53
+ if (resolveNext) {
54
+ resolveNext({ value: undefined, done: true });
55
+ resolveNext = null;
56
+ }
57
+ },
58
+ [Symbol.asyncIterator]() {
59
+ return {
60
+ next: () => {
61
+ if (items.length)
62
+ return Promise.resolve({ value: items.shift(), done: false });
63
+ if (closed)
64
+ return Promise.resolve({ value: undefined, done: true });
65
+ return new Promise((res) => (resolveNext = res));
66
+ },
67
+ };
68
+ },
69
+ };
70
+ }
71
+ /**
72
+ * Run the bare tool loop, yielding SDKMessages until the model stops or maxTurns.
73
+ *
74
+ * const ctx = { fs, exec, cwd: '/work', readFiles: new Set() } as ToolContext
75
+ * for await (const m of runToolLoop({ history, tools, llm, model, ctx })) render(m)
76
+ */
77
+ export async function* runToolLoop(opts) {
78
+ const { history, llm, model, ctx, signal, canUseTool } = opts;
79
+ const tools = opts.tools;
80
+ const maxTurns = opts.maxTurns ?? 50;
81
+ const sessionId = opts.sessionId ?? uuid();
82
+ const emitPartial = !!opts.includePartialMessages;
83
+ const byName = toolByName(tools);
84
+ const defs = toolDefs(tools);
85
+ const startedAt = Date.now();
86
+ let apiMs = 0;
87
+ let turns = 0;
88
+ let lastText = '';
89
+ let resultModel = model ?? 'unknown';
90
+ let hitMaxTurns = false;
91
+ let errored = null;
92
+ const usageTotal = emptyUsage();
93
+ while (true) {
94
+ if (signal?.aborted)
95
+ break;
96
+ if (turns >= maxTurns) {
97
+ hitMaxTurns = true;
98
+ break;
99
+ }
100
+ turns++;
101
+ let streamedText = '';
102
+ let captured = [];
103
+ const apiStart = Date.now();
104
+ let result;
105
+ try {
106
+ if (emitPartial) {
107
+ const q = createPushQueue();
108
+ let inToolMarkup = false;
109
+ const sp = llm.streamChat(history, {
110
+ model,
111
+ tools: defs,
112
+ signal,
113
+ onToken: (delta) => {
114
+ streamedText += delta;
115
+ if (!inToolMarkup && /<tool_call|<function\s*=/.test(streamedText))
116
+ inToolMarkup = true;
117
+ if (inToolMarkup)
118
+ return;
119
+ q.push({
120
+ type: 'stream_event',
121
+ event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: delta } },
122
+ parent_tool_use_id: null,
123
+ uuid: uuid(),
124
+ session_id: sessionId,
125
+ });
126
+ },
127
+ onTool: (calls) => {
128
+ captured = calls;
129
+ },
130
+ });
131
+ sp.then(() => { }, () => { }).finally(() => q.close());
132
+ for await (const ev of q)
133
+ yield ev;
134
+ result = await sp;
135
+ }
136
+ else {
137
+ result = await llm.streamChat(history, {
138
+ model,
139
+ tools: defs,
140
+ signal,
141
+ onToken: (delta) => {
142
+ streamedText += delta;
143
+ },
144
+ onTool: (calls) => {
145
+ captured = calls;
146
+ },
147
+ });
148
+ }
149
+ }
150
+ catch (err) {
151
+ errored = err instanceof Error ? err.message : String(err);
152
+ break;
153
+ }
154
+ apiMs += Date.now() - apiStart;
155
+ const text = result.text || streamedText;
156
+ const calls = result.toolCalls.length ? result.toolCalls : captured;
157
+ lastText = text || lastText;
158
+ resultModel = result.model || resultModel;
159
+ addUsage(usageTotal, result.usage);
160
+ const stopReason = calls.length ? 'tool_use' : result.stopReason ?? 'end_turn';
161
+ const assistantContent = [];
162
+ if (text)
163
+ assistantContent.push({ type: 'text', text });
164
+ assistantContent.push(...toolUseBlocks(calls));
165
+ const apiAssistant = {
166
+ id: 'msg_' + uuid().replace(/-/g, '').slice(0, 24),
167
+ type: 'message',
168
+ role: 'assistant',
169
+ model: resultModel,
170
+ content: assistantContent,
171
+ stop_reason: stopReason,
172
+ stop_sequence: null,
173
+ usage: result.usage ?? emptyUsage(),
174
+ };
175
+ yield { type: 'assistant', message: apiAssistant, parent_tool_use_id: null, uuid: uuid(), session_id: sessionId };
176
+ history.push({ role: 'assistant', content: text, tool_calls: calls.length ? calls : undefined });
177
+ if (!calls.length)
178
+ break;
179
+ const toolResultBlocks = [];
180
+ const turnMedia = [];
181
+ for (const call of calls) {
182
+ if (signal?.aborted)
183
+ break;
184
+ const name = call.function.name;
185
+ let input = safeParse(call.function.arguments);
186
+ const tool = byName.get(name);
187
+ let content = '';
188
+ let isError = false;
189
+ if (!tool) {
190
+ content = `Error: unknown tool "${name}"`;
191
+ isError = true;
192
+ }
193
+ else {
194
+ const decision = canUseTool
195
+ ? await canUseTool(name, input, { signal, toolUseId: call.id })
196
+ : { behavior: 'allow' };
197
+ if (decision.behavior === 'deny') {
198
+ content = `Permission denied: ${decision.message}`;
199
+ isError = true;
200
+ }
201
+ else {
202
+ if ('updatedInput' in decision && decision.updatedInput)
203
+ input = decision.updatedInput;
204
+ try {
205
+ const r = await tool.run(input, ctx);
206
+ content = r.content;
207
+ isError = !!r.isError;
208
+ }
209
+ catch (err) {
210
+ content = `Error executing ${name}: ${err instanceof Error ? err.message : String(err)}`;
211
+ isError = true;
212
+ }
213
+ }
214
+ }
215
+ const textOut = resultToText(content);
216
+ history.push({ role: 'tool', tool_call_id: call.id, content: textOut });
217
+ if (Array.isArray(content)) {
218
+ for (const b of content)
219
+ if (b.type === 'image' || b.type === 'document')
220
+ turnMedia.push(b);
221
+ }
222
+ toolResultBlocks.push({
223
+ type: 'tool_result',
224
+ tool_use_id: call.id,
225
+ content: typeof content === 'string' ? textOut : toToolResultContent(content),
226
+ is_error: isError || undefined,
227
+ });
228
+ }
229
+ if (turnMedia.length) {
230
+ history.push({
231
+ role: 'user',
232
+ content: [{ type: 'text', text: 'Attached file content from the tools above:' }, ...turnMedia],
233
+ });
234
+ }
235
+ if (toolResultBlocks.length) {
236
+ yield {
237
+ type: 'user',
238
+ message: { role: 'user', content: toolResultBlocks },
239
+ parent_tool_use_id: null,
240
+ isSynthetic: true,
241
+ timestamp: new Date().toISOString(),
242
+ uuid: uuid(),
243
+ session_id: sessionId,
244
+ };
245
+ }
246
+ }
247
+ const durationMs = Date.now() - startedAt;
248
+ if (errored || hitMaxTurns) {
249
+ yield {
250
+ type: 'result',
251
+ subtype: hitMaxTurns ? 'error_max_turns' : 'error_during_execution',
252
+ duration_ms: durationMs,
253
+ duration_api_ms: apiMs,
254
+ is_error: true,
255
+ num_turns: turns,
256
+ stop_reason: hitMaxTurns ? 'max_turns' : 'error',
257
+ total_cost_usd: 0,
258
+ usage: usageTotal,
259
+ modelUsage: {},
260
+ permission_denials: [],
261
+ errors: errored ? [errored] : [`Reached max turns (${maxTurns})`],
262
+ uuid: uuid(),
263
+ session_id: sessionId,
264
+ };
265
+ }
266
+ else {
267
+ yield {
268
+ type: 'result',
269
+ subtype: 'success',
270
+ duration_ms: durationMs,
271
+ duration_api_ms: apiMs,
272
+ is_error: false,
273
+ num_turns: turns,
274
+ result: lastText,
275
+ stop_reason: 'end_turn',
276
+ total_cost_usd: 0,
277
+ usage: usageTotal,
278
+ modelUsage: {},
279
+ permission_denials: [],
280
+ uuid: uuid(),
281
+ session_id: sessionId,
282
+ };
283
+ }
284
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyclaude-sdk",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
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",
@@ -63,6 +63,10 @@
63
63
  "./prompt": {
64
64
  "types": "./dist/prompt.d.ts",
65
65
  "import": "./dist/prompt.js"
66
+ },
67
+ "./loop": {
68
+ "types": "./dist/loop.d.ts",
69
+ "import": "./dist/loop.js"
66
70
  }
67
71
  },
68
72
  "sideEffects": false,