@jxburros/ai-nugget 0.4.1 → 0.7.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 (187) hide show
  1. package/README.md +158 -382
  2. package/UPGRADING.md +182 -0
  3. package/dist/adapters/engines/anthropic.d.ts +7 -1
  4. package/dist/adapters/engines/anthropic.js +82 -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 +80 -29
  9. package/dist/adapters/engines/ollama.d.ts +2 -1
  10. package/dist/adapters/engines/ollama.js +57 -15
  11. package/dist/adapters/engines/openaiChat.d.ts +8 -1
  12. package/dist/adapters/engines/openaiChat.js +109 -29
  13. package/dist/adapters/profiles.d.ts +21 -1
  14. package/dist/adapters/profiles.js +12 -2
  15. package/dist/agent/loop.d.ts +45 -1
  16. package/dist/agent/loop.js +117 -12
  17. package/dist/cjs/adapters/engines/anthropic.js +326 -0
  18. package/dist/cjs/adapters/engines/base.js +147 -0
  19. package/dist/cjs/adapters/engines/google.js +291 -0
  20. package/dist/cjs/adapters/engines/ollama.js +205 -0
  21. package/dist/cjs/adapters/engines/openaiChat.js +312 -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 +373 -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 +586 -0
  30. package/dist/cjs/index.js +28 -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/reasoning.js +159 -0
  36. package/dist/cjs/redact.js +78 -0
  37. package/dist/cjs/tokens.js +22 -0
  38. package/dist/cjs/transport.js +172 -0
  39. package/dist/cjs/types.js +6 -0
  40. package/dist/cjs/util.js +126 -0
  41. package/dist/errors.js +25 -1
  42. package/dist/handler.d.ts +67 -1
  43. package/dist/handler.js +266 -61
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +1 -0
  46. package/dist/reasoning.d.ts +45 -0
  47. package/dist/reasoning.js +154 -0
  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 +112 -2
  53. package/dist/util.d.ts +22 -0
  54. package/dist/util.js +59 -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 +323 -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 +288 -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 +202 -0
  71. package/nugget/dist/adapters/engines/ollama.js.map +1 -0
  72. package/nugget/dist/adapters/engines/openaiChat.d.ts +22 -0
  73. package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
  74. package/nugget/dist/adapters/engines/openaiChat.js +308 -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 +114 -0
  89. package/nugget/dist/agent/loop.d.ts.map +1 -0
  90. package/nugget/dist/agent/loop.js +371 -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 +326 -0
  97. package/nugget/dist/cjs/adapters/engines/base.js +147 -0
  98. package/nugget/dist/cjs/adapters/engines/google.js +291 -0
  99. package/nugget/dist/cjs/adapters/engines/ollama.js +205 -0
  100. package/nugget/dist/cjs/adapters/engines/openaiChat.js +312 -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 +373 -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 +586 -0
  109. package/nugget/dist/cjs/index.js +28 -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/reasoning.js +159 -0
  115. package/nugget/dist/cjs/redact.js +78 -0
  116. package/nugget/dist/cjs/tokens.js +22 -0
  117. package/nugget/dist/cjs/transport.js +172 -0
  118. package/nugget/dist/cjs/types.js +6 -0
  119. package/nugget/dist/cjs/util.js +126 -0
  120. package/nugget/dist/connect.d.ts +31 -0
  121. package/nugget/dist/connect.d.ts.map +1 -0
  122. package/nugget/dist/connect.js +25 -0
  123. package/nugget/dist/connect.js.map +1 -0
  124. package/nugget/dist/errors.d.ts +22 -0
  125. package/nugget/dist/errors.d.ts.map +1 -0
  126. package/nugget/dist/errors.js +104 -0
  127. package/nugget/dist/errors.js.map +1 -0
  128. package/nugget/dist/handler.d.ts +114 -0
  129. package/nugget/dist/handler.d.ts.map +1 -0
  130. package/nugget/dist/handler.js +582 -0
  131. package/nugget/dist/handler.js.map +1 -0
  132. package/nugget/dist/index.d.ts +13 -0
  133. package/nugget/dist/index.d.ts.map +1 -0
  134. package/nugget/dist/index.js +13 -0
  135. package/nugget/dist/index.js.map +1 -0
  136. package/nugget/dist/json.d.ts +7 -0
  137. package/nugget/dist/json.d.ts.map +1 -0
  138. package/nugget/dist/json.js +114 -0
  139. package/nugget/dist/json.js.map +1 -0
  140. package/nugget/dist/keys.d.ts +7 -0
  141. package/nugget/dist/keys.d.ts.map +1 -0
  142. package/nugget/dist/keys.js +75 -0
  143. package/nugget/dist/keys.js.map +1 -0
  144. package/nugget/dist/policy.d.ts +6 -0
  145. package/nugget/dist/policy.d.ts.map +1 -0
  146. package/nugget/dist/policy.js +37 -0
  147. package/nugget/dist/policy.js.map +1 -0
  148. package/nugget/dist/reasoning.d.ts +45 -0
  149. package/nugget/dist/reasoning.d.ts.map +1 -0
  150. package/nugget/dist/reasoning.js +154 -0
  151. package/nugget/dist/reasoning.js.map +1 -0
  152. package/nugget/dist/redact.d.ts +26 -0
  153. package/nugget/dist/redact.d.ts.map +1 -0
  154. package/nugget/dist/redact.js +74 -0
  155. package/nugget/dist/redact.js.map +1 -0
  156. package/nugget/dist/tokens.d.ts +5 -0
  157. package/nugget/dist/tokens.d.ts.map +1 -0
  158. package/nugget/dist/tokens.js +18 -0
  159. package/nugget/dist/tokens.js.map +1 -0
  160. package/nugget/dist/transport.d.ts +30 -0
  161. package/nugget/dist/transport.d.ts.map +1 -0
  162. package/nugget/dist/transport.js +165 -0
  163. package/nugget/dist/transport.js.map +1 -0
  164. package/nugget/dist/types.d.ts +301 -0
  165. package/nugget/dist/types.d.ts.map +1 -0
  166. package/nugget/dist/types.js +4 -0
  167. package/nugget/dist/types.js.map +1 -0
  168. package/nugget/dist/util.d.ts +31 -0
  169. package/nugget/dist/util.d.ts.map +1 -0
  170. package/nugget/dist/util.js +114 -0
  171. package/nugget/dist/util.js.map +1 -0
  172. package/nugget/src/adapters/engines/anthropic.ts +78 -37
  173. package/nugget/src/adapters/engines/base.ts +50 -7
  174. package/nugget/src/adapters/engines/google.ts +79 -29
  175. package/nugget/src/adapters/engines/ollama.ts +56 -17
  176. package/nugget/src/adapters/engines/openaiChat.ts +104 -30
  177. package/nugget/src/adapters/profiles.ts +53 -3
  178. package/nugget/src/agent/loop.ts +156 -13
  179. package/nugget/src/errors.ts +25 -1
  180. package/nugget/src/handler.ts +308 -59
  181. package/nugget/src/index.ts +1 -0
  182. package/nugget/src/reasoning.ts +174 -0
  183. package/nugget/src/redact.ts +32 -1
  184. package/nugget/src/transport.ts +20 -1
  185. package/nugget/src/types.ts +113 -1
  186. package/nugget/src/util.ts +64 -0
  187. package/package.json +18 -5
