@jxburros/ai-nugget 0.4.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 (175) hide show
  1. package/README.md +132 -385
  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 +7 -1
  8. package/dist/adapters/engines/google.js +73 -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 +116 -12
  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/errors.js +25 -1
  41. package/dist/handler.d.ts +61 -1
  42. package/dist/handler.js +239 -59
  43. package/dist/redact.d.ts +18 -0
  44. package/dist/redact.js +29 -1
  45. package/dist/transport.d.ts +3 -1
  46. package/dist/transport.js +21 -1
  47. package/dist/types.d.ts +96 -2
  48. package/dist/util.d.ts +15 -0
  49. package/dist/util.js +46 -0
  50. package/nugget/VERSION.txt +2 -2
  51. package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
  52. package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
  53. package/nugget/dist/adapters/engines/anthropic.js +305 -0
  54. package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
  55. package/nugget/dist/adapters/engines/base.d.ts +50 -0
  56. package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
  57. package/nugget/dist/adapters/engines/base.js +137 -0
  58. package/nugget/dist/adapters/engines/base.js.map +1 -0
  59. package/nugget/dist/adapters/engines/google.d.ts +24 -0
  60. package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
  61. package/nugget/dist/adapters/engines/google.js +281 -0
  62. package/nugget/dist/adapters/engines/google.js.map +1 -0
  63. package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
  64. package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
  65. package/nugget/dist/adapters/engines/ollama.js +196 -0
  66. package/nugget/dist/adapters/engines/ollama.js.map +1 -0
  67. package/nugget/dist/adapters/engines/openaiChat.d.ts +16 -0
  68. package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
  69. package/nugget/dist/adapters/engines/openaiChat.js +258 -0
  70. package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
  71. package/nugget/dist/adapters/index.d.ts +5 -0
  72. package/nugget/dist/adapters/index.d.ts.map +1 -0
  73. package/nugget/dist/adapters/index.js +17 -0
  74. package/nugget/dist/adapters/index.js.map +1 -0
  75. package/nugget/dist/adapters/profiles.d.ts +62 -0
  76. package/nugget/dist/adapters/profiles.d.ts.map +1 -0
  77. package/nugget/dist/adapters/profiles.js +121 -0
  78. package/nugget/dist/adapters/profiles.js.map +1 -0
  79. package/nugget/dist/agent/index.d.ts +3 -0
  80. package/nugget/dist/agent/index.d.ts.map +1 -0
  81. package/nugget/dist/agent/index.js +3 -0
  82. package/nugget/dist/agent/index.js.map +1 -0
  83. package/nugget/dist/agent/loop.d.ts +112 -0
  84. package/nugget/dist/agent/loop.d.ts.map +1 -0
  85. package/nugget/dist/agent/loop.js +370 -0
  86. package/nugget/dist/agent/loop.js.map +1 -0
  87. package/nugget/dist/agent/tools.d.ts +29 -0
  88. package/nugget/dist/agent/tools.d.ts.map +1 -0
  89. package/nugget/dist/agent/tools.js +48 -0
  90. package/nugget/dist/agent/tools.js.map +1 -0
  91. package/nugget/dist/cjs/adapters/engines/anthropic.js +308 -0
  92. package/nugget/dist/cjs/adapters/engines/base.js +147 -0
  93. package/nugget/dist/cjs/adapters/engines/google.js +284 -0
  94. package/nugget/dist/cjs/adapters/engines/ollama.js +199 -0
  95. package/nugget/dist/cjs/adapters/engines/openaiChat.js +261 -0
  96. package/nugget/dist/cjs/adapters/index.js +22 -0
  97. package/nugget/dist/cjs/adapters/profiles.js +125 -0
  98. package/nugget/dist/cjs/agent/index.js +18 -0
  99. package/nugget/dist/cjs/agent/loop.js +372 -0
  100. package/nugget/dist/cjs/agent/tools.js +51 -0
  101. package/nugget/dist/cjs/connect.js +27 -0
  102. package/nugget/dist/cjs/errors.js +110 -0
  103. package/nugget/dist/cjs/handler.js +561 -0
  104. package/nugget/dist/cjs/index.js +27 -0
  105. package/nugget/dist/cjs/json.js +121 -0
  106. package/nugget/dist/cjs/keys.js +81 -0
  107. package/nugget/dist/cjs/package.json +3 -0
  108. package/nugget/dist/cjs/policy.js +42 -0
  109. package/nugget/dist/cjs/redact.js +78 -0
  110. package/nugget/dist/cjs/tokens.js +22 -0
  111. package/nugget/dist/cjs/transport.js +172 -0
  112. package/nugget/dist/cjs/types.js +6 -0
  113. package/nugget/dist/cjs/util.js +112 -0
  114. package/nugget/dist/connect.d.ts +31 -0
  115. package/nugget/dist/connect.d.ts.map +1 -0
  116. package/nugget/dist/connect.js +25 -0
  117. package/nugget/dist/connect.js.map +1 -0
  118. package/nugget/dist/errors.d.ts +22 -0
  119. package/nugget/dist/errors.d.ts.map +1 -0
  120. package/nugget/dist/errors.js +104 -0
  121. package/nugget/dist/errors.js.map +1 -0
  122. package/nugget/dist/handler.d.ts +108 -0
  123. package/nugget/dist/handler.d.ts.map +1 -0
  124. package/nugget/dist/handler.js +557 -0
  125. package/nugget/dist/handler.js.map +1 -0
  126. package/nugget/dist/index.d.ts +12 -0
  127. package/nugget/dist/index.d.ts.map +1 -0
  128. package/nugget/dist/index.js +12 -0
  129. package/nugget/dist/index.js.map +1 -0
  130. package/nugget/dist/json.d.ts +7 -0
  131. package/nugget/dist/json.d.ts.map +1 -0
  132. package/nugget/dist/json.js +114 -0
  133. package/nugget/dist/json.js.map +1 -0
  134. package/nugget/dist/keys.d.ts +7 -0
  135. package/nugget/dist/keys.d.ts.map +1 -0
  136. package/nugget/dist/keys.js +75 -0
  137. package/nugget/dist/keys.js.map +1 -0
  138. package/nugget/dist/policy.d.ts +6 -0
  139. package/nugget/dist/policy.d.ts.map +1 -0
  140. package/nugget/dist/policy.js +37 -0
  141. package/nugget/dist/policy.js.map +1 -0
  142. package/nugget/dist/redact.d.ts +26 -0
  143. package/nugget/dist/redact.d.ts.map +1 -0
  144. package/nugget/dist/redact.js +74 -0
  145. package/nugget/dist/redact.js.map +1 -0
  146. package/nugget/dist/tokens.d.ts +5 -0
  147. package/nugget/dist/tokens.d.ts.map +1 -0
  148. package/nugget/dist/tokens.js +18 -0
  149. package/nugget/dist/tokens.js.map +1 -0
  150. package/nugget/dist/transport.d.ts +30 -0
  151. package/nugget/dist/transport.d.ts.map +1 -0
  152. package/nugget/dist/transport.js +165 -0
  153. package/nugget/dist/transport.js.map +1 -0
  154. package/nugget/dist/types.d.ts +285 -0
  155. package/nugget/dist/types.d.ts.map +1 -0
  156. package/nugget/dist/types.js +4 -0
  157. package/nugget/dist/types.js.map +1 -0
  158. package/nugget/dist/util.d.ts +24 -0
  159. package/nugget/dist/util.d.ts.map +1 -0
  160. package/nugget/dist/util.js +101 -0
  161. package/nugget/dist/util.js.map +1 -0
  162. package/nugget/src/adapters/engines/anthropic.ts +62 -37
  163. package/nugget/src/adapters/engines/base.ts +50 -7
  164. package/nugget/src/adapters/engines/google.ts +72 -29
  165. package/nugget/src/adapters/engines/ollama.ts +51 -17
  166. package/nugget/src/adapters/engines/openaiChat.ts +61 -32
  167. package/nugget/src/adapters/profiles.ts +53 -3
  168. package/nugget/src/agent/loop.ts +153 -13
  169. package/nugget/src/errors.ts +25 -1
  170. package/nugget/src/handler.ts +279 -57
  171. package/nugget/src/redact.ts +32 -1
  172. package/nugget/src/transport.ts +20 -1
  173. package/nugget/src/types.ts +96 -1
  174. package/nugget/src/util.ts +50 -0
  175. package/package.json +18 -5
