@molecule/api-ai-agents-llm 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-26T04:27:09.169Z
6
+ Generated: 2026-10-27T17:09:52.462Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-agents-llm
@@ -178,6 +178,25 @@ on every turn. Streamed reasoning is retained in `ChatMessage.reasoning`
178
178
  across tool calls, separately from assistant text and tool arguments.
179
179
  Omitting the option does not enable thinking or manufacture reasoning.
180
180
 
181
+ Two further options are OPT-IN and change nothing when omitted:
182
+
183
+ - `input.maxToolResultChars` bounds the `tool_result` copy sent to the model
184
+ (a whole-file or log dump otherwise crowds out the instruction that asked
185
+ for it) and appends an explicit truncation note telling the model the value
186
+ was cut and how to recover. The step record keeps the complete tool result.
187
+ - `input.finalSynthesisOnBudgetExhaustion` spends ONE tool-free turn after
188
+ the `maxSteps` budget is exhausted, so a run that ended on a tool
189
+ invocation still returns a final answer instead of the budget note. That
190
+ turn carries the same history, system, model, thinking, cache, streaming
191
+ and signal settings plus one short generic instruction to answer now from
192
+ the evidence already gathered (without it a model mid-investigation can
193
+ keep planning). Its events reach `onEvent`, its usage is metered, and it
194
+ cannot call tools: a tool request on that turn is never executed and is not
195
+ accepted as a final answer. If the turn produces no answer text, `run()`
196
+ rejects with `AgentRunError` instead of returning the budget note, and the
197
+ message says whether the turn requested another tool or returned no
198
+ content (e.g. only reasoning).
199
+
181
200
  Failure semantics (how to tell failure modes apart):
182
201
 
183
202
  - **Tool failure** (throwing `execute()`, unknown tool name) — NON-fatal:
@@ -192,7 +211,10 @@ Failure semantics (how to tell failure modes apart):
192
211
  `result.output` means the model genuinely produced no text, not that the
193
212
  API failed.
194
213
  - **Step budget exhausted** — resolves normally; `output` is the last
195
- assistant text or an explicit "step budget exhausted" note.
214
+ assistant text or an explicit "step budget exhausted" note. With
215
+ `input.finalSynthesisOnBudgetExhaustion` the output is instead the answer
216
+ from one final tool-free turn, and a synthesis that produced no answer
217
+ rejects (as an `AgentRunError`) rather than resolving with the note.
196
218
 
197
219
  Swappable like any bond: replace this LLM agent with your own
198
220
  `AIAgentsProvider` via `bond('ai-agents', myProvider)` — nothing else changes.
package/dist/index.d.ts CHANGED
@@ -67,6 +67,25 @@
67
67
  * across tool calls, separately from assistant text and tool arguments.
68
68
  * Omitting the option does not enable thinking or manufacture reasoning.
69
69
  *
70
+ * Two further options are OPT-IN and change nothing when omitted:
71
+ *
72
+ * - `input.maxToolResultChars` bounds the `tool_result` copy sent to the model
73
+ * (a whole-file or log dump otherwise crowds out the instruction that asked
74
+ * for it) and appends an explicit truncation note telling the model the value
75
+ * was cut and how to recover. The step record keeps the complete tool result.
76
+ * - `input.finalSynthesisOnBudgetExhaustion` spends ONE tool-free turn after
77
+ * the `maxSteps` budget is exhausted, so a run that ended on a tool
78
+ * invocation still returns a final answer instead of the budget note. That
79
+ * turn carries the same history, system, model, thinking, cache, streaming
80
+ * and signal settings plus one short generic instruction to answer now from
81
+ * the evidence already gathered (without it a model mid-investigation can
82
+ * keep planning). Its events reach `onEvent`, its usage is metered, and it
83
+ * cannot call tools: a tool request on that turn is never executed and is not
84
+ * accepted as a final answer. If the turn produces no answer text, `run()`
85
+ * rejects with `AgentRunError` instead of returning the budget note, and the
86
+ * message says whether the turn requested another tool or returned no
87
+ * content (e.g. only reasoning).
88
+ *
70
89
  * Failure semantics (how to tell failure modes apart):
71
90
  * - **Tool failure** (throwing `execute()`, unknown tool name) — NON-fatal:
72
91
  * recorded on the step with `isError: true` and fed back to the model.
@@ -80,7 +99,10 @@
80
99
  * `result.output` means the model genuinely produced no text, not that the
81
100
  * API failed.
