@cubicecho/agent-core 2.14.0 → 2.15.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 +12 -5
- package/dist/agent-loop.d.ts +18 -4
- package/dist/agent-loop.js +8 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -370,10 +370,17 @@ const { turn, messages, usage, loaded } = await runAgentLoop({
|
|
|
370
370
|
|
|
371
371
|
Each step is one `runTurn` with the body from `buildBody`, so everything `negotiate` answers is
|
|
372
372
|
answered here too, and a request that is too big throws `ContextOverflow` whichever side found
|
|
373
|
-
out. Between steps the loop runs the calls: sequentially by default, or together with
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
retry. What a tool throws is what the model
|
|
373
|
+
out. Between steps the loop runs the calls: sequentially by default, or together with `parallel: true`.
|
|
374
|
+
Either way an identical call — the same name and arguments, byte for byte — is made once and its
|
|
375
|
+
answer handed to the repeat, and two still in flight share the request. A call that threw is
|
|
376
|
+
forgotten rather than cached, so asking again is a real retry. What a tool throws is what the model
|
|
377
|
+
reads, and so are arguments that did not parse.
|
|
378
|
+
|
|
379
|
+
The scope is the step, not the run: between steps other tools have run, and the file the model read
|
|
380
|
+
may be the file it has since written. `dedupeToolCalls: false` dispatches everything, and a
|
|
381
|
+
predicate is asked per call — which is how `send_email` opts out, since twice is two emails and
|
|
382
|
+
nothing in an OpenAI tool definition says which tools those are. A pool that reads the MCP
|
|
383
|
+
`readOnlyHint` and `idempotentHint` annotations can answer it; this package cannot.
|
|
377
384
|
|
|
378
385
|
Arguments go through `parseToolArguments`, which is lenient where the model's meaning is plain:
|
|
379
386
|
JSON held in a string is opened, and the almost-JSON local models write — single quotes, Python's
|
|
@@ -382,7 +389,7 @@ string. What still is not an object throws a `ToolArgumentsError` whose `kind` i
|
|
|
382
389
|
the turn stopped at the ceiling, with a message telling the model so, and `malformed` otherwise.
|
|
383
390
|
The repaired JSON is what the transcript keeps, and an unreadable call is replayed as `{}`, because
|
|
384
391
|
a server that parses replayed arguments refuses the originals on every later request. `dispatch`
|
|
385
|
-
is still handed the model's own text as `raw`, and the
|
|
392
|
+
is still handed the model's own text as `raw`, and the dedupe compares repaired arguments,
|
|
386
393
|
so `{'a': 1}` and `{"a": 1}` are one call.
|
|
387
394
|
|
|
388
395
|
A server whose tool-call parser was written for another template streams the model's call as
|
package/dist/agent-loop.d.ts
CHANGED
|
@@ -152,12 +152,26 @@ export interface AgentLoopOptions {
|
|
|
152
152
|
/** Runs one tool call and returns what the model reads. What it throws, the model reads too. */
|
|
153
153
|
dispatch: (call: ToolCallRequest, signal?: AbortSignal) => Promise<string>;
|
|
154
154
|
/**
|
|
155
|
-
* Runs a step's calls together rather than one after another
|
|
156
|
-
*
|
|
157
|
-
* answer. A call that threw is not an answer and is made again. Results still go into the
|
|
158
|
-
* transcript in the order the model asked.
|
|
155
|
+
* Runs a step's calls together rather than one after another. Results still go into the
|
|
156
|
+
* transcript in the order the model asked. See `dedupeToolCalls`, which applies either way.
|
|
159
157
|
*/
|
|
160
158
|
parallel?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Answers an identical repeat of a call — the same name and the same arguments, word for word —
|
|
161
|
+
* within one step from the first one, rather than dispatching it again.
|
|
162
|
+
*
|
|
163
|
+
* On by default, and on whether or not the calls run in `parallel`: a model that asks the same
|
|
164
|
+
* question twice in one reply gets one answer, and two that are still in flight share the
|
|
165
|
+
* request. A call that threw is not an answer and is made again. The scope is the step and not
|
|
166
|
+
* the run, because between steps other tools have run and the file the model read may be the
|
|
167
|
+
* file it has since written.
|
|
168
|
+
*
|
|
169
|
+
* `false` dispatches every call. A predicate is asked per call and is how a tool that does
|
|
170
|
+
* something rather than reads something opts out — `send_email` twice is two emails, and this
|
|
171
|
+
* package cannot tell which tools those are. A pool that reads the MCP `readOnlyHint` and
|
|
172
|
+
* `idempotentHint` annotations can answer it; nothing in an OpenAI tool definition can.
|
|
173
|
+
*/
|
|
174
|
+
dedupeToolCalls?: boolean | ((call: ToolCallRequest) => boolean);
|
|
161
175
|
/** Hooks gathered onto the question before the first request, and told the reply after. */
|
|
162
176
|
hooks?: AgentLoopHooks;
|
|
163
177
|
/**
|
package/dist/agent-loop.js
CHANGED
|
@@ -202,7 +202,8 @@ function cacheDiagnosis(previous, messages, tools, usage) {
|
|
|
202
202
|
*/
|
|
203
203
|
export async function runAgentLoop(options) {
|
|
204
204
|
const { config, system = "", tools = [], catalog = [], dispatch, hooks, signal } = options;
|
|
205
|
-
const { onTurn, beforeStep, parallel = false, recoverToolCalls: recover = true, maxContinuations = 0, toolOrder = true, } = options;
|
|
205
|
+
const { onTurn, beforeStep, parallel = false, recoverToolCalls: recover = true, maxContinuations = 0, toolOrder = true, dedupeToolCalls = true, } = options;
|
|
206
|
+
const dedupable = typeof dedupeToolCalls === "function" ? dedupeToolCalls : () => dedupeToolCalls;
|
|
206
207
|
const started = Date.now();
|
|
207
208
|
// What the loop emitted, less the token deltas, for `runMetrics` at the end. Stamped here rather
|
|
208
209
|
// than by the bus, which the loop does not know about.
|
|
@@ -259,7 +260,6 @@ export async function runAgentLoop(options) {
|
|
|
259
260
|
: { context: "", notes: [] };
|
|
260
261
|
const usage = { prompt: 0, completion: 0, total: 0, cached: 0 };
|
|
261
262
|
const toolCalls = [];
|
|
262
|
-
const answered = new Map();
|
|
263
263
|
const loads = { toolsLoaded: 0, redundantLoads: 0, unknownToolNames: 0 };
|
|
264
264
|
let previous;
|
|
265
265
|
for (let step = 0; step < config.maxToolIterations; step++) {
|
|
@@ -411,6 +411,9 @@ export async function runAgentLoop(options) {
|
|
|
411
411
|
},
|
|
412
412
|
};
|
|
413
413
|
}
|
|
414
|
+
// Per step, not per run: the answer to a call made two steps ago was true before the tools in
|
|
415
|
+
// between ran, and the file the model read may be the file it has since written.
|
|
416
|
+
const answered = new Map();
|
|
414
417
|
const run = async ({ call, args, error: unreadable, normal }) => {
|
|
415
418
|
const { name, arguments: raw } = call.function;
|
|
416
419
|
onEvent({ kind: "tool-call", name, text: preview(raw) });
|
|
@@ -436,7 +439,7 @@ export async function runAgentLoop(options) {
|
|
|
436
439
|
loaded.add(name);
|
|
437
440
|
used.add(name);
|
|
438
441
|
const request = { id: call.id, name, args, raw };
|
|
439
|
-
content =
|
|
442
|
+
content = dedupable(request)
|
|
440
443
|
? await once(answered, `${name}\0${normal}`, () => dispatch(request, signal))
|
|
441
444
|
: await dispatch(request, signal);
|
|
442
445
|
}
|
|
@@ -470,8 +473,8 @@ export async function runAgentLoop(options) {
|
|
|
470
473
|
}
|
|
471
474
|
/**
|
|
472
475
|
* Makes a call at most once per key, sharing the in-flight promise so two identical calls in one
|
|
473
|
-
* step make one request between them
|
|
474
|
-
* real retry rather than a replayed failure.
|
|
476
|
+
* step make one request between them whether they run together or one after the other. A call
|
|
477
|
+
* that rejected is forgotten, so asking again is a real retry rather than a replayed failure.
|
|
475
478
|
*/
|
|
476
479
|
async function once(answered, key, make) {
|
|
477
480
|
const previous = answered.get(key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openai",
|