@@ -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
  ];
@@ -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
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_TIMEOUT_MS = void 0;
4
+ exports.streamError = streamError;
5
+ exports.streamTimeout = streamTimeout;
6
+ exports.listOpenModels = listOpenModels;
7
+ exports.health = health;
8
+ exports.parseArgs = parseArgs;
9
+ exports.safeParse = safeParse;
10
+ exports.randomId = randomId;
11
+ exports.streamAnomaly = streamAnomaly;
12
+ const errors_js_1 = require("../../errors.js");
13
+ const transport_js_1 = require("../../transport.js");
14
+ const util_js_1 = require("../../util.js");
15
+ exports.DEFAULT_TIMEOUT_MS = 120_000;
16
+ /**
17
+ * Maps a raw streaming failure onto a typed {@link AIError}. A fired timeout is
18
+ * reported as `timeout` regardless of how the underlying `fetch` surfaced the
19
+ * abort; anything else is normalized through {@link fromUnknown} (which passes
20
+ * an existing {@link AIError} through untouched, preserving retryability).
21
+ */
22
+ function streamError(error, timeout, provider) {
23
+ if (timeout.timedOut())
24
+ return new errors_js_1.AIError('Request timed out', { kind: 'timeout', provider });
25
+ return (0, errors_js_1.fromUnknown)(error, provider);
26
+ }
27
+ /**
28
+ * Create a timeout/abort scope covering the full stream lifetime, plus an
29
+ * optional idle timeout (`conn.idleTimeoutMs`) that the engine rearms via
30
+ * `bump()` on each received chunk.
31
+ */
32
+ function streamTimeout(conn, signal) {
33
+ return (0, transport_js_1.withTimeout)(conn.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS, signal, conn.idleTimeoutMs);
34
+ }
35
+ async function listOpenModels(conn, profile) {
36
+ if (!profile.listModelsPath)
37
+ return [];
38
+ const { data } = await (0, transport_js_1.fetchJson)((0, util_js_1.joinUrl)(conn.baseUrl, profile.listModelsPath), {
39
+ method: 'GET',
40
+ headers: conn.headers,
41
+ timeoutMs: conn.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS,
42
+ provider: conn.provider,
43
+ });
44
+ const record = (0, util_js_1.asRecord)(data);
45
+ const items = Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
46
+ const models = [];
47
+ for (const item of items) {
48
+ const row = (0, util_js_1.asRecord)(item);
49
+ const id = (0, util_js_1.asString)(row?.id) ?? (0, util_js_1.asString)(row?.name);
50
+ if (!id)
51
+ continue;
52
+ const contextWindow = asContextWindow(row);
53
+ const capabilities = asCapabilities(row);
54
+ const model = {
55
+ id,
56
+ source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
57
+ };
58
+ if (contextWindow !== undefined)
59
+ model.contextWindow = contextWindow;
60
+ if (capabilities)
61
+ model.capabilities = capabilities;
62
+ models.push(model);
63
+ }
64
+ return models;
65
+ }
66
+ async function health(conn, profile) {
67
+ const path = profile.healthPath ?? profile.listModelsPath;
68
+ if (!path)
69
+ return { ok: true };
70
+ try {
71
+ await (0, transport_js_1.fetchJson)((0, util_js_1.joinUrl)(conn.baseUrl, path), {
72
+ method: 'GET',
73
+ headers: conn.headers,
74
+ timeoutMs: Math.min(conn.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS, 10_000),
75
+ provider: conn.provider,
76
+ });
77
+ return { ok: true };
78
+ }
79
+ catch (error) {
80
+ return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
81
+ }
82
+ }
83
+ /**
84
+ * Coerce a provider's tool-call `arguments` into an object. Providers disagree
85
+ * on the shape: OpenAI/Anthropic stream a JSON *string*, Google and Ollama send
86
+ * a native object — but Ollama-compatible backends (llama.cpp and friends)
87
+ * sometimes send a string too. Malformed JSON degrades to `{}` so a single bad
88
+ * tool call surfaces as an argument-validation error rather than a stream crash.
89
+ */
90
+ function parseArgs(raw) {
91
+ if (raw === undefined || raw === null)
92
+ return {};
93
+ if (typeof raw !== 'string')
94
+ return raw;
95
+ if (!raw.trim())
96
+ return {};
97
+ try {
98
+ return JSON.parse(raw);
99
+ }
100
+ catch {
101
+ return {};
102
+ }
103
+ }
104
+ /** JSON.parse that returns undefined instead of throwing — for per-line stream frames. */
105
+ function safeParse(line) {
106
+ try {
107
+ return JSON.parse(line);
108
+ }
109
+ catch {
110
+ return undefined;
111
+ }
112
+ }
113
+ /** Stable-enough id for a tool call a provider didn't give one for. */
114
+ function randomId() {
115
+ return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
116
+ }
117
+ /**
118
+ * A stream that ended without the provider's terminal marker (an OpenAI
119
+ * `finish_reason`, an Anthropic `message_delta.stop_reason`, a Google
120
+ * `finishReason`, an Ollama `done`) was almost certainly truncated. Every
121
+ * engine yields this so a dropped connection is diagnosable identically
122
+ * regardless of provider.
123
+ */
124
+ function streamAnomaly(reason = 'stream ended without a terminal finish reason') {
125
+ return { type: 'context', kind: 'stream_anomaly', data: { reason } };
126
+ }
127
+ /**
128
+ * Pulls a context-window figure out of the many shapes providers use for it
129
+ * (OpenRouter `context_length`, OpenAI-style `context_window`, Ollama
130
+ * `/api/show` `model_info` entries surfaced as `contextWindow`).
131
+ */
132
+ function asContextWindow(row) {
133
+ if (!row)
134
+ return undefined;
135
+ const direct = row['context_length'] ?? row['context_window'] ?? row['contextWindow'];
136
+ return typeof direct === 'number' && Number.isFinite(direct) ? direct : undefined;
137
+ }
138
+ function asCapabilities(row) {
139
+ if (!row)
140
+ return undefined;
141
+ const value = row['capabilities'] ?? (0, util_js_1.asRecord)(row['architecture'])?.['modality'];
142
+ if (Array.isArray(value) && value.every((entry) => typeof entry === 'string'))
143
+ return value;
144
+ if (typeof value === 'string')
145
+ return [value];
146
+ return undefined;
147
+ }