@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 +24 -2
- package/dist/index.d.ts +23 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +154 -37
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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-
|
|
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.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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.
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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",
|