82
101
  * - **Step budget exhausted** — resolves normally; `output` is the last
83
- * assistant text or an explicit "step budget exhausted" note.
102
+ * assistant text or an explicit "step budget exhausted" note. With
103
+ * `input.finalSynthesisOnBudgetExhaustion` the output is instead the answer
104
+ * from one final tool-free turn, and a synthesis that produced no answer
105
+ * rejects (as an `AgentRunError`) rather than resolving with the note.
84
106
  *
85
107
  * Swappable like any bond: replace this LLM agent with your own
86
108
  * `AIAgentsProvider` via `bond('ai-agents', myProvider)` — nothing else changes.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8GG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -67,6 +67,25 @@
67
67
  * across tool calls, separately from assistant text and tool arguments.
68
68
  * Omitting the option does not enable thinking or manufacture reasoning.
69
69
  *
70
+ * Two further options are OPT-IN and change nothing when omitted:
71
+ *
72
+ * - `input.maxToolResultChars` bounds the `tool_result` copy sent to the model
73
+ * (a whole-file or log dump otherwise crowds out the instruction that asked
74
+ * for it) and appends an explicit truncation note telling the model the value
75
+ * was cut and how to recover. The step record keeps the complete tool result.
76
+ * - `input.finalSynthesisOnBudgetExhaustion` spends ONE tool-free turn after
77
+ * the `maxSteps` budget is exhausted, so a run that ended on a tool
78
+ * invocation still returns a final answer instead of the budget note. That
79
+ * turn carries the same history, system, model, thinking, cache, streaming
80
+ * and signal settings plus one short generic instruction to answer now from
81
+ * the evidence already gathered (without it a model mid-investigation can
82
+ * keep planning). Its events reach `onEvent`, its usage is metered, and it
83
+ * cannot call tools: a tool request on that turn is never executed and is not
84
+ * accepted as a final answer. If the turn produces no answer text, `run()`
85
+ * rejects with `AgentRunError` instead of returning the budget note, and the
86
+ * message says whether the turn requested another tool or returned no
87
+ * content (e.g. only reasoning).
88
+ *
70
89
  * Failure semantics (how to tell failure modes apart):
71
90
  * - **Tool failure** (throwing `execute()`, unknown tool name) — NON-fatal:
72
91
  * recorded on the step with `isError: true` and fed back to the model.
@@ -80,7 +99,10 @@
80
99
  * `result.output` means the model genuinely produced no text, not that the
81
100
  * API failed.
82
101
  * - **Step budget exhausted** — resolves normally; `output` is the last
83
- * assistant text or an explicit "step budget exhausted" note.
102
+ * assistant text or an explicit "step budget exhausted" note. With
103
+ * `input.finalSynthesisOnBudgetExhaustion` the output is instead the answer
104
+ * from one final tool-free turn, and a synthesis that produced no answer
105
+ * rejects (as an `AgentRunError`) rather than resolving with the note.
84
106
  *
85
107
  * Swappable like any bond: replace this LLM agent with your own
86
108
  * `AIAgentsProvider` via `bond('ai-agents', myProvider)` — nothing else changes.
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,YAAY,CAAA;AAOnB,OAAO,KAAK,EAKV,gBAAgB,EACjB,MAAM,yBAAyB,CAAA;AAuHhC;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,gBAiLtB,CAAA"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,YAAY,CAAA;AAOnB,OAAO,KAAK,EAKV,gBAAgB,EACjB,MAAM,yBAAyB,CAAA;AAkLhC;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,gBA0QtB,CAAA"}
package/dist/provider.js CHANGED
@@ -55,6 +55,57 @@ function stringifyResult(result) {
55
55
  return String(result);
56
56
  }
57
57
  }
