@jxburros/ai-nugget 0.3.1 → 0.6.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/README.md +134 -271
- package/UPGRADING.md +158 -0
- package/dist/adapters/engines/anthropic.d.ts +7 -1
- package/dist/adapters/engines/anthropic.js +64 -37
- package/dist/adapters/engines/base.d.ts +29 -2
- package/dist/adapters/engines/base.js +52 -8
- package/dist/adapters/engines/google.d.ts +17 -1
- package/dist/adapters/engines/google.js +93 -29
- package/dist/adapters/engines/ollama.d.ts +2 -1
- package/dist/adapters/engines/ollama.js +51 -15
- package/dist/adapters/engines/openaiChat.d.ts +2 -1
- package/dist/adapters/engines/openaiChat.js +60 -30
- package/dist/adapters/profiles.d.ts +21 -1
- package/dist/adapters/profiles.js +12 -2
- package/dist/agent/loop.d.ts +43 -1
- package/dist/agent/loop.js +120 -13
- package/dist/cjs/adapters/engines/anthropic.js +308 -0
- package/dist/cjs/adapters/engines/base.js +147 -0
- package/dist/cjs/adapters/engines/google.js +284 -0
- package/dist/cjs/adapters/engines/ollama.js +199 -0
- package/dist/cjs/adapters/engines/openaiChat.js +261 -0
- package/dist/cjs/adapters/index.js +22 -0
- package/dist/cjs/adapters/profiles.js +125 -0
- package/dist/cjs/agent/index.js +18 -0
- package/dist/cjs/agent/loop.js +372 -0
- package/dist/cjs/agent/tools.js +51 -0
- package/dist/cjs/connect.js +27 -0
- package/dist/cjs/errors.js +110 -0
- package/dist/cjs/handler.js +561 -0
- package/dist/cjs/index.js +27 -0
- package/dist/cjs/json.js +121 -0
- package/dist/cjs/keys.js +81 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/policy.js +42 -0
- package/dist/cjs/redact.js +78 -0
- package/dist/cjs/tokens.js +22 -0
- package/dist/cjs/transport.js +172 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/util.js +112 -0
- package/dist/connect.d.ts +31 -0
- package/dist/connect.js +25 -0
- package/dist/errors.js +25 -1
- package/dist/handler.d.ts +61 -1
- package/dist/handler.js +247 -61
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keys.js +1 -4
- package/dist/redact.d.ts +18 -0
- package/dist/redact.js +29 -1
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js +21 -1
- package/dist/types.d.ts +96 -2
- package/dist/util.d.ts +16 -0
- package/dist/util.js +50 -0
- package/nugget/VERSION.txt +2 -2
- package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
- package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/anthropic.js +305 -0
- package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
- package/nugget/dist/adapters/engines/base.d.ts +50 -0
- package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/base.js +137 -0
- package/nugget/dist/adapters/engines/base.js.map +1 -0
- package/nugget/dist/adapters/engines/google.d.ts +24 -0
- package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/google.js +281 -0
- package/nugget/dist/adapters/engines/google.js.map +1 -0
- package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
- package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/ollama.js +196 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts +16 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.js +258 -0
- package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
- package/nugget/dist/adapters/index.d.ts +5 -0
- package/nugget/dist/adapters/index.d.ts.map +1 -0
- package/nugget/dist/adapters/index.js +17 -0
- package/nugget/dist/adapters/index.js.map +1 -0
- package/nugget/dist/adapters/profiles.d.ts +62 -0
- package/nugget/dist/adapters/profiles.d.ts.map +1 -0
- package/nugget/dist/adapters/profiles.js +121 -0
- package/nugget/dist/adapters/profiles.js.map +1 -0
- package/nugget/dist/agent/index.d.ts +3 -0
- package/nugget/dist/agent/index.d.ts.map +1 -0
- package/nugget/dist/agent/index.js +3 -0
- package/nugget/dist/agent/index.js.map +1 -0
- package/nugget/dist/agent/loop.d.ts +112 -0
- package/nugget/dist/agent/loop.d.ts.map +1 -0
- package/nugget/dist/agent/loop.js +370 -0
- package/nugget/dist/agent/loop.js.map +1 -0
- package/nugget/dist/agent/tools.d.ts +29 -0
- package/nugget/dist/agent/tools.d.ts.map +1 -0
- package/nugget/dist/agent/tools.js +48 -0
- package/nugget/dist/agent/tools.js.map +1 -0
- package/nugget/dist/cjs/adapters/engines/anthropic.js +308 -0
- package/nugget/dist/cjs/adapters/engines/base.js +147 -0
- package/nugget/dist/cjs/adapters/engines/google.js +284 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +199 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +261 -0
- package/nugget/dist/cjs/adapters/index.js +22 -0
- package/nugget/dist/cjs/adapters/profiles.js +125 -0
- package/nugget/dist/cjs/agent/index.js +18 -0
- package/nugget/dist/cjs/agent/loop.js +372 -0
- package/nugget/dist/cjs/agent/tools.js +51 -0
- package/nugget/dist/cjs/connect.js +27 -0
- package/nugget/dist/cjs/errors.js +110 -0
- package/nugget/dist/cjs/handler.js +561 -0
- package/nugget/dist/cjs/index.js +27 -0
- package/nugget/dist/cjs/json.js +121 -0
- package/nugget/dist/cjs/keys.js +81 -0
- package/nugget/dist/cjs/package.json +3 -0
- package/nugget/dist/cjs/policy.js +42 -0
- package/nugget/dist/cjs/redact.js +78 -0
- package/nugget/dist/cjs/tokens.js +22 -0
- package/nugget/dist/cjs/transport.js +172 -0
- package/nugget/dist/cjs/types.js +6 -0
- package/nugget/dist/cjs/util.js +112 -0
- package/nugget/dist/connect.d.ts +31 -0
- package/nugget/dist/connect.d.ts.map +1 -0
- package/nugget/dist/connect.js +25 -0
- package/nugget/dist/connect.js.map +1 -0
- package/nugget/dist/errors.d.ts +22 -0
- package/nugget/dist/errors.d.ts.map +1 -0
- package/nugget/dist/errors.js +104 -0
- package/nugget/dist/errors.js.map +1 -0
- package/nugget/dist/handler.d.ts +108 -0
- package/nugget/dist/handler.d.ts.map +1 -0
- package/nugget/dist/handler.js +557 -0
- package/nugget/dist/handler.js.map +1 -0
- package/nugget/dist/index.d.ts +12 -0
- package/nugget/dist/index.d.ts.map +1 -0
- package/nugget/dist/index.js +12 -0
- package/nugget/dist/index.js.map +1 -0
- package/nugget/dist/json.d.ts +7 -0
- package/nugget/dist/json.d.ts.map +1 -0
- package/nugget/dist/json.js +114 -0
- package/nugget/dist/json.js.map +1 -0
- package/nugget/dist/keys.d.ts +7 -0
- package/nugget/dist/keys.d.ts.map +1 -0
- package/nugget/dist/keys.js +75 -0
- package/nugget/dist/keys.js.map +1 -0
- package/nugget/dist/policy.d.ts +6 -0
- package/nugget/dist/policy.d.ts.map +1 -0
- package/nugget/dist/policy.js +37 -0
- package/nugget/dist/policy.js.map +1 -0
- package/nugget/dist/redact.d.ts +26 -0
- package/nugget/dist/redact.d.ts.map +1 -0
- package/nugget/dist/redact.js +74 -0
- package/nugget/dist/redact.js.map +1 -0
- package/nugget/dist/tokens.d.ts +5 -0
- package/nugget/dist/tokens.d.ts.map +1 -0
- package/nugget/dist/tokens.js +18 -0
- package/nugget/dist/tokens.js.map +1 -0
- package/nugget/dist/transport.d.ts +30 -0
- package/nugget/dist/transport.d.ts.map +1 -0
- package/nugget/dist/transport.js +165 -0
- package/nugget/dist/transport.js.map +1 -0
- package/nugget/dist/types.d.ts +285 -0
- package/nugget/dist/types.d.ts.map +1 -0
- package/nugget/dist/types.js +4 -0
- package/nugget/dist/types.js.map +1 -0
- package/nugget/dist/util.d.ts +24 -0
- package/nugget/dist/util.d.ts.map +1 -0
- package/nugget/dist/util.js +101 -0
- package/nugget/dist/util.js.map +1 -0
- package/nugget/src/adapters/engines/anthropic.ts +62 -37
- package/nugget/src/adapters/engines/base.ts +50 -7
- package/nugget/src/adapters/engines/google.ts +92 -29
- package/nugget/src/adapters/engines/ollama.ts +51 -17
- package/nugget/src/adapters/engines/openaiChat.ts +61 -32
- package/nugget/src/adapters/profiles.ts +53 -3
- package/nugget/src/agent/loop.ts +158 -14
- package/nugget/src/connect.ts +48 -0
- package/nugget/src/errors.ts +25 -1
- package/nugget/src/handler.ts +286 -59
- package/nugget/src/index.ts +1 -0
- package/nugget/src/keys.ts +1 -5
- package/nugget/src/redact.ts +32 -1
- package/nugget/src/transport.ts +20 -1
- package/nugget/src/types.ts +96 -1
- package/nugget/src/util.ts +55 -0
- package/package.json +20 -6
package/dist/agent/loop.js
CHANGED
|
@@ -2,7 +2,7 @@ import { profileFor } from '../adapters/profiles.js';
|
|
|
2
2
|
import { AIError } from '../errors.js';
|
|
3
3
|
import { extractJson } from '../json.js';
|
|
4
4
|
import { mergeUsage } from '../tokens.js';
|
|
5
|
-
import { sleep } from '../util.js';
|
|
5
|
+
import { asRecord, sleep } from '../util.js';
|
|
6
6
|
import { validateToolArgs } from './tools.js';
|
|
7
7
|
export function runAgent(opts) {
|
|
8
8
|
let resolveResult;
|
|
@@ -21,6 +21,9 @@ async function* run(opts, resolveResult) {
|
|
|
21
21
|
let finalText = '';
|
|
22
22
|
let step = 0;
|
|
23
23
|
let settled = false;
|
|
24
|
+
// Disclose the resolved tool protocol once up front (a silent promptJson
|
|
25
|
+
// fallback was the friction behind FR‑2).
|
|
26
|
+
yield emit(opts, { type: 'tool_mode', mode: toolMode });
|
|
24
27
|
try {
|
|
25
28
|
while (step < maxSteps) {
|
|
26
29
|
step += 1;
|
|
@@ -30,16 +33,32 @@ async function* run(opts, resolveResult) {
|
|
|
30
33
|
const calls = [];
|
|
31
34
|
let stepText = '';
|
|
32
35
|
let streamFailure;
|
|
36
|
+
// In promptJson mode a tool directive must not leak into the visible
|
|
37
|
+
// stream, so directive-looking text is withheld from `delta` events (still
|
|
38
|
+
// accumulated in `stepText` for parsing). Prose streams normally.
|
|
39
|
+
const filter = toolMode === 'promptJson' ? new PromptJsonDeltaFilter() : undefined;
|
|
33
40
|
const requestMessages = toolMode === 'promptJson' ? withPromptJsonInstruction(messages, opts.tools) : messages;
|
|
34
41
|
for await (const event of opts.handler.stream(opts.connection, {
|
|
35
42
|
model: opts.model,
|
|
36
43
|
messages: requestMessages,
|
|
37
44
|
tools: toolMode === 'promptJson' ? undefined : opts.tools,
|
|
45
|
+
temperature: opts.temperature,
|
|
46
|
+
maxTokens: opts.maxTokens,
|
|
47
|
+
topP: opts.topP,
|
|
48
|
+
stopSequences: opts.stopSequences,
|
|
49
|
+
providerOptions: opts.providerOptions,
|
|
38
50
|
signal: agentSignal.signal,
|
|
39
51
|
metadata: { ...opts.metadata, agentStep: step },
|
|
40
52
|
})) {
|
|
41
|
-
if (event.type === 'delta')
|
|
53
|
+
if (event.type === 'delta') {
|
|
42
54
|
stepText += event.text;
|
|
55
|
+
if (filter) {
|
|
56
|
+
const visible = filter.push(event.text);
|
|
57
|
+
if (visible)
|
|
58
|
+
yield emit(opts, { type: 'delta', text: visible });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
43
62
|
if (event.type === 'tool_call')
|
|
44
63
|
calls.push(event.call);
|
|
45
64
|
if (event.type === 'error')
|
|
@@ -51,10 +70,17 @@ async function* run(opts, resolveResult) {
|
|
|
51
70
|
}
|
|
52
71
|
yield emit(opts, event);
|
|
53
72
|
}
|
|
73
|
+
// If withheld text turned out not to be a real directive, flush it so the
|
|
74
|
+
// user still sees the model's answer.
|
|
75
|
+
if (filter && calls.length === 0) {
|
|
76
|
+
const flushed = filter.flush();
|
|
77
|
+
if (flushed)
|
|
78
|
+
yield emit(opts, { type: 'delta', text: flushed });
|
|
79
|
+
}
|
|
54
80
|
// A handler-level failure (auth, policy, cancel, exhausted retries) must
|
|
55
81
|
// stop the loop honestly rather than looking like an empty completion.
|
|
56
82
|
if (streamFailure)
|
|
57
|
-
return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : streamFailure.kind === 'canceled' ? 'canceled' : 'error');
|
|
83
|
+
return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : streamFailure.kind === 'canceled' ? 'canceled' : 'error', errorInfo(streamFailure));
|
|
58
84
|
finalText = stepText;
|
|
59
85
|
messages.push(toolMode === 'promptJson'
|
|
60
86
|
? { role: 'assistant', content: stepText }
|
|
@@ -75,7 +101,8 @@ async function* run(opts, resolveResult) {
|
|
|
75
101
|
continue;
|
|
76
102
|
}
|
|
77
103
|
let args = validation.args;
|
|
78
|
-
|
|
104
|
+
const requiresApproval = tool.sideEffects || opts.approvalMode === 'all';
|
|
105
|
+
if (requiresApproval) {
|
|
79
106
|
if (!opts.approval) {
|
|
80
107
|
yield* appendToolDenied(opts, messages, step, call, 'No approval gate configured');
|
|
81
108
|
continue;
|
|
@@ -115,8 +142,16 @@ async function* run(opts, resolveResult) {
|
|
|
115
142
|
continue;
|
|
116
143
|
}
|
|
117
144
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
// Guard against a tool returning `undefined` (JSON.stringify(undefined)
|
|
146
|
+
// is the value `undefined`, not a string) — an unguarded value here
|
|
147
|
+
// used to crash the next step's message mapper and kill the whole run.
|
|
148
|
+
const serialized = JSON.stringify(executed.result) ?? 'null';
|
|
149
|
+
messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: formatToolResult(serialized, opts.toolResult) });
|
|
150
|
+
// A tool that returns a structured `{ ok: false, ... }` is a recoverable
|
|
151
|
+
// error the model should react to — surface it as an error result.
|
|
152
|
+
const record = asRecord(executed.result);
|
|
153
|
+
const isError = record?.ok === false;
|
|
154
|
+
yield emit(opts, { type: 'tool_result', step, call, result: executed.result, isError });
|
|
120
155
|
}
|
|
121
156
|
catch (error) {
|
|
122
157
|
const message = error instanceof Error ? error.message : 'Tool result was not serializable';
|
|
@@ -131,7 +166,8 @@ async function* run(opts, resolveResult) {
|
|
|
131
166
|
return yield* yieldDone('max_steps');
|
|
132
167
|
}
|
|
133
168
|
catch (error) {
|
|
134
|
-
const
|
|
169
|
+
const stopReason = agentSignal.timedOut() ? 'deadline' : (agentSignal.signal?.aborted || (error instanceof AIError && error.kind === 'canceled')) ? 'canceled' : 'error';
|
|
170
|
+
const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason, stopReason === 'error' ? errorInfo(error) : undefined);
|
|
135
171
|
settled = true;
|
|
136
172
|
resolveResult(result);
|
|
137
173
|
yield emit(opts, { type: 'agent_done', result });
|
|
@@ -143,8 +179,8 @@ async function* run(opts, resolveResult) {
|
|
|
143
179
|
resolveResult(result);
|
|
144
180
|
}
|
|
145
181
|
}
|
|
146
|
-
function* yieldDone(stopReason) {
|
|
147
|
-
const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason);
|
|
182
|
+
function* yieldDone(stopReason, error) {
|
|
183
|
+
const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason, error);
|
|
148
184
|
settled = true;
|
|
149
185
|
resolveResult(result);
|
|
150
186
|
yield emit(opts, { type: 'agent_done', result });
|
|
@@ -154,8 +190,67 @@ function emit(opts, event) {
|
|
|
154
190
|
opts.onEvent?.(event);
|
|
155
191
|
return event;
|
|
156
192
|
}
|
|
157
|
-
function makeResult(finalText, messages, usage, steps, stopReason) {
|
|
158
|
-
return { finalText, messages, usage, steps, stopReason };
|
|
193
|
+
function makeResult(finalText, messages, usage, steps, stopReason, error) {
|
|
194
|
+
return error ? { finalText, messages, usage, steps, stopReason, error } : { finalText, messages, usage, steps, stopReason };
|
|
195
|
+
}
|
|
196
|
+
function errorInfo(error) {
|
|
197
|
+
if (error instanceof AIError)
|
|
198
|
+
return { kind: error.kind, message: error.message };
|
|
199
|
+
return { kind: 'tool_error', message: error instanceof Error ? error.message : 'Agent run failed' };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Applies the optional size cap and untrusted-content framing to a serialized
|
|
203
|
+
* tool result before it re-enters the model's context.
|
|
204
|
+
*/
|
|
205
|
+
function formatToolResult(serialized, opts) {
|
|
206
|
+
let out = serialized;
|
|
207
|
+
const max = opts?.maxChars;
|
|
208
|
+
if (max !== undefined && out.length > max) {
|
|
209
|
+
out = `${out.slice(0, max)}\n…[truncated ${out.length - max} chars]`;
|
|
210
|
+
}
|
|
211
|
+
if (opts?.wrapUntrusted) {
|
|
212
|
+
out = `<untrusted_tool_output>\n${out}\n</untrusted_tool_output>`;
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Withholds a promptJson tool directive from the visible `delta` stream. Once
|
|
218
|
+
* the leading non-whitespace character is known: a `{`, `[`, or fence marks a
|
|
219
|
+
* directive candidate whose text is buffered (never streamed); anything else is
|
|
220
|
+
* prose that streams through untouched. `flush()` returns any buffered text when
|
|
221
|
+
* the stream ended without yielding a parsed directive.
|
|
222
|
+
*/
|
|
223
|
+
class PromptJsonDeltaFilter {
|
|
224
|
+
decided = false;
|
|
225
|
+
withhold = false;
|
|
226
|
+
buffer = '';
|
|
227
|
+
push(text) {
|
|
228
|
+
if (!this.decided) {
|
|
229
|
+
this.buffer += text;
|
|
230
|
+
const lead = this.buffer.replace(/^\s+/, '');
|
|
231
|
+
if (lead.length === 0)
|
|
232
|
+
return '';
|
|
233
|
+
this.decided = true;
|
|
234
|
+
this.withhold = /^[[{`]/.test(lead);
|
|
235
|
+
if (this.withhold)
|
|
236
|
+
return '';
|
|
237
|
+
const flushed = this.buffer;
|
|
238
|
+
this.buffer = '';
|
|
239
|
+
return flushed;
|
|
240
|
+
}
|
|
241
|
+
if (this.withhold) {
|
|
242
|
+
this.buffer += text;
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
return text;
|
|
246
|
+
}
|
|
247
|
+
flush() {
|
|
248
|
+
if (!this.withhold)
|
|
249
|
+
return '';
|
|
250
|
+
const out = this.buffer;
|
|
251
|
+
this.buffer = '';
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
159
254
|
}
|
|
160
255
|
function* appendToolError(opts, messages, step, call, message) {
|
|
161
256
|
const result = { error: message };
|
|
@@ -170,13 +265,22 @@ function resolveToolMode(opts) {
|
|
|
170
265
|
const mode = opts.toolMode ?? 'auto';
|
|
171
266
|
if (mode !== 'auto')
|
|
172
267
|
return mode;
|
|
268
|
+
// A model that actually advertises tool support gets native tool-calling even
|
|
269
|
+
// on a local runtime whose profile is conservatively `nativeTools: false`.
|
|
270
|
+
if (opts.modelCapabilities?.includes('tools'))
|
|
271
|
+
return 'native';
|
|
173
272
|
return profileFor(opts.connection.provider, opts.connection.baseUrl).capabilities.nativeTools ? 'native' : 'promptJson';
|
|
174
273
|
}
|
|
175
274
|
function withPromptJsonInstruction(messages, tools) {
|
|
275
|
+
// Include each tool's parameter schema, not just its description, so the model
|
|
276
|
+
// knows the argument shape instead of guessing it (FR‑3).
|
|
277
|
+
const catalog = tools
|
|
278
|
+
.map((tool) => `- ${tool.name}: ${tool.description}\n parameters: ${JSON.stringify(tool.parameters)}`)
|
|
279
|
+
.join('\n');
|
|
176
280
|
return [
|
|
177
281
|
{
|
|
178
282
|
role: 'system',
|
|
179
|
-
content: `When you need tools, respond only with JSON. For one tool: {"tool":"name","input":{...}}. For several in one turn: {"tools":[{"tool":"name","input":{...}}]}.
|
|
283
|
+
content: `When you need tools, respond only with JSON. For one tool: {"tool":"name","input":{...}}. For several in one turn: {"tools":[{"tool":"name","input":{...}}]}. The "input" object must match the tool's parameters schema. Available tools:\n${catalog}`,
|
|
180
284
|
},
|
|
181
285
|
...messages.map(toPromptJsonMessage),
|
|
182
286
|
];
|
|
@@ -216,7 +320,7 @@ function callsFromPromptJson(text) {
|
|
|
216
320
|
if (typeof record.tool !== 'string')
|
|
217
321
|
continue;
|
|
218
322
|
calls.push({
|
|
219
|
-
id:
|
|
323
|
+
id: randomId(),
|
|
220
324
|
name: record.tool,
|
|
221
325
|
arguments: record.input && typeof record.input === 'object' ? record.input : {},
|
|
222
326
|
raw: JSON.stringify(record.input ?? {}),
|
|
@@ -224,6 +328,9 @@ function callsFromPromptJson(text) {
|
|
|
224
328
|
}
|
|
225
329
|
return calls;
|
|
226
330
|
}
|
|
331
|
+
function randomId() {
|
|
332
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
333
|
+
}
|
|
227
334
|
function textContent(content) {
|
|
228
335
|
if (typeof content === 'string')
|
|
229
336
|
return content;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AnthropicAdapter = void 0;
|
|
4
|
+
const errors_js_1 = require("../../errors.js");
|
|
5
|
+
const tokens_js_1 = require("../../tokens.js");
|
|
6
|
+
const transport_js_1 = require("../../transport.js");
|
|
7
|
+
const util_js_1 = require("../../util.js");
|
|
8
|
+
const base_js_1 = require("./base.js");
|
|
9
|
+
const JSON_MODE_TOOL = 'json_output';
|
|
10
|
+
class AnthropicAdapter {
|
|
11
|
+
provider;
|
|
12
|
+
constructor(provider) {
|
|
13
|
+
this.provider = provider;
|
|
14
|
+
}
|
|
15
|
+
async chat(conn, req) {
|
|
16
|
+
let result;
|
|
17
|
+
for await (const event of this.stream(conn, req)) {
|
|
18
|
+
if (event.type === 'done')
|
|
19
|
+
result = event.result;
|
|
20
|
+
if (event.type === 'error')
|
|
21
|
+
throw event.error;
|
|
22
|
+
}
|
|
23
|
+
if (!result)
|
|
24
|
+
throw new errors_js_1.AIError('Anthropic stream ended without a result', { kind: 'invalid_response', provider: conn.provider });
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
async *stream(conn, req) {
|
|
28
|
+
const started = Date.now();
|
|
29
|
+
const jsonMode = req.responseFormat?.type === 'json' && !req.tools?.length;
|
|
30
|
+
let firstTokenMs = null;
|
|
31
|
+
let text = '';
|
|
32
|
+
let inputTokens;
|
|
33
|
+
let outputTokens;
|
|
34
|
+
let stopReason;
|
|
35
|
+
let sawTerminal = false;
|
|
36
|
+
const emittedTools = [];
|
|
37
|
+
// Partial tool_use blocks keyed by content-block index (input_json_delta accumulation).
|
|
38
|
+
const blocks = new Map();
|
|
39
|
+
const timeout = (0, base_js_1.streamTimeout)(conn, req.signal);
|
|
40
|
+
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
41
|
+
// Anthropic JSON mode is implemented as a forced `json_output` tool, which
|
|
42
|
+
// cannot coexist with the caller's own tools — same silent-downgrade shape
|
|
43
|
+
// as Gemini, so it gets the same signal rather than quietly dropping.
|
|
44
|
+
if (req.responseFormat?.type === 'json' && req.tools?.length) {
|
|
45
|
+
yield {
|
|
46
|
+
type: 'context',
|
|
47
|
+
kind: 'json_mode_downgraded',
|
|
48
|
+
data: { reason: 'Anthropic JSON mode uses a forced tool and cannot be combined with caller tools; the request was sent without JSON mode', provider: conn.provider },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const res = await (0, transport_js_1.postResponse)(`${conn.baseUrl}/v1/messages`, body(req, jsonMode), conn.headers, timeout.signal, conn.provider);
|
|
53
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
54
|
+
if (!contentType.includes('text/event-stream')) {
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
const parsed = parseResponse(data);
|
|
57
|
+
inputTokens = parsed.inputTokens;
|
|
58
|
+
outputTokens = parsed.outputTokens;
|
|
59
|
+
stopReason = parsed.stopReason;
|
|
60
|
+
sawTerminal = true;
|
|
61
|
+
if (jsonMode) {
|
|
62
|
+
text = jsonTextFrom(parsed.toolCalls, parsed.text);
|
|
63
|
+
if (text)
|
|
64
|
+
yield { type: 'delta', text };
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
text = parsed.text;
|
|
68
|
+
if (text)
|
|
69
|
+
yield { type: 'delta', text };
|
|
70
|
+
for (const call of parsed.toolCalls) {
|
|
71
|
+
emittedTools.push(call);
|
|
72
|
+
yield { type: 'tool_call', call };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
for await (const line of (0, transport_js_1.sseLines)(res)) {
|
|
78
|
+
timeout.bump();
|
|
79
|
+
const record = (0, util_js_1.asRecord)((0, base_js_1.safeParse)(line));
|
|
80
|
+
if (!record)
|
|
81
|
+
continue;
|
|
82
|
+
const type = (0, util_js_1.asString)(record.type);
|
|
83
|
+
if (type === 'message_start') {
|
|
84
|
+
const usage = (0, util_js_1.asRecord)((0, util_js_1.asRecord)(record.message)?.usage);
|
|
85
|
+
inputTokens = (0, util_js_1.asNumber)(usage?.input_tokens) ?? inputTokens;
|
|
86
|
+
outputTokens = (0, util_js_1.asNumber)(usage?.output_tokens) ?? outputTokens;
|
|
87
|
+
}
|
|
88
|
+
else if (type === 'content_block_start') {
|
|
89
|
+
const index = (0, util_js_1.asNumber)(record.index) ?? 0;
|
|
90
|
+
const block = (0, util_js_1.asRecord)(record.content_block);
|
|
91
|
+
if (block?.type === 'tool_use') {
|
|
92
|
+
blocks.set(index, { id: (0, util_js_1.asString)(block.id) ?? (0, base_js_1.randomId)(), name: (0, util_js_1.asString)(block.name) ?? 'unknown', raw: '' });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (type === 'content_block_delta') {
|
|
96
|
+
const index = (0, util_js_1.asNumber)(record.index) ?? 0;
|
|
97
|
+
const delta = (0, util_js_1.asRecord)(record.delta);
|
|
98
|
+
if (delta?.type === 'text_delta') {
|
|
99
|
+
const piece = (0, util_js_1.asString)(delta.text) ?? '';
|
|
100
|
+
if (piece) {
|
|
101
|
+
if (firstTokenMs === null)
|
|
102
|
+
firstTokenMs = Date.now() - started;
|
|
103
|
+
text += piece;
|
|
104
|
+
yield { type: 'delta', text: piece };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (delta?.type === 'thinking_delta') {
|
|
108
|
+
// Extended-thinking tokens ride the reasoning channel, kept out of `text`.
|
|
109
|
+
const piece = (0, util_js_1.asString)(delta.thinking) ?? '';
|
|
110
|
+
if (piece)
|
|
111
|
+
yield { type: 'reasoning', text: piece };
|
|
112
|
+
}
|
|
113
|
+
else if (delta?.type === 'input_json_delta') {
|
|
114
|
+
const partial = blocks.get(index);
|
|
115
|
+
if (partial)
|
|
116
|
+
partial.raw += (0, util_js_1.asString)(delta.partial_json) ?? '';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (type === 'content_block_stop') {
|
|
120
|
+
const index = (0, util_js_1.asNumber)(record.index) ?? 0;
|
|
121
|
+
const partial = blocks.get(index);
|
|
122
|
+
if (partial) {
|
|
123
|
+
const call = { id: partial.id, name: partial.name, raw: partial.raw, arguments: (0, base_js_1.parseArgs)(partial.raw) };
|
|
124
|
+
blocks.delete(index);
|
|
125
|
+
if (jsonMode && call.name === JSON_MODE_TOOL) {
|
|
126
|
+
text = jsonTextFrom([call], text);
|
|
127
|
+
if (firstTokenMs === null)
|
|
128
|
+
firstTokenMs = Date.now() - started;
|
|
129
|
+
yield { type: 'delta', text };
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
emittedTools.push(call);
|
|
133
|
+
yield { type: 'tool_call', call };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (type === 'message_stop') {
|
|
138
|
+
sawTerminal = true;
|
|
139
|
+
}
|
|
140
|
+
else if (type === 'message_delta') {
|
|
141
|
+
const delta = (0, util_js_1.asRecord)(record.delta);
|
|
142
|
+
if ((0, util_js_1.asString)(delta?.stop_reason))
|
|
143
|
+
sawTerminal = true;
|
|
144
|
+
stopReason = (0, util_js_1.asString)(delta?.stop_reason) ?? stopReason;
|
|
145
|
+
const usage = (0, util_js_1.asRecord)(record.usage);
|
|
146
|
+
outputTokens = (0, util_js_1.asNumber)(usage?.output_tokens) ?? outputTokens;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!sawTerminal)
|
|
151
|
+
yield (0, base_js_1.streamAnomaly)('stream ended without a stop_reason or message_stop');
|
|
152
|
+
const hasTools = emittedTools.length > 0;
|
|
153
|
+
yield { type: 'done', result: {
|
|
154
|
+
text,
|
|
155
|
+
toolCalls: hasTools ? emittedTools : undefined,
|
|
156
|
+
finishReason: mapStop(stopReason, hasTools),
|
|
157
|
+
usage: inputTokens !== undefined || outputTokens !== undefined ? { inputTokens, outputTokens, estimated: false } : (0, tokens_js_1.estimatedUsage)((0, util_js_1.textFromMessages)(req.messages), text),
|
|
158
|
+
timing: { firstTokenMs, totalMs: Date.now() - started },
|
|
159
|
+
model: req.model,
|
|
160
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
161
|
+
} };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
throw (0, base_js_1.streamError)(error, timeout, conn.provider);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
timeout.done();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Lists models from Anthropic's `/v1/models` endpoint (auth + version headers
|
|
172
|
+
* already applied to `conn.headers`). The endpoint does not report a context
|
|
173
|
+
* window, so `contextWindow` is left undefined rather than guessed.
|
|
174
|
+
*/
|
|
175
|
+
async listModels(conn) {
|
|
176
|
+
const { data } = await (0, transport_js_1.fetchJson)((0, util_js_1.joinUrl)(conn.baseUrl, '/v1/models'), {
|
|
177
|
+
method: 'GET',
|
|
178
|
+
headers: conn.headers,
|
|
179
|
+
timeoutMs: Math.min(conn.timeoutMs ?? base_js_1.DEFAULT_TIMEOUT_MS, 15_000),
|
|
180
|
+
provider: conn.provider,
|
|
181
|
+
});
|
|
182
|
+
const rows = Array.isArray((0, util_js_1.asRecord)(data)?.data) ? (0, util_js_1.asRecord)(data).data : [];
|
|
183
|
+
return rows
|
|
184
|
+
.map((row) => (0, util_js_1.asString)((0, util_js_1.asRecord)(row)?.id))
|
|
185
|
+
.filter((id) => Boolean(id))
|
|
186
|
+
.map((id) => ({ id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } }));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
exports.AnthropicAdapter = AnthropicAdapter;
|
|
190
|
+
function body(req, jsonMode) {
|
|
191
|
+
const system = req.messages.filter((m) => m.role === 'system').map((m) => textContent(m.content)).join('\n\n') || undefined;
|
|
192
|
+
const base = {
|
|
193
|
+
model: req.model,
|
|
194
|
+
max_tokens: req.maxTokens ?? 4096,
|
|
195
|
+
temperature: req.temperature,
|
|
196
|
+
top_p: req.topP,
|
|
197
|
+
stop_sequences: req.stopSequences,
|
|
198
|
+
system,
|
|
199
|
+
messages: toAnthropicMessages(req.messages.filter((m) => m.role !== 'system')),
|
|
200
|
+
stream: true,
|
|
201
|
+
};
|
|
202
|
+
if (jsonMode) {
|
|
203
|
+
// Forced-tool JSON mode: Anthropic has no response_format, so we make the
|
|
204
|
+
// model answer through a single required tool whose input is the JSON we want.
|
|
205
|
+
base.tools = [{ name: JSON_MODE_TOOL, description: 'Return the answer as a JSON object.', input_schema: schemaFor(req) }];
|
|
206
|
+
base.tool_choice = { type: 'tool', name: JSON_MODE_TOOL };
|
|
207
|
+
}
|
|
208
|
+
else if (req.tools?.length) {
|
|
209
|
+
base.tools = req.tools.map((tool) => ({ name: tool.name, description: tool.description, input_schema: tool.parameters }));
|
|
210
|
+
if (typeof req.toolChoice === 'object')
|
|
211
|
+
base.tool_choice = { type: 'tool', name: req.toolChoice.name };
|
|
212
|
+
else if (req.toolChoice === 'auto')
|
|
213
|
+
base.tool_choice = { type: 'auto' };
|
|
214
|
+
else if (req.toolChoice === 'none')
|
|
215
|
+
base.tool_choice = { type: 'none' };
|
|
216
|
+
}
|
|
217
|
+
// providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
|
|
218
|
+
// top-level `cache_control` extras, `service_tier`, …) without a release.
|
|
219
|
+
return (0, util_js_1.applyProviderOptions)(base, req.providerOptions);
|
|
220
|
+
}
|
|
221
|
+
function toAnthropicMessages(messages) {
|
|
222
|
+
const out = [];
|
|
223
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
224
|
+
const message = messages[index];
|
|
225
|
+
if (message.role !== 'tool') {
|
|
226
|
+
out.push(toAnthropicMessage(message));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const content = [];
|
|
230
|
+
while (index < messages.length && messages[index]?.role === 'tool') {
|
|
231
|
+
const toolMessage = messages[index];
|
|
232
|
+
content.push({ type: 'tool_result', tool_use_id: toolMessage.toolCallId ?? '', content: textContent(toolMessage.content) });
|
|
233
|
+
index += 1;
|
|
234
|
+
}
|
|
235
|
+
index -= 1;
|
|
236
|
+
out.push({ role: 'user', content });
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
function toAnthropicMessage(m) {
|
|
241
|
+
// Tool-role messages are handled by toAnthropicMessages' batching loop and
|
|
242
|
+
// never reach here, so no tool branch is needed.
|
|
243
|
+
// assistant turns that carried tool calls replay them as tool_use blocks,
|
|
244
|
+
// preserving any text/image content parts that accompanied the calls.
|
|
245
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
246
|
+
const content = [...contentBlocks(m.content)];
|
|
247
|
+
for (const call of m.toolCalls)
|
|
248
|
+
content.push({ type: 'tool_use', id: call.id, name: call.name, input: call.arguments ?? {} });
|
|
249
|
+
return { role: 'assistant', content };
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
253
|
+
content: typeof m.content === 'string' ? m.content : contentBlocks(m.content),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** Maps text/image content — a string or a ContentPart[] — onto Anthropic content blocks. */
|
|
257
|
+
function contentBlocks(content) {
|
|
258
|
+
if (typeof content === 'string')
|
|
259
|
+
return content ? [{ type: 'text', text: content }] : [];
|
|
260
|
+
return content.map((part) => part.type === 'image'
|
|
261
|
+
? { type: 'image', source: { type: 'base64', media_type: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
262
|
+
: { type: 'text', text: part.text ?? '' });
|
|
263
|
+
}
|
|
264
|
+
function textContent(content) {
|
|
265
|
+
if (typeof content === 'string')
|
|
266
|
+
return content;
|
|
267
|
+
if (!content)
|
|
268
|
+
return '';
|
|
269
|
+
return content.filter((part) => part.type === 'text').map((part) => part.text ?? '').join('\n');
|
|
270
|
+
}
|
|
271
|
+
function parseResponse(data) {
|
|
272
|
+
const record = (0, util_js_1.asRecord)(data);
|
|
273
|
+
const content = Array.isArray(record?.content) ? record.content : [];
|
|
274
|
+
let text = '';
|
|
275
|
+
const toolCalls = [];
|
|
276
|
+
for (const blockValue of content) {
|
|
277
|
+
const block = (0, util_js_1.asRecord)(blockValue);
|
|
278
|
+
if (block?.type === 'text')
|
|
279
|
+
text += (0, util_js_1.asString)(block.text) ?? '';
|
|
280
|
+
if (block?.type === 'tool_use') {
|
|
281
|
+
toolCalls.push({
|
|
282
|
+
id: (0, util_js_1.asString)(block.id) ?? (0, base_js_1.randomId)(),
|
|
283
|
+
name: (0, util_js_1.asString)(block.name) ?? 'unknown',
|
|
284
|
+
arguments: block.input ?? {},
|
|
285
|
+
raw: JSON.stringify(block.input ?? {}),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const usage = (0, util_js_1.asRecord)(record?.usage);
|
|
290
|
+
return { text, inputTokens: (0, util_js_1.asNumber)(usage?.input_tokens), outputTokens: (0, util_js_1.asNumber)(usage?.output_tokens), stopReason: (0, util_js_1.asString)(record?.stop_reason), toolCalls };
|
|
291
|
+
}
|
|
292
|
+
function schemaFor(req) {
|
|
293
|
+
const schema = req.responseFormat?.type === 'json' ? req.responseFormat.schema : undefined;
|
|
294
|
+
return schema ?? { type: 'object' };
|
|
295
|
+
}
|
|
296
|
+
function jsonTextFrom(toolCalls, fallback) {
|
|
297
|
+
const json = toolCalls.find((call) => call.name === JSON_MODE_TOOL) ?? toolCalls[0];
|
|
298
|
+
if (json)
|
|
299
|
+
return json.raw && json.raw.trim() ? json.raw : JSON.stringify(json.arguments ?? {});
|
|
300
|
+
return fallback;
|
|
301
|
+
}
|
|
302
|
+
function mapStop(stop, hasTools) {
|
|
303
|
+
if (stop === 'tool_use' && hasTools)
|
|
304
|
+
return 'tool';
|
|
305
|
+
if (stop === 'max_tokens')
|
|
306
|
+
return 'length';
|
|
307
|
+
return 'stop';
|
|
308
|
+
}
|