@cubicecho/agent-core 4.16.0 → 4.17.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 +22 -0
- package/dist/run/agent-loop.d.ts +19 -0
- package/dist/run/agent-loop.js +55 -8
- package/dist/run/run-calls.d.ts +2 -0
- package/dist/run/run-calls.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -470,6 +470,7 @@ const { turn, messages, usage, loaded } = await runAgentLoop({
|
|
|
470
470
|
messages: history, // ending in the question; not written to
|
|
471
471
|
tools, // every tool the run may reach
|
|
472
472
|
catalog, // the same, name-only, for on-demand and proxied loading
|
|
473
|
+
hostTools, // the host's own, declared in every mode and never catalogued
|
|
473
474
|
serverInstructions: true, // the catalogue's `instructions` after `system`; off by default
|
|
474
475
|
preselected, // from `preselect`, if a small model chose
|
|
475
476
|
dispatch: ({ name, args }, signal) => pool.call(name, args, signal),
|
|
@@ -688,6 +689,27 @@ a `tool-call` and a `tool-result` event, told to `onToolCall` and `onToolResult`
|
|
|
688
689
|
id is numbered by where the call lands in the transcript, so two questions do not share one. A name
|
|
689
690
|
with no definition in `tools`, or outside the catalogue, is left out, and none left is no exchange.
|
|
690
691
|
|
|
692
|
+
A host with tools of its own — a question for a person, a resource read — passes them as
|
|
693
|
+
`hostTools`, and they are declared on every step in every mode: after `PROXY_TOOLS` proxied, after
|
|
694
|
+
`load_tools` and ahead of anything loaded on demand, after `tools` in eager mode, and after the
|
|
695
|
+
shortlist on a preselected first step. They reach `dispatch` like any other call and are kept out
|
|
696
|
+
of discovery: taken out of `catalog`, `loaded` and `preselected` before the run starts, never
|
|
697
|
+
counted as a load, and a `call_tool` naming one is refused with a pointer to call it directly.
|
|
698
|
+
Proxied and eager, the array is one set for the whole run, so the cache holds whatever `toolOrder`
|
|
699
|
+
does with it; on demand, `toolOrder: false` keeps them in the prefix every load appends after.
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
await runAgentLoop({
|
|
703
|
+
config: { ...config, toolDiscovery: "proxy" },
|
|
704
|
+
messages: history,
|
|
705
|
+
tools: pool.tools(),
|
|
706
|
+
catalog: pool.catalog(),
|
|
707
|
+
hostTools: [ASK_PERSON_DEFINITION], // declared beside load_tools and call_tool, on every step
|
|
708
|
+
dispatch: (call, signal) =>
|
|
709
|
+
call.name === ASK_PERSON ? ask(call.args) : pool.call(call.name, call.args, signal),
|
|
710
|
+
});
|
|
711
|
+
```
|
|
712
|
+
|
|
691
713
|
### What the servers said on the way
|
|
692
714
|
|
|
693
715
|
A server can log while a call runs — a warning that the cache was stale, an error it recovered
|
package/dist/run/agent-loop.d.ts
CHANGED
|
@@ -152,6 +152,23 @@ export interface AgentLoopOptions {
|
|
|
152
152
|
* loaded so far, by name; proxied mode declares none, and answers a load with them instead.
|
|
153
153
|
*/
|
|
154
154
|
tools?: OpenAI.ChatCompletionTool[];
|
|
155
|
+
/**
|
|
156
|
+
* The host's own tools — a question for a person, a resource read — declared on every step in
|
|
157
|
+
* every discovery mode and dispatched like any other call, but never catalogued, loaded, or
|
|
158
|
+
* reached through `call_tool`.
|
|
159
|
+
*
|
|
160
|
+
* @remarks
|
|
161
|
+
* Each sits after the loop's own definitions: after `PROXY_TOOLS` proxied, after `load_tools`
|
|
162
|
+
* and ahead of anything loaded on demand, after `tools` in eager mode, and after the shortlist
|
|
163
|
+
* on a preselected first step; `toolOrder` then orders the array as it always does. Proxied and
|
|
164
|
+
* eager, the array is one set for the whole run, so the cache holds in either order; on demand,
|
|
165
|
+
* `toolOrder: false` keeps them in the prefix every load appends after.
|
|
166
|
+
*
|
|
167
|
+
* A name of one is taken out of `catalog`, `loaded` and `preselected` before the run starts, is
|
|
168
|
+
* never counted as a load, and is declared once where `tools` or the loop has it too. A
|
|
169
|
+
* `call_tool` naming one is refused with a pointer to call it directly.
|
|
170
|
+
*/
|
|
171
|
+
hostTools?: OpenAI.ChatCompletionTool[];
|
|
155
172
|
/**
|
|
156
173
|
* The same tools as a name-only catalogue. On-demand and proxied modes need it, and are eager
|
|
157
174
|
* without it.
|
|
@@ -595,6 +612,8 @@ export interface AgentLoopResult {
|
|
|
595
612
|
* tool array to go into, so it is written into the transcript as a `load_tools` exchange after
|
|
596
613
|
* the question. `call_tool` is honoured on demand too, unless the host has a tool of that name —
|
|
597
614
|
* a session switched out of proxied mode still has it in its history, and the model copies it.
|
|
615
|
+
* The host's own `hostTools` are declared beside whatever the mode sends, in every mode, and are
|
|
616
|
+
* never catalogued.
|
|
598
617
|
*
|
|
599
618
|
* Neither side task is run unless asked for. `preselect` has the loop make the preselection and
|
|
600
619
|
* `compact` has it fold the transcript, each by the task `config.tasks` carries for it and on
|
package/dist/run/agent-loop.js
CHANGED
|
@@ -112,6 +112,8 @@ function cacheDiagnosis(previous, next, usage) {
|
|
|
112
112
|
* tool array to go into, so it is written into the transcript as a `load_tools` exchange after
|
|
113
113
|
* the question. `call_tool` is honoured on demand too, unless the host has a tool of that name —
|
|
114
114
|
* a session switched out of proxied mode still has it in its history, and the model copies it.
|
|
115
|
+
* The host's own `hostTools` are declared beside whatever the mode sends, in every mode, and are
|
|
116
|
+
* never catalogued.
|
|
115
117
|
*
|
|
116
118
|
* Neither side task is run unless asked for. `preselect` has the loop make the preselection and
|
|
117
119
|
* `compact` has it fold the transcript, each by the task `config.tasks` carries for it and on
|
|
@@ -133,9 +135,10 @@ export async function runAgentLoop(options) {
|
|
|
133
135
|
}),
|
|
134
136
|
};
|
|
135
137
|
let result;
|
|
138
|
+
const apart = hostToolsApart(options);
|
|
136
139
|
try {
|
|
137
|
-
pin = await pinSession(
|
|
138
|
-
result = await runSteps(
|
|
140
|
+
pin = await pinSession(apart);
|
|
141
|
+
result = await runSteps(apart, standing, pin);
|
|
139
142
|
}
|
|
140
143
|
catch (error) {
|
|
141
144
|
// Wrapped rather than annotated: an abort's reason is one object shared by every run under
|
|
@@ -399,6 +402,43 @@ function recorder(heard) {
|
|
|
399
402
|
};
|
|
400
403
|
return { recorded, onEvent };
|
|
401
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* The names of the host's own tools.
|
|
407
|
+
*
|
|
408
|
+
* @param [tools] - The host's definitions. One that is not a function has no name, and is left out.
|
|
409
|
+
* @returns A new set, empty where there are none.
|
|
410
|
+
*/
|
|
411
|
+
const hostToolNames = (tools = []) => new Set(tools.flatMap((tool) => toolName(tool) ?? []));
|
|
412
|
+
/**
|
|
413
|
+
* The run's options with the host's own tools kept out of discovery: off the catalogue, and out of
|
|
414
|
+
* what was carried and what was picked.
|
|
415
|
+
*
|
|
416
|
+
* @param options - What `runAgentLoop` was given. Not written to.
|
|
417
|
+
* @returns `options` itself where there are no host tools, and otherwise a copy with `catalog`,
|
|
418
|
+
* `loaded` and `preselected` filtered, a server that offers none of them kept as it was.
|
|
419
|
+
*
|
|
420
|
+
* @remarks
|
|
421
|
+
* Done once, here, so that preselection, `load_tools`, `call_tool` and the catalogue prompt all
|
|
422
|
+
* read one catalogue, and a host tool is never loadable however a name of one reaches the loop.
|
|
423
|
+
*/
|
|
424
|
+
function hostToolsApart(options) {
|
|
425
|
+
const own = hostToolNames(options.hostTools);
|
|
426
|
+
if (own.size === 0) {
|
|
427
|
+
return options;
|
|
428
|
+
}
|
|
429
|
+
const isCatalogued = (name) => own.has(name) === false;
|
|
430
|
+
const { catalog, loaded, preselected } = options;
|
|
431
|
+
const apart = (server) => server.tools.every((entry) => isCatalogued(entry.name))
|
|
432
|
+
? server
|
|
433
|
+
: { ...server, tools: server.tools.filter((entry) => isCatalogued(entry.name)) };
|
|
434
|
+
return {
|
|
435
|
+
...options,
|
|
436
|
+
...(catalog ? { catalog: catalog.map(apart) } : {}),
|
|
437
|
+
...(loaded ? { loaded: [...loaded].filter(isCatalogued) } : {}),
|
|
438
|
+
// Undefined stays undefined: an empty shortlist is a host saying it has already decided.
|
|
439
|
+
...(preselected ? { preselected: preselected.filter(isCatalogued) } : {}),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
402
442
|
/** What opens the notice naming the loop's own picks, ahead of the names. */
|
|
403
443
|
export const PRESELECTED_NOTICE = 'tools picked before the run: ';
|
|
404
444
|
/**
|
|
@@ -570,11 +610,17 @@ async function runSteps(options, standing, pin) {
|
|
|
570
610
|
const summarise = ownSummariser(options, notice);
|
|
571
611
|
const used = new Set();
|
|
572
612
|
const definitions = definitionsByName(tools);
|
|
613
|
+
// Each once, by name, so one given twice is declared once.
|
|
614
|
+
const hostTools = loadedTools([], options.hostTools ?? []);
|
|
615
|
+
const hostNames = hostToolNames(hostTools);
|
|
616
|
+
// After the head the mode declares, which a host tool of the same name does not displace.
|
|
617
|
+
const withHost = (head) => (hostTools.length ? loadedTools(head, hostTools) : head);
|
|
573
618
|
// Looked through unless the host has a tool by that name, which is then the host's to answer.
|
|
574
|
-
const proxies = onDemand && !definitions.has(CALL_TOOL);
|
|
619
|
+
const proxies = onDemand && !definitions.has(CALL_TOOL) && !hostNames.has(CALL_TOOL);
|
|
575
620
|
// What a call the model wrote as text may name: the host's tools, and the loop's own on demand.
|
|
576
621
|
const recoverable = [
|
|
577
622
|
...tools.map(toolName).filter((name) => name !== undefined),
|
|
623
|
+
...hostNames,
|
|
578
624
|
...(onDemand ? [LOAD_TOOLS, ...(proxies ? [CALL_TOOL] : [])] : []),
|
|
579
625
|
];
|
|
580
626
|
// In the order the names are given, not the order of `tools`: `loaded` is a set, which iterates
|
|
@@ -626,6 +672,7 @@ async function runSteps(options, standing, pin) {
|
|
|
626
672
|
proxied,
|
|
627
673
|
proxies,
|
|
628
674
|
definitions,
|
|
675
|
+
hostNames,
|
|
629
676
|
loaded,
|
|
630
677
|
loads,
|
|
631
678
|
used,
|
|
@@ -691,15 +738,15 @@ async function runSteps(options, standing, pin) {
|
|
|
691
738
|
// Ordered here rather than left to `buildBody`, so `names` below is what the request actually
|
|
692
739
|
// declared — a diagnosis reading an order the server never saw calls an untouched tool array
|
|
693
740
|
// `tools-changed`.
|
|
694
|
-
let offered = tools;
|
|
741
|
+
let offered = withHost(tools);
|
|
695
742
|
if (routed) {
|
|
696
|
-
offered = byName(new Set(preselected));
|
|
743
|
+
offered = withHost(byName(new Set(preselected)));
|
|
697
744
|
}
|
|
698
745
|
else if (proxied) {
|
|
699
|
-
offered = [...PROXY_TOOLS];
|
|
746
|
+
offered = withHost([...PROXY_TOOLS]);
|
|
700
747
|
}
|
|
701
748
|
else if (onDemand) {
|
|
702
|
-
offered = loadedTools([LOAD_TOOLS_DEFINITION], byName(loaded));
|
|
749
|
+
offered = loadedTools(withHost([LOAD_TOOLS_DEFINITION]), byName(loaded));
|
|
703
750
|
}
|
|
704
751
|
const declared = orderTools(offered, toolOrder);
|
|
705
752
|
// Unmarked, so the system prompt is the same text on every step and a load does not throw
|
|
@@ -783,7 +830,7 @@ async function runSteps(options, standing, pin) {
|
|
|
783
830
|
if (turn.finishReason === FinishReason.Length) {
|
|
784
831
|
notice(`the model stopped at maxTokens (${config.maxTokens}); this turn is cut short`);
|
|
785
832
|
}
|
|
786
|
-
const canRecover = recover && (tools.length > 0 || onDemand);
|
|
833
|
+
const canRecover = recover && (tools.length > 0 || hostTools.length > 0 || onDemand);
|
|
787
834
|
const { calls, content } = turnCalls(turn, canRecover ? recoverable : undefined, notice);
|
|
788
835
|
const shown = { ...turn, content, toolCalls: calls };
|
|
789
836
|
onTurn?.(shown, step);
|
package/dist/run/run-calls.d.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface Calling {
|
|
|
86
86
|
/** Whether a `call_tool` is looked through, which it is unless the host has a tool by that name. */
|
|
87
87
|
proxies: boolean;
|
|
88
88
|
definitions: ReadonlyMap<string, OpenAI.ChatCompletionTool>;
|
|
89
|
+
/** The host's own tools by name: declared on every step, and never reached through `call_tool`. */
|
|
90
|
+
hostNames: ReadonlySet<string>;
|
|
89
91
|
loaded: Set<string>;
|
|
90
92
|
loads: {
|
|
91
93
|
toolsLoaded: number;
|
package/dist/run/run-calls.js
CHANGED
|
@@ -256,7 +256,12 @@ function prepare(run, entry) {
|
|
|
256
256
|
// nothing a load could not, and the refusal is the call's answer.
|
|
257
257
|
let inner = args ?? {};
|
|
258
258
|
let refused;
|
|
259
|
-
if (proxy && args) {
|
|
259
|
+
if (proxy && args && run.hostNames.has(name)) {
|
|
260
|
+
// Declared already, so the model is pointed at the call it can make rather than told the
|
|
261
|
+
// tool does not exist.
|
|
262
|
+
refused = { error: new Error(`${name} is not in the catalogue; call it directly, not through ${CALL_TOOL}.`) };
|
|
263
|
+
}
|
|
264
|
+
else if (proxy && args) {
|
|
260
265
|
try {
|
|
261
266
|
inner = proxiedCall(args, catalog).input;
|
|
262
267
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.17.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",
|