58
+ /**
59
+ * Bounds a serialized tool result for the MODEL-FACING conversation and, when
60
+ * it has to cut, appends an explicit truncation + recovery note.
61
+ *
62
+ * @remarks
63
+ * Only the copy sent to the model is bounded: the step record keeps the
64
+ * complete value (see `AgentToolCall.result`). The note is appended after the
65
+ * budget, so it is not counted against it, and the cut never splits a
66
+ * surrogate pair (an unpaired half is not valid text).
67
+ *
68
+ * @param serialized - The complete serialized tool result.
69
+ * @param maxChars - The character budget, or `undefined` for unbounded.
70
+ * @returns The content for the `tool_result` block.
71
+ */
72
+ function boundToolResult(serialized, maxChars) {
73
+ if (maxChars === undefined || serialized.length <= maxChars)
74
+ return serialized;
75
+ let end = maxChars;
76
+ const lastCode = serialized.charCodeAt(end - 1);
77
+ // Drop a split surrogate pair rather than send half a code point.
78
+ if (lastCode >= 0xd800 && lastCode <= 0xdbff)
79
+ end -= 1;
80
+ return (serialized.slice(0, end) +
81
+ t('ai-agents.toolResultTruncated', { shown: end, total: serialized.length }, {
82
+ defaultValue: '\n\n[Truncated: {{shown}} of {{total}} characters shown. The complete result is retained outside this conversation; call the tool again with a narrower range or smaller scope to read the rest.]',
83
+ }));
84
+ }
85
+ /**
86
+ * The model-facing instruction appended to the ONE tool-free synthesis turn
87
+ * after the step budget is exhausted.
88
+ *
89
+ * @remarks
90
+ * A final turn carrying only the loop's own history is indistinguishable from
91
+ * any other turn — same system prompt, same conversation, no tools — so a model
92
+ * that was mid-investigation can keep planning (or emit reasoning alone)
93
+ * instead of answering. A real bounded review hit exactly that: the whole step
94
+ * budget was spent, the tool-free turn produced no answer text, and the run
95
+ * failed with nothing to show for the evidence it had gathered.
96
+ *
97
+ * The note is deliberately short and generic — it never names a reviewer,
98
+ * benchmark, project or route — and is a USER message appended AFTER the
99
+ * recorded history for that one request, so every earlier message (including
100
+ * retained native `reasoning`) is still sent unchanged.
101
+ *
102
+ * @returns The instruction text for the synthesis turn.
103
+ */
104
+ function finalSynthesisInstruction() {
105
+ return t('ai-agents.finalSynthesisInstruction', undefined, {
106
+ defaultValue: 'The step budget is exhausted and no further tool calls are allowed. Do not request more tools. Answer now, using only the evidence already gathered in this conversation, and briefly state anything you had to leave unchecked.',
107
+ });
108
+ }
58
109
  /**
59
110
  * Drains a single chat stream, accumulating text and tool calls, forwarding
60
111
  * every event to `onEvent`, and summing `done.usage`.
@@ -132,6 +183,15 @@ export const provider = {
132
183
  defaultValue: 'AgentRunInput accepts either `task` or `messages`, not both. Put the task in the `messages` history (as the last user message) or drop one of the two.',
133
184
  }));
134
185
  }
186
+ // Validate the opt-in tool-result budget up front: a bad number would
187
+ // otherwise surface mid-loop as a truncation bug instead of a caller error.
188
+ const toolResultLimit = input.maxToolResultChars === undefined ? undefined : Math.floor(input.maxToolResultChars);
189
+ if (toolResultLimit !== undefined &&
190
+ (!Number.isFinite(toolResultLimit) || toolResultLimit < 1)) {
191
+ throw new Error(t('ai-agents.error.invalidMaxToolResultChars', { value: String(input.maxToolResultChars) }, {
192
+ defaultValue: '`maxToolResultChars` must be a finite number of at least 1 character (received {{value}}; fractional values are floored).',
193
+ }));
194
+ }
135
195
  const ai = input.provider ? getAiProviderByName(input.provider) : requireAiProvider();
136
196
  if (!ai) {
137
197
  throw new Error(t('ai-agents.error.aiProviderNotFound', { name: input.provider ?? '' }, { defaultValue: `AI provider "${input.provider}" is not bonded.` }));
@@ -143,6 +203,57 @@ export const provider = {
143
203
  const steps = [];
144
204
  const usage = { inputTokens: 0, outputTokens: 0 };
145
205
  let lastText = '';
206
+ /**
207
+ * Sends ONE model turn over the bonded chat provider, forwarding every
208
+ * event to `onEvent`, booking its usage, and rejecting on an in-band
209
+ * provider error (so "the API failed" never looks like an empty answer).
210
+ *
211
+ * @param offerTools - When `false` the request carries NO `tools` field,
212
+ * so the provider cannot ask for a tool call (the final synthesis turn).
213
+ * @param trailingMessages - Messages appended AFTER the loop's history for
214
+ * this ONE request only; the loop's own `messages` array is untouched, so
215
+ * every other turn sends it exactly as before.
216
+ * @returns The drained turn.
217
+ */
218
+ const modelTurn = async (offerTools, trailingMessages = []) => {
219
+ const requestMessages = trailingMessages.length > 0 ? [...messages, ...trailingMessages] : messages;
220
+ const turn = await drainStream(ai.chat({
221
+ messages: requestMessages,
222
+ system: input.system,
223
+ ...(offerTools ? { tools: input.tools } : {}),
224
+ model: input.model,
225
+ maxTokens: input.maxTokens,
226
+ temperature: input.temperature,
227
+ // Keep the caller's native thinking settings on every tool turn.
228
+ ...(input.thinking === undefined ? {} : { thinking: input.thinking }),
229
+ // Defaults to streaming (see the `stream` augmentation in
230
+ // types.ts): onEvent is a LIVE hook, and non-streaming requests
231
+ // with large outputs (a big tool input at a raised maxTokens) are
232
+ // exactly what provider docs warn will time out over HTTP.
233
+ // drainStream already folds streamed events correctly, so there
234
+ // is no extra cost to defaulting streaming on. Set
235
+ // `input.stream: false` to force non-streaming.
236
+ stream: input.stream !== false,
237
+ // Prompt-cache breakpoint hint: the system+tools prefix is
238
+ // identical across turns, so passing this through avoids
239
+ // re-billing the full (growing) prompt on every one of up to
240
+ // maxSteps turns. Providers without cache support ignore it.
241
+ cacheControl: input.cacheControl,
242
+ signal: input.signal,
243
+ }), input.onEvent);
244
+ addUsage(usage, turn.usage);
245
+ // A provider API failure (rate limit, auth, overload, network) arrives
246
+ // as an in-band `error` ChatEvent — the stream ends without text or
247
+ // tools. Rejecting here disambiguates "the AI provider failed" from
248
+ // "the model legitimately answered nothing": the old fall-through
249
+ // returned { output: '', steps, usage } as if the run succeeded, and
250
+ // callers debugging the empty output had no signal that the API ever
251
+ // errored.
252
+ if (turn.errorMessage !== undefined) {
253
+ throw new Error(t('ai-agents.error.providerError', { message: turn.errorMessage }, { defaultValue: `AI provider error during agent run: ${turn.errorMessage}` }));
254
+ }
255
+ return turn;
256
+ };
146
257
  // Everything below can fail partway through a multi-turn run (a provider