@@ -1,5 +1,5 @@
1
1
  import type { AIHandler } from '../handler.js';
2
- import type { ChatMessage, Connection, StreamEvent, ToolCall, Usage } from '../types.js';
2
+ import type { AIErrorKind, ChatMessage, Connection, ReasoningEffort, StreamEvent, ToolCall, Usage } from '../types.js';
3
3
  import type { ToolSpec } from './tools.js';
4
4
  export interface AgentOptions {
5
5
  handler: AIHandler;
@@ -18,12 +18,45 @@ export interface AgentOptions {
18
18
  * dependent, not protocol-guaranteed.
19
19
  */
20
20
  toolMode?: 'native' | 'promptJson' | 'auto';
21
+ /**
22
+ * Discovered capabilities of the specific model (e.g. Ollama `/api/show`
23
+ * `["tools"]`). When `toolMode` is `auto`, a `tools` capability here upgrades a
24
+ * local model to `native` tool-calling instead of the profile's conservative
25
+ * `promptJson` default — so a capable local model isn't forced onto the fallback.
26
+ */
27
+ modelCapabilities?: string[];
28
+ /** Sampler/decoding options forwarded to every model turn (parity with a plain chat call). */
29
+ temperature?: number;
30
+ maxTokens?: number;
31
+ topP?: number;
32
+ stopSequences?: string[];
33
+ /** Reasoning effort forwarded to every turn — see {@link ChatRequest.reasoningEffort}. */
34
+ reasoningEffort?: ReasoningEffort;
35
+ /** Provider-native passthrough forwarded to every turn — see {@link ChatRequest.providerOptions}. */
36
+ providerOptions?: Record<string, unknown>;
21
37
  budget?: {
22
38
  maxSteps?: number;
23
39
  maxTokens?: number;
24
40
  deadlineMs?: number;
25
41
  };
26
42
  approval?: ApprovalGate;
43
+ /**
44
+ * Which tools require the approval gate. `sideEffectsOnly` (default) gates only
45
+ * tools declared `sideEffects: true`; `all` gates every tool — so `tool_denied`
46
+ * is reachable without marking each tool, and a consumer can require approval
47
+ * for a whole run in one place.
48
+ */
49
+ approvalMode?: 'sideEffectsOnly' | 'all';
50
+ /**
51
+ * Bounds and framing applied to each tool result before it re-enters the
52
+ * model's context. `maxChars` truncates oversized results (with a notice) so a
53
+ * single large return can't blow the next turn's window; `wrapUntrusted` fences
54
+ * the result in an `<untrusted_tool_output>` envelope. Both are opt-in.
55
+ */
56
+ toolResult?: {
57
+ maxChars?: number;
58
+ wrapUntrusted?: boolean;
59
+ };
27
60
  onEvent?: (e: AgentEvent) => void;
28
61
  signal?: AbortSignal;
29
62
  metadata?: Record<string, unknown>;
@@ -38,6 +71,9 @@ export type ApprovalGate = (req: {
38
71
  export type AgentEvent = StreamEvent | {
39
72
  type: 'step_start';
40
73
  step: number;
74
+ } | {
75
+ type: 'tool_mode';
76
+ mode: 'native' | 'promptJson';
41
77
  } | {
42
78
  type: 'tool_start';
43
79
  step: number;
@@ -63,6 +99,14 @@ export interface AgentResult {
63
99
  usage: Usage;
64
100
  steps: number;
65
101
  stopReason: 'finished' | 'max_steps' | 'budget' | 'deadline' | 'canceled' | 'error';
102
+ /**
103
+ * Populated when `stopReason` is `error`: the failure that ended the run, so
104
+ * the caller can see what went wrong without replaying the event stream.
105
+ */
106
+ error?: {
107
+ kind: AIErrorKind;
108
+ message: string;
109
+ };
66
110
  }
67
111
  export declare function runAgent(opts: AgentOptions): AsyncIterable<AgentEvent> & {
68
112
  result: Promise<AgentResult>;
@@ -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,33 @@ 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
+ reasoningEffort: opts.reasoningEffort,
50
+ providerOptions: opts.providerOptions,
38
51
  signal: agentSignal.signal,
39
52
  metadata: { ...opts.metadata, agentStep: step },
40
53
  })) {
41
- if (event.type === 'delta')
54
+ if (event.type === 'delta') {
42
55
  stepText += event.text;
56
+ if (filter) {
57
+ const visible = filter.push(event.text);
58
+ if (visible)
59
+ yield emit(opts, { type: 'delta', text: visible });
60
+ continue;
61
+ }
62
+ }
43
63
  if (event.type === 'tool_call')
44
64
  calls.push(event.call);
45
65
  if (event.type === 'error')
@@ -51,10 +71,17 @@ async function* run(opts, resolveResult) {
51
71
  }
52
72
  yield emit(opts, event);
53
73
  }
74
+ // If withheld text turned out not to be a real directive, flush it so the
75
+ // user still sees the model's answer.
76
+ if (filter && calls.length === 0) {
77
+ const flushed = filter.flush();
78
+ if (flushed)
79
+ yield emit(opts, { type: 'delta', text: flushed });
80
+ }
54
81
  // A handler-level failure (auth, policy, cancel, exhausted retries) must
55
82
  // stop the loop honestly rather than looking like an empty completion.
56
83
  if (streamFailure)
57
- return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : streamFailure.kind === 'canceled' ? 'canceled' : 'error');
84
+ return yield* yieldDone(agentSignal.timedOut() ? 'deadline' : streamFailure.kind === 'canceled' ? 'canceled' : 'error', errorInfo(streamFailure));
58
85
  finalText = stepText;
59
86
  messages.push(toolMode === 'promptJson'
60
87
  ? { role: 'assistant', content: stepText }
@@ -75,7 +102,8 @@ async function* run(opts, resolveResult) {
75
102
  continue;
76
103
  }
77
104
  let args = validation.args;
78
- if (tool.sideEffects) {
105
+ const requiresApproval = tool.sideEffects || opts.approvalMode === 'all';
106
+ if (requiresApproval) {
79
107
  if (!opts.approval) {
80
108
  yield* appendToolDenied(opts, messages, step, call, 'No approval gate configured');
81
109
  continue;
@@ -115,8 +143,16 @@ async function* run(opts, resolveResult) {
115
143
  continue;
116
144
  }
117
145
  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 });
146
+ // Guard against a tool returning `undefined` (JSON.stringify(undefined)
147
+ // is the value `undefined`, not a string) — an unguarded value here
148
+ // used to crash the next step's message mapper and kill the whole run.
149
+ const serialized = JSON.stringify(executed.result) ?? 'null';
150
+ messages.push({ role: 'tool', toolCallId: call.id, name: call.name, content: formatToolResult(serialized, opts.toolResult) });
151
+ // A tool that returns a structured `{ ok: false, ... }` is a recoverable
152
+ // error the model should react to — surface it as an error result.
153
+ const record = asRecord(executed.result);
154
+ const isError = record?.ok === false;
155
+ yield emit(opts, { type: 'tool_result', step, call, result: executed.result, isError });
120
156
  }
121
157
  catch (error) {
122
158
  const message = error instanceof Error ? error.message : 'Tool result was not serializable';
@@ -131,7 +167,8 @@ async function* run(opts, resolveResult) {
131
167
  return yield* yieldDone('max_steps');
132
168
  }
133
169
  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');
170
+ const stopReason = agentSignal.timedOut() ? 'deadline' : (agentSignal.signal?.aborted || (error instanceof AIError && error.kind === 'canceled')) ? 'canceled' : 'error';
171
+ const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason, stopReason === 'error' ? errorInfo(error) : undefined);
135
172
  settled = true;
136
173
  resolveResult(result);
137
174
  yield emit(opts, { type: 'agent_done', result });
@@ -143,8 +180,8 @@ async function* run(opts, resolveResult) {
143
180
  resolveResult(result);
144
181
  }
145
182
  }
146
- function* yieldDone(stopReason) {
147
- const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason);
183
+ function* yieldDone(stopReason, error) {
184
+ const result = makeResult(finalText, messages, usage ?? { estimated: true }, step, stopReason, error);
148
185
  settled = true;
149
186
  resolveResult(result);
150
187
  yield emit(opts, { type: 'agent_done', result });
@@ -154,8 +191,67 @@ function emit(opts, event) {
154
191
  opts.onEvent?.(event);
155
192
  return event;
156
193
  }
157
- function makeResult(finalText, messages, usage, steps, stopReason) {
158
- return { finalText, messages, usage, steps, stopReason };
194
+ function makeResult(finalText, messages, usage, steps, stopReason, error) {
195
+ return error ? { finalText, messages, usage, steps, stopReason, error } : { finalText, messages, usage, steps, stopReason };
196
+ }
197
+ function errorInfo(error) {
198
+ if (error instanceof AIError)
199
+ return { kind: error.kind, message: error.message };
200
+ return { kind: 'tool_error', message: error instanceof Error ? error.message : 'Agent run failed' };
201
+ }
202
+ /**
203
+ * Applies the optional size cap and untrusted-content framing to a serialized
204
+ * tool result before it re-enters the model's context.
205
+ */
206
+ function formatToolResult(serialized, opts) {
207
+ let out = serialized;
208
+ const max = opts?.maxChars;
209
+ if (max !== undefined && out.length > max) {
210
+ out = `${out.slice(0, max)}\n…[truncated ${out.length - max} chars]`;
211
+ }
212
+ if (opts?.wrapUntrusted) {
213
+ out = `<untrusted_tool_output>\n${out}\n</untrusted_tool_output>`;
214
+ }
215
+ return out;
216
+ }
217
+ /**
218
+ * Withholds a promptJson tool directive from the visible `delta` stream. Once
219
+ * the leading non-whitespace character is known: a `{`, `[`, or fence marks a
220
+ * directive candidate whose text is buffered (never streamed); anything else is
221
+ * prose that streams through untouched. `flush()` returns any buffered text when
222
+ * the stream ended without yielding a parsed directive.
223
+ */
224
+ class PromptJsonDeltaFilter {
225
+ decided = false;
226
+ withhold = false;
227
+ buffer = '';
228
+ push(text) {
229
+ if (!this.decided) {
230
+ this.buffer += text;
231
+ const lead = this.buffer.replace(/^\s+/, '');
232
+ if (lead.length === 0)
233
+ return '';
234
+ this.decided = true;
235
+ this.withhold = /^[[{`]/.test(lead);
236
+ if (this.withhold)
237
+ return '';
238
+ const flushed = this.buffer;
239
+ this.buffer = '';
240
+ return flushed;
241
+ }
242
+ if (this.withhold) {
243
+ this.buffer += text;
244
+ return '';
245
+ }
246
+ return text;
247
+ }
248
+ flush() {
249
+ if (!this.withhold)
250
+ return '';
251
+ const out = this.buffer;
252
+ this.buffer = '';
253
+ return out;
254
+ }
159
255
  }
160
256
  function* appendToolError(opts, messages, step, call, message) {
161
257
  const result = { error: message };
@@ -170,13 +266,22 @@ function resolveToolMode(opts) {
170
266
  const mode = opts.toolMode ?? 'auto';
171
267
  if (mode !== 'auto')
172
268
  return mode;
269
+ // A model that actually advertises tool support gets native tool-calling even
270
+ // on a local runtime whose profile is conservatively `nativeTools: false`.
271
+ if (opts.modelCapabilities?.includes('tools'))
272
+ return 'native';
173
273
  return profileFor(opts.connection.provider, opts.connection.baseUrl).capabilities.nativeTools ? 'native' : 'promptJson';
174
274
  }
175
275
  function withPromptJsonInstruction(messages, tools) {
276
+ // Include each tool's parameter schema, not just its description, so the model
277
+ // knows the argument shape instead of guessing it (FR‑3).
278
+ const catalog = tools
279
+ .map((tool) => `- ${tool.name}: ${tool.description}\n parameters: ${JSON.stringify(tool.parameters)}`)
280
+ .join('\n');
176
281
  return [
177
282
  {
178
283
  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('; ')}`,
284
+ 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
285
  },
181
286
  ...messages.map(toPromptJsonMessage),
182
287
  ];
@@ -0,0 +1,326 @@
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
+ if (req.reasoningEffort !== undefined) {
218
+ // First-class reasoning effort → extended thinking. Anthropic requires
219
+ // `budget_tokens < max_tokens` and rejects temperature/top_p alongside
220
+ // thinking, so the budget is fitted under max_tokens (raising max_tokens
221
+ // when the caller's value leaves no room) and the samplers are dropped.
222
+ if (req.reasoningEffort === 'none') {
223
+ base.thinking = { type: 'disabled' };
224
+ }
225
+ else {
226
+ const budget = util_js_1.REASONING_BUDGET_TOKENS[req.reasoningEffort];
227
+ const maxTokens = typeof base.max_tokens === 'number' ? base.max_tokens : 4096;
228
+ if (maxTokens <= budget)
229
+ base.max_tokens = budget + 1024;
230
+ base.thinking = { type: 'enabled', budget_tokens: budget };
231
+ delete base.temperature;
232
+ delete base.top_p;
233
+ }
234
+ }
235
+ // providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
236
+ // top-level `cache_control` extras, `service_tier`, …) without a release.
237
+ return (0, util_js_1.applyProviderOptions)(base, req.providerOptions);
238
+ }
239
+ function toAnthropicMessages(messages) {
240
+ const out = [];
241
+ for (let index = 0; index < messages.length; index += 1) {
242
+ const message = messages[index];
243
+ if (message.role !== 'tool') {
244
+ out.push(toAnthropicMessage(message));
245
+ continue;
246
+ }
247
+ const content = [];
248
+ while (index < messages.length && messages[index]?.role === 'tool') {
249
+ const toolMessage = messages[index];
250
+ content.push({ type: 'tool_result', tool_use_id: toolMessage.toolCallId ?? '', content: textContent(toolMessage.content) });
251
+ index += 1;
252
+ }
253
+ index -= 1;
254
+ out.push({ role: 'user', content });
255
+ }
256
+ return out;
257
+ }
258
+ function toAnthropicMessage(m) {
259
+ // Tool-role messages are handled by toAnthropicMessages' batching loop and
260
+ // never reach here, so no tool branch is needed.
261
+ // assistant turns that carried tool calls replay them as tool_use blocks,
262
+ // preserving any text/image content parts that accompanied the calls.
263
+ if (m.role === 'assistant' && m.toolCalls?.length) {
264
+ const content = [...contentBlocks(m.content)];
265
+ for (const call of m.toolCalls)
266
+ content.push({ type: 'tool_use', id: call.id, name: call.name, input: call.arguments ?? {} });
267
+ return { role: 'assistant', content };
268
+ }
269
+ return {
270
+ role: m.role === 'assistant' ? 'assistant' : 'user',
271
+ content: typeof m.content === 'string' ? m.content : contentBlocks(m.content),
272
+ };
273
+ }
274
+ /** Maps text/image content — a string or a ContentPart[] — onto Anthropic content blocks. */
275
+ function contentBlocks(content) {
276
+ if (typeof content === 'string')
277
+ return content ? [{ type: 'text', text: content }] : [];
278
+ return content.map((part) => part.type === 'image'
279
+ ? { type: 'image', source: { type: 'base64', media_type: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
280
+ : { type: 'text', text: part.text ?? '' });
281
+ }
282
+ function textContent(content) {
283
+ if (typeof content === 'string')
284
+ return content;
285
+ if (!content)
286
+ return '';
287
+ return content.filter((part) => part.type === 'text').map((part) => part.text ?? '').join('\n');
288
+ }
289
+ function parseResponse(data) {
290
+ const record = (0, util_js_1.asRecord)(data);
291
+ const content = Array.isArray(record?.content) ? record.content : [];
292
+ let text = '';
293
+ const toolCalls = [];
294
+ for (const blockValue of content) {
295
+ const block = (0, util_js_1.asRecord)(blockValue);
296
+ if (block?.type === 'text')
297
+ text += (0, util_js_1.asString)(block.text) ?? '';
298
+ if (block?.type === 'tool_use') {
299
+ toolCalls.push({
300
+ id: (0, util_js_1.asString)(block.id) ?? (0, base_js_1.randomId)(),
301
+ name: (0, util_js_1.asString)(block.name) ?? 'unknown',
302
+ arguments: block.input ?? {},
303
+ raw: JSON.stringify(block.input ?? {}),
304
+ });
305
+ }
306
+ }
307
+ const usage = (0, util_js_1.asRecord)(record?.usage);
308
+ 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 };
309
+ }
310
+ function schemaFor(req) {
311
+ const schema = req.responseFormat?.type === 'json' ? req.responseFormat.schema : undefined;
312
+ return schema ?? { type: 'object' };
313
+ }
314
+ function jsonTextFrom(toolCalls, fallback) {
315
+ const json = toolCalls.find((call) => call.name === JSON_MODE_TOOL) ?? toolCalls[0];
316
+ if (json)
317
+ return json.raw && json.raw.trim() ? json.raw : JSON.stringify(json.arguments ?? {});
318
+ return fallback;
319
+ }
320
+ function mapStop(stop, hasTools) {
321
+ if (stop === 'tool_use' && hasTools)
322
+ return 'tool';
323
+ if (stop === 'max_tokens')
324
+ return 'length';
325
+ return 'stop';
326
+ }