@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.
Files changed (183) hide show
  1. package/README.md +134 -271
  2. package/UPGRADING.md +158 -0
  3. package/dist/adapters/engines/anthropic.d.ts +7 -1
  4. package/dist/adapters/engines/anthropic.js +64 -37
  5. package/dist/adapters/engines/base.d.ts +29 -2
  6. package/dist/adapters/engines/base.js +52 -8
  7. package/dist/adapters/engines/google.d.ts +17 -1
  8. package/dist/adapters/engines/google.js +93 -29
  9. package/dist/adapters/engines/ollama.d.ts +2 -1
  10. package/dist/adapters/engines/ollama.js +51 -15
  11. package/dist/adapters/engines/openaiChat.d.ts +2 -1
  12. package/dist/adapters/engines/openaiChat.js +60 -30
  13. package/dist/adapters/profiles.d.ts +21 -1
  14. package/dist/adapters/profiles.js +12 -2
  15. package/dist/agent/loop.d.ts +43 -1
  16. package/dist/agent/loop.js +120 -13
  17. package/dist/cjs/adapters/engines/anthropic.js +308 -0
  18. package/dist/cjs/adapters/engines/base.js +147 -0
  19. package/dist/cjs/adapters/engines/google.js +284 -0
  20. package/dist/cjs/adapters/engines/ollama.js +199 -0
  21. package/dist/cjs/adapters/engines/openaiChat.js +261 -0
  22. package/dist/cjs/adapters/index.js +22 -0
  23. package/dist/cjs/adapters/profiles.js +125 -0
  24. package/dist/cjs/agent/index.js +18 -0
  25. package/dist/cjs/agent/loop.js +372 -0
  26. package/dist/cjs/agent/tools.js +51 -0
  27. package/dist/cjs/connect.js +27 -0
  28. package/dist/cjs/errors.js +110 -0
  29. package/dist/cjs/handler.js +561 -0
  30. package/dist/cjs/index.js +27 -0
  31. package/dist/cjs/json.js +121 -0
  32. package/dist/cjs/keys.js +81 -0
  33. package/dist/cjs/package.json +3 -0
  34. package/dist/cjs/policy.js +42 -0
  35. package/dist/cjs/redact.js +78 -0
  36. package/dist/cjs/tokens.js +22 -0
  37. package/dist/cjs/transport.js +172 -0
  38. package/dist/cjs/types.js +6 -0
  39. package/dist/cjs/util.js +112 -0
  40. package/dist/connect.d.ts +31 -0
  41. package/dist/connect.js +25 -0
  42. package/dist/errors.js +25 -1
  43. package/dist/handler.d.ts +61 -1
  44. package/dist/handler.js +247 -61
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/keys.js +1 -4
  48. package/dist/redact.d.ts +18 -0
  49. package/dist/redact.js +29 -1
  50. package/dist/transport.d.ts +3 -1
  51. package/dist/transport.js +21 -1
  52. package/dist/types.d.ts +96 -2
  53. package/dist/util.d.ts +16 -0
  54. package/dist/util.js +50 -0
  55. package/nugget/VERSION.txt +2 -2
  56. package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
  57. package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
  58. package/nugget/dist/adapters/engines/anthropic.js +305 -0
  59. package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
  60. package/nugget/dist/adapters/engines/base.d.ts +50 -0
  61. package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
  62. package/nugget/dist/adapters/engines/base.js +137 -0
  63. package/nugget/dist/adapters/engines/base.js.map +1 -0
  64. package/nugget/dist/adapters/engines/google.d.ts +24 -0
  65. package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
  66. package/nugget/dist/adapters/engines/google.js +281 -0
  67. package/nugget/dist/adapters/engines/google.js.map +1 -0
  68. package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
  69. package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
  70. package/nugget/dist/adapters/engines/ollama.js +196 -0
  71. package/nugget/dist/adapters/engines/ollama.js.map +1 -0
  72. package/nugget/dist/adapters/engines/openaiChat.d.ts +16 -0
  73. package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
  74. package/nugget/dist/adapters/engines/openaiChat.js +258 -0
  75. package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
  76. package/nugget/dist/adapters/index.d.ts +5 -0
  77. package/nugget/dist/adapters/index.d.ts.map +1 -0
  78. package/nugget/dist/adapters/index.js +17 -0
  79. package/nugget/dist/adapters/index.js.map +1 -0
  80. package/nugget/dist/adapters/profiles.d.ts +62 -0
  81. package/nugget/dist/adapters/profiles.d.ts.map +1 -0
  82. package/nugget/dist/adapters/profiles.js +121 -0
  83. package/nugget/dist/adapters/profiles.js.map +1 -0
  84. package/nugget/dist/agent/index.d.ts +3 -0
  85. package/nugget/dist/agent/index.d.ts.map +1 -0
  86. package/nugget/dist/agent/index.js +3 -0
  87. package/nugget/dist/agent/index.js.map +1 -0
  88. package/nugget/dist/agent/loop.d.ts +112 -0
  89. package/nugget/dist/agent/loop.d.ts.map +1 -0
  90. package/nugget/dist/agent/loop.js +370 -0
  91. package/nugget/dist/agent/loop.js.map +1 -0
  92. package/nugget/dist/agent/tools.d.ts +29 -0
  93. package/nugget/dist/agent/tools.d.ts.map +1 -0
  94. package/nugget/dist/agent/tools.js +48 -0
  95. package/nugget/dist/agent/tools.js.map +1 -0
  96. package/nugget/dist/cjs/adapters/engines/anthropic.js +308 -0
  97. package/nugget/dist/cjs/adapters/engines/base.js +147 -0
  98. package/nugget/dist/cjs/adapters/engines/google.js +284 -0
  99. package/nugget/dist/cjs/adapters/engines/ollama.js +199 -0
  100. package/nugget/dist/cjs/adapters/engines/openaiChat.js +261 -0
  101. package/nugget/dist/cjs/adapters/index.js +22 -0
  102. package/nugget/dist/cjs/adapters/profiles.js +125 -0
  103. package/nugget/dist/cjs/agent/index.js +18 -0
  104. package/nugget/dist/cjs/agent/loop.js +372 -0
  105. package/nugget/dist/cjs/agent/tools.js +51 -0
  106. package/nugget/dist/cjs/connect.js +27 -0
  107. package/nugget/dist/cjs/errors.js +110 -0
  108. package/nugget/dist/cjs/handler.js +561 -0
  109. package/nugget/dist/cjs/index.js +27 -0
  110. package/nugget/dist/cjs/json.js +121 -0
  111. package/nugget/dist/cjs/keys.js +81 -0
  112. package/nugget/dist/cjs/package.json +3 -0
  113. package/nugget/dist/cjs/policy.js +42 -0
  114. package/nugget/dist/cjs/redact.js +78 -0
  115. package/nugget/dist/cjs/tokens.js +22 -0
  116. package/nugget/dist/cjs/transport.js +172 -0
  117. package/nugget/dist/cjs/types.js +6 -0
  118. package/nugget/dist/cjs/util.js +112 -0
  119. package/nugget/dist/connect.d.ts +31 -0
  120. package/nugget/dist/connect.d.ts.map +1 -0
  121. package/nugget/dist/connect.js +25 -0
  122. package/nugget/dist/connect.js.map +1 -0
  123. package/nugget/dist/errors.d.ts +22 -0
  124. package/nugget/dist/errors.d.ts.map +1 -0
  125. package/nugget/dist/errors.js +104 -0
  126. package/nugget/dist/errors.js.map +1 -0
  127. package/nugget/dist/handler.d.ts +108 -0
  128. package/nugget/dist/handler.d.ts.map +1 -0
  129. package/nugget/dist/handler.js +557 -0
  130. package/nugget/dist/handler.js.map +1 -0
  131. package/nugget/dist/index.d.ts +12 -0
  132. package/nugget/dist/index.d.ts.map +1 -0
  133. package/nugget/dist/index.js +12 -0
  134. package/nugget/dist/index.js.map +1 -0
  135. package/nugget/dist/json.d.ts +7 -0
  136. package/nugget/dist/json.d.ts.map +1 -0
  137. package/nugget/dist/json.js +114 -0
  138. package/nugget/dist/json.js.map +1 -0
  139. package/nugget/dist/keys.d.ts +7 -0
  140. package/nugget/dist/keys.d.ts.map +1 -0
  141. package/nugget/dist/keys.js +75 -0
  142. package/nugget/dist/keys.js.map +1 -0
  143. package/nugget/dist/policy.d.ts +6 -0
  144. package/nugget/dist/policy.d.ts.map +1 -0
  145. package/nugget/dist/policy.js +37 -0
  146. package/nugget/dist/policy.js.map +1 -0
  147. package/nugget/dist/redact.d.ts +26 -0
  148. package/nugget/dist/redact.d.ts.map +1 -0
  149. package/nugget/dist/redact.js +74 -0
  150. package/nugget/dist/redact.js.map +1 -0
  151. package/nugget/dist/tokens.d.ts +5 -0
  152. package/nugget/dist/tokens.d.ts.map +1 -0
  153. package/nugget/dist/tokens.js +18 -0
  154. package/nugget/dist/tokens.js.map +1 -0
  155. package/nugget/dist/transport.d.ts +30 -0
  156. package/nugget/dist/transport.d.ts.map +1 -0
  157. package/nugget/dist/transport.js +165 -0
  158. package/nugget/dist/transport.js.map +1 -0
  159. package/nugget/dist/types.d.ts +285 -0
  160. package/nugget/dist/types.d.ts.map +1 -0
  161. package/nugget/dist/types.js +4 -0
  162. package/nugget/dist/types.js.map +1 -0
  163. package/nugget/dist/util.d.ts +24 -0
  164. package/nugget/dist/util.d.ts.map +1 -0
  165. package/nugget/dist/util.js +101 -0
  166. package/nugget/dist/util.js.map +1 -0
  167. package/nugget/src/adapters/engines/anthropic.ts +62 -37
  168. package/nugget/src/adapters/engines/base.ts +50 -7
  169. package/nugget/src/adapters/engines/google.ts +92 -29
  170. package/nugget/src/adapters/engines/ollama.ts +51 -17
  171. package/nugget/src/adapters/engines/openaiChat.ts +61 -32
  172. package/nugget/src/adapters/profiles.ts +53 -3
  173. package/nugget/src/agent/loop.ts +158 -14
  174. package/nugget/src/connect.ts +48 -0
  175. package/nugget/src/errors.ts +25 -1
  176. package/nugget/src/handler.ts +286 -59
  177. package/nugget/src/index.ts +1 -0
  178. package/nugget/src/keys.ts +1 -5
  179. package/nugget/src/redact.ts +32 -1
  180. package/nugget/src/transport.ts +20 -1
  181. package/nugget/src/types.ts +96 -1
  182. package/nugget/src/util.ts +55 -0
  183. package/package.json +20 -6
@@ -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
- if (tool.sideEffects) {
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
- messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: JSON.stringify(executed.result) });
119
- yield emit(opts, { type: 'tool_result', step, call, result: executed.result, isError: false });
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 result = makeResult(finalText, messages, usage ?? { estimated: true }, step, agentSignal.timedOut() ? 'deadline' : (agentSignal.signal?.aborted || (error instanceof AIError && error.kind === 'canceled')) ? 'canceled' : 'error');
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":{...}}]}. Available tools: ${tools.map((tool) => `${tool.name}: ${tool.description}`).join('; ')}`,
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: crypto.randomUUID(),
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
+ }