147
258
  // API error, an abort, or an unexpected throw from ai.chat() itself —
148
259
  // e.g. a signal-aborted fetch). A plain rejection would silently drop
@@ -154,41 +265,7 @@ export const provider = {
154
265
  // `instanceof AgentRunError`.
155
266
  try {
156
267
  for (let step = 0; step < maxSteps; step++) {
157
- const turn = await drainStream(ai.chat({
158
- messages,
159
- system: input.system,
160
- tools: input.tools,
161
- model: input.model,
162
- maxTokens: input.maxTokens,
163
- temperature: input.temperature,
164
- // Keep the caller's native thinking settings on every tool turn.
165
- ...(input.thinking === undefined ? {} : { thinking: input.thinking }),
166
- // Defaults to streaming (see the `stream` augmentation in
167
- // types.ts): onEvent is a LIVE hook, and non-streaming requests
168
- // with large outputs (a big tool input at a raised maxTokens) are
169
- // exactly what provider docs warn will time out over HTTP.
170
- // drainStream already folds streamed events correctly, so there
171
- // is no extra cost to defaulting streaming on. Set
172
- // `input.stream: false` to force non-streaming.
173
- stream: input.stream !== false,
174
- // Prompt-cache breakpoint hint: the system+tools prefix is
175
- // identical across turns, so passing this through avoids
176
- // re-billing the full (growing) prompt on every one of up to
177
- // maxSteps turns. Providers without cache support ignore it.
178
- cacheControl: input.cacheControl,
179
- signal: input.signal,
180
- }), input.onEvent);
181
- addUsage(usage, turn.usage);
182
- // A provider API failure (rate limit, auth, overload, network) arrives
183
- // as an in-band `error` ChatEvent — the stream ends without text or
184
- // tools. Rejecting here disambiguates "the AI provider failed" from
185
- // "the model legitimately answered nothing": the old fall-through
186
- // returned { output: '', steps, usage } as if the run succeeded, and
187
- // callers debugging the empty output had no signal that the API ever
188
- // errored.
189
- if (turn.errorMessage !== undefined) {
190
- throw new Error(t('ai-agents.error.providerError', { message: turn.errorMessage }, { defaultValue: `AI provider error during agent run: ${turn.errorMessage}` }));
191
- }
268
+ const turn = await modelTurn(true);
192
269
  if (turn.text)
193
270
  lastText = turn.text;
194
271
  // Append the assistant turn: a text block (if any) + one tool_use block
@@ -235,17 +312,57 @@ export const provider = {
235
312
  isError = true;
236
313
  }
237
314
  }
315
+ // `result` (above) is retained COMPLETE on the step record; only the
316
+ // copy the model reads is bounded when `maxToolResultChars` is set.
238
317
  toolCalls.push({ id: tu.id, name: tu.name, input: tu.input, result, isError });
239
318
  resultBlocks.push({
240
319
  type: 'tool_result',
241
320
  tool_use_id: tu.id,
242
- content: stringifyResult(result),
321
+ content: boundToolResult(stringifyResult(result), toolResultLimit),
243
322
  });
244
323
  }
245
324
  messages.push({ role: 'user', content: resultBlocks });
246
325
  steps.push({ text: turn.text || undefined, toolCalls });
247
326
  }
248
- // Step budget exhausted while the model was still calling tools.
327
+ // Step budget exhausted while the model was still calling tools. With
328
+ // `finalSynthesisOnBudgetExhaustion`, buy exactly ONE tool-free turn to
329
+ // answer from what the run already saw — the loop ended on a tool
330
+ // invocation, so there is no final assistant text to return otherwise.
331
+ // The request also carries one short generic instruction saying the
332
+ // budget is spent and to answer now: without it the turn is just the same
333
+ // conversation with no tools, and a model mid-investigation can keep
334
+ // planning (a real bounded review produced no answer text at exactly that
335
+ // turn). The instruction is appended for this request only; the loop's
336
+ // history, system prompt, model settings and cache/usage/error contracts
337
+ // are all unchanged.
338
+ if (input.finalSynthesisOnBudgetExhaustion === true) {
339
+ const finalTurn = await modelTurn(false, [
340
+ { role: 'user', content: finalSynthesisInstruction() },
341
+ ]);
342
+ // A renewed tool request is NOT an answer. That turn offered no tools,
343
+ // so nothing it asks for can be executed, and a plan to keep going must
344
+ // not be passed off as the final output — whether or not the turn also
345
+ // emitted text. Report this cause distinctly from a content-free turn.
346
+ if (finalTurn.toolUses.length > 0) {
347
+ throw new Error(t('ai-agents.error.finalSynthesisToolRequest', undefined, {
348
+ defaultValue: 'Agent step budget exhausted and the final synthesis turn requested another tool instead of answering; no further tool calls are allowed.',
349
+ }));
350
+ }
351
+ // Content-free is its own cause: a turn that emitted only reasoning (or
352
+ // nothing at all) produced no answer, and the message says which, so a
353
+ // caller can tell "the model kept thinking" from "the model tried to
354
+ // call a tool".
355
+ if (!finalTurn.text.trim()) {
356
+ const reasoningOnly = finalTurn.reasoning?.trim()
357
+ ? ' It returned only reasoning text and no answer.'
358
+ : '';
359
+ throw new Error(t('ai-agents.error.finalSynthesisEmpty', undefined, {
360
+ defaultValue: 'Agent step budget exhausted and the final synthesis turn produced no answer.' +
361
+ reasoningOnly,
362
+ }));
363
+ }
364
+ return { output: finalTurn.text, steps, usage };
365
+ }
249
366
  const output = lastText ||
250
367
  t('ai-agents.stepBudgetExhausted', undefined, {
251
368
  defaultValue: 'Agent stopped: step budget exhausted before a final answer.',
package/dist/types.d.ts CHANGED
@@ -50,6 +50,70 @@ declare module '@molecule/api-ai-agents' {
50
50
  cacheControl?: {
51
51
  type: 'ephemeral';
52
52
  };
53
+ /**
54
+ * Caps how many characters of a serialized tool result are placed in the
55
+ * model-facing conversation (default: unbounded).
56
+ *
57
+ * A tool that returns a whole file, log, or search dump can consume most
58
+ * of the context window in one step and crowd out the instruction that
59
+ * asked for it, so a bounded run can "forget" what it was doing and answer
60
+ * from the dump instead. When set, each `tool_result` block sent to the
61
+ * model carries at most this many characters of the serialized result,
62
+ * followed by an explicit note stating that the value was truncated and
63
+ * how to recover (re-run the tool with a narrower range or scope). The
64
+ * step record's `AgentToolCall.result` keeps the COMPLETE value — only the
65
+ * copy the model sees is bounded, so a caller reading the steps (a report,
66
+ * a diff, an assertion) still gets everything the tool returned.
67
+ *
68
+ * The limit applies to the result body; the truncation note is appended
69
+ * after it and is not counted against the budget. Results at or under the
70
+ * limit — including the loop's own error strings — pass through unchanged.
71
+ * A cut never splits a surrogate pair. Must be a finite number of at least
72
+ * 1 character (fractional values are floored); any other value rejects the
73
+ * run before a model call is made. Omit it for the previous unbounded
74
+ * behavior.
75
+ */
76
+ maxToolResultChars?: number;
77
+ /**
78
+ * After the `maxSteps` budget is exhausted while the model was still
79
+ * calling tools, spend ONE final synthesis turn with NO tools and return
80
+ * its text as `output` (default `false`).
81
+ *
82
+ * Without it, a run that ends on a tool invocation has no final assistant
83
+ * text: `output` falls back to the "step budget exhausted" note and any
84
+ * report the model was about to write is lost, even though the work that
85
+ * filled the budget succeeded. Opting in buys the model one tool-free turn
86
+ * to answer from what it has already seen.
87
+ *
88
+ * The synthesis turn preserves everything the loop was using: the full
89
+ * message history (including retained reasoning), `system`, `model`,
90
+ * `maxTokens`, `temperature`, `thinking`, `stream`, `cacheControl`,
91
+ * `provider`, and `signal`. It also carries ONE short, generic instruction
92
+ * (a user message appended after the history, for that request only)
93
+ * stating that the step budget is exhausted, that no further tool calls
94
+ * are allowed, and to answer now from the evidence already gathered.
95
+ * Without it a tool-free turn is just the same conversation with no tools,
96
+ * and a model mid-investigation can keep planning instead of answering.
97
+ * Its events reach `onEvent` and its usage is added to `result.usage`, so
98
+ * live monitoring and metering see the extra call. The instruction is
99
+ * never sent on any other turn, so normal runs are byte- and
100
+ * cost-identical.
101
+ *
102
+ * It offers no tools and never executes a tool call: a tool request on that
103
+ * turn cannot be executed and is NOT accepted as a final answer (even when
104
+ * the turn also emitted text), because it is a plan to keep going rather
105
+ * than an answer. The recorded `steps` stay exactly the tool-invoking
106
+ * rounds — `steps.length >= maxSteps` still means "budget exhausted".
107
+ *
108
+ * The response is returned as-is (not trimmed). A turn that produces no
109
+ * answer text is NOT papered over with the exhausted-budget note: `run()`
110
+ * rejects with an `AgentRunError` carrying the accumulated `usage`/`steps`,
111
+ * and the message distinguishes a content-free turn (including one that
112
+ * returned only reasoning) from a turn that requested another tool. A
113
+ * provider error on that turn rejects the same way. Only the literal `true`
114
+ * enables this; omit it for the previous behavior and cost.
115
+ */
116
+ finalSynthesisOnBudgetExhaustion?: boolean;
53
117
  }
54
118
  }
55
119
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,yBAAyB,CAAA;AAEhC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD,OAAO,QAAQ,yBAAyB,CAAC;IACvC,UAAU,aAAa;QACrB;;;;;WAKG;QACH,QAAQ,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAA;QACjC;;;;;;;;;;;;;;WAcG;QACH,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB;;;;;;WAMG;QACH,YAAY,CAAC,EAAE;YAAE,IAAI,EAAE,WAAW,CAAA;SAAE,CAAA;KACrC;CACF"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,yBAAyB,CAAA;AAEhC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD,OAAO,QAAQ,yBAAyB,CAAC;IACvC,UAAU,aAAa;QACrB;;;;;WAKG;QACH,QAAQ,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAA;QACjC;;;;;;;;;;;;;;WAcG;QACH,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB;;;;;;WAMG;QACH,YAAY,CAAC,EAAE;YAAE,IAAI,EAAE,WAAW,CAAA;SAAE,CAAA;QACpC;;;;;;;;;;;;;;;;;;;;;;WAsBG;QACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAsCG;QACH,gCAAgC,CAAC,EAAE,OAAO,CAAA;KAC3C;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-agents-llm",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "LLM-backed AI agents provider for molecule.dev — a tool-calling loop over the swappable @molecule/api-ai chat bond",
5
5
  "homepage": "https://www.molecule.dev/packages/api-ai-agents-llm",
6
6
  "type": "module",