@cubicecho/agent-core 2.2.4 → 2.3.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 +122 -3
- package/dist/capabilities.d.ts +7 -3
- package/dist/capabilities.js +22 -9
- package/dist/client.d.ts +28 -0
- package/dist/client.js +94 -9
- package/dist/events.d.ts +71 -1
- package/dist/events.js +70 -35
- package/dist/hooks.d.ts +241 -0
- package/dist/hooks.js +216 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -4
- package/dist/retry.d.ts +5 -0
- package/dist/retry.js +5 -0
- package/dist/run-turn.d.ts +5 -0
- package/dist/run-turn.js +14 -1
- package/dist/side-task.js +8 -8
- package/dist/stream.d.ts +19 -1
- package/dist/stream.js +11 -1
- package/dist/tool-loading.d.ts +30 -10
- package/dist/tool-loading.js +45 -18
- package/llms.txt +27 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* What is here is everything that does not know what the agent is *for*: making a tool schema
|
|
5
5
|
* a strict server will accept, getting tool definitions in front of a model without paying for
|
|
6
6
|
* all of them, reading one streamed turn back into a message, answering an endpoint that
|
|
7
|
-
* refuses one of those, one-shot calls that support a run, the
|
|
8
|
-
* pooled client, and the rules about retrying. What is not here is the work — orchestration,
|
|
7
|
+
* refuses one of those, one-shot calls that support a run, the host's side of lifecycle hooks,
|
|
8
|
+
* the event bus a watcher reads, a pooled client, and the rules about retrying. What is not here is the work — orchestration,
|
|
9
9
|
* prompts, and whatever the run is about — because that is the caller's, and it is the part
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
@@ -14,7 +14,8 @@ export type { CatalogServer } from "./catalog.ts";
|
|
|
14
14
|
export { contextLimitFor, getClient, listModels, type ModelInfo, NO_KEY, resetClients, timeoutMs, } from "./client.ts";
|
|
15
15
|
export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } from "./config.ts";
|
|
16
16
|
export { errorMessage } from "./errors.ts";
|
|
17
|
-
export { emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunUsage, resetEvents, watch, } from "./events.ts";
|
|
17
|
+
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunUsage, resetEvents, watch, } from "./events.ts";
|
|
18
|
+
export { assembleContext, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, type HookContext, type HookEvent, type HookMessage, type HookNote, type HookOutcome, type HookRunner, INJECT_EVENTS, notify, turnIndex, turnMessages, withContext, } from "./hooks.ts";
|
|
18
19
|
export { resetAll } from "./reset.ts";
|
|
19
20
|
export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.ts";
|
|
20
21
|
export { type RunTurnOptions, runTurn } from "./run-turn.ts";
|
|
@@ -22,4 +23,4 @@ export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.ts";
|
|
|
22
23
|
export { ask, clean, listLines, parseJson, resetHints, type SideTaskOptions, tryAsk, } from "./side-task.ts";
|
|
23
24
|
export { type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from "./stream.ts";
|
|
24
25
|
export { estimateTokens } from "./tokens.ts";
|
|
25
|
-
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SYSTEM, preselectInput, preselection, requestedNames, } from "./tool-loading.ts";
|
|
26
|
+
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.ts";
|
package/dist/index.js
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
* What is here is everything that does not know what the agent is *for*: making a tool schema
|
|
5
5
|
* a strict server will accept, getting tool definitions in front of a model without paying for
|
|
6
6
|
* all of them, reading one streamed turn back into a message, answering an endpoint that
|
|
7
|
-
* refuses one of those, one-shot calls that support a run, the
|
|
8
|
-
* pooled client, and the rules about retrying. What is not here is the work — orchestration,
|
|
7
|
+
* refuses one of those, one-shot calls that support a run, the host's side of lifecycle hooks,
|
|
8
|
+
* the event bus a watcher reads, a pooled client, and the rules about retrying. What is not here is the work — orchestration,
|
|
9
9
|
* prompts, and whatever the run is about — because that is the caller's, and it is the part
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
12
12
|
export { capabilitiesFor, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
13
13
|
export { contextLimitFor, getClient, listModels, NO_KEY, resetClients, timeoutMs, } from "./client.js";
|
|
14
14
|
export { errorMessage } from "./errors.js";
|
|
15
|
-
export { emit, endRun, fold, history, resetEvents, watch, } from "./events.js";
|
|
15
|
+
export { configureEvents, emit, endRun, fold, history, resetEvents, watch, } from "./events.js";
|
|
16
|
+
export { assembleContext, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, turnIndex, turnMessages, withContext, } from "./hooks.js";
|
|
16
17
|
export { resetAll } from "./reset.js";
|
|
17
18
|
export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
|
|
18
19
|
export { runTurn } from "./run-turn.js";
|
|
@@ -20,4 +21,4 @@ export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.js";
|
|
|
20
21
|
export { ask, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
|
|
21
22
|
export { streamTurn, } from "./stream.js";
|
|
22
23
|
export { estimateTokens } from "./tokens.js";
|
|
23
|
-
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SYSTEM, preselectInput, preselection, requestedNames, } from "./tool-loading.js";
|
|
24
|
+
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.js";
|
package/dist/retry.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export declare class EndpointSilent extends Error {
|
|
|
14
14
|
/**
|
|
15
15
|
* The request was bigger than the model will read. Its own class so nothing retries it: sending
|
|
16
16
|
* the same too-large request again is the same refusal, one round trip later.
|
|
17
|
+
*
|
|
18
|
+
* `runTurn` raises it from either side of the round trip — its own pre-flight guard, or the
|
|
19
|
+
* endpoint's refusal read back through `isOverflow` — so a caller has one thing to catch whether
|
|
20
|
+
* or not it gave a `contextLimit`. The second carries the endpoint's own message, and the error
|
|
21
|
+
* it was built from as `cause`.
|
|
17
22
|
*/
|
|
18
23
|
export declare class ContextOverflow extends Error {
|
|
19
24
|
readonly name = "ContextOverflow";
|
package/dist/retry.js
CHANGED
|
@@ -15,6 +15,11 @@ export class EndpointSilent extends Error {
|
|
|
15
15
|
/**
|
|
16
16
|
* The request was bigger than the model will read. Its own class so nothing retries it: sending
|
|
17
17
|
* the same too-large request again is the same refusal, one round trip later.
|
|
18
|
+
*
|
|
19
|
+
* `runTurn` raises it from either side of the round trip — its own pre-flight guard, or the
|
|
20
|
+
* endpoint's refusal read back through `isOverflow` — so a caller has one thing to catch whether
|
|
21
|
+
* or not it gave a `contextLimit`. The second carries the endpoint's own message, and the error
|
|
22
|
+
* it was built from as `cause`.
|
|
18
23
|
*/
|
|
19
24
|
export class ContextOverflow extends Error {
|
|
20
25
|
name = "ContextOverflow";
|
package/dist/run-turn.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ import { type StreamTurnOptions, type Turn } from "./stream.ts";
|
|
|
12
12
|
* The outer one is the endpoint being unreachable, busy or silent, which is not about this
|
|
13
13
|
* request at all and is worth simply waiting out.
|
|
14
14
|
*
|
|
15
|
+
* A request too big for the window is neither, and comes out of here as `ContextOverflow`
|
|
16
|
+
* however it was found out about — by the `contextLimit` guard below before a round trip was
|
|
17
|
+
* spent, or by the endpoint's own refusal after one. The option decides how early the caller
|
|
18
|
+
* hears, not what it hears.
|
|
19
|
+
*
|
|
15
20
|
* Both are bounded by the same rule: nothing is sent again once the model has started
|
|
16
21
|
* answering. The tokens are already out and on their way to whoever is watching, and a second
|
|
17
22
|
* attempt would say everything twice. That is what `produced` is, one box per attempt — set by
|
package/dist/run-turn.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { negotiate } from "./capabilities.js";
|
|
2
2
|
import { errorMessage } from "./errors.js";
|
|
3
|
-
import { backoffMs, ContextOverflow, compact, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
|
|
3
|
+
import { backoffMs, ContextOverflow, compact, isOverflow, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
|
|
4
4
|
import { streamTurn } from "./stream.js";
|
|
5
5
|
/**
|
|
6
6
|
* `request` is a callback rather than a body because the body has to be rebuilt from whatever
|
|
@@ -46,6 +46,19 @@ export async function runTurn(client, supports, request, { maxRetries = 0, onNot
|
|
|
46
46
|
// rules in `retry.ts` — so classifying first brings a cancelled run back from the dead.
|
|
47
47
|
if (produced.any || stream.signal?.aborted)
|
|
48
48
|
throw error;
|
|
49
|
+
// The endpoint's own refusal, classified here rather than left to the caller. One failure
|
|
50
|
+
// had two error types depending on an option about something else: a caller that gave a
|
|
51
|
+
// `contextLimit` got `ContextOverflow` from the guard above, and one that did not — the
|
|
52
|
+
// default — got a raw SDK error and had to know to run `isOverflow` over its message
|
|
53
|
+
// itself. `runTurn` is offered as the whole loop, so the classification this package
|
|
54
|
+
// already knows how to do belongs inside it.
|
|
55
|
+
//
|
|
56
|
+
// The original is kept as `cause`, because the endpoint's wording is the half that names
|
|
57
|
+
// the number. A rate limit borrows the same words and is not one of these — `isOverflow`
|
|
58
|
+
// rules it out, and it goes on to be retried below as the 429 it is.
|
|
59
|
+
if (!(error instanceof ContextOverflow) && isOverflow(errorMessage(error))) {
|
|
60
|
+
throw new ContextOverflow(errorMessage(error), { cause: error });
|
|
61
|
+
}
|
|
49
62
|
if (attempt >= maxRetries || !isTransient(error))
|
|
50
63
|
throw error;
|
|
51
64
|
const wait = backoffMs(attempt);
|
package/dist/side-task.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
import { capabilitiesFor, negotiate } from "./capabilities.js";
|
|
3
|
-
import { getClient } from "./client.js";
|
|
3
|
+
import { endpointKey, getClient } from "./client.js";
|
|
4
4
|
import { errorMessage } from "./errors.js";
|
|
5
5
|
import { isTransient } from "./retry.js";
|
|
6
6
|
/**
|
|
@@ -24,10 +24,10 @@ const NO_THINKING = { chat_template_kwargs: { enable_thinking: false } };
|
|
|
24
24
|
/**
|
|
25
25
|
* The models that turned out not to take the hints, by endpoint and model.
|
|
26
26
|
*
|
|
27
|
-
* Keyed rather than global for the reason the client cache is keyed
|
|
28
|
-
* what is on the other end, not about this
|
|
29
|
-
*
|
|
30
|
-
* second from ever being asked.
|
|
27
|
+
* Keyed rather than global for the reason the client cache is keyed, and on the same
|
|
28
|
+
* `endpointKey` it is: a refusal is a fact about what is on the other end, not about this
|
|
29
|
+
* process. A llama.cpp box and a cloud API are both reachable from one consumer over its
|
|
30
|
+
* lifetime, and the first one's refusal must not stop the second from ever being asked.
|
|
31
31
|
*
|
|
32
32
|
* The model belongs in the key for the same reason. One base URL is routinely many models —
|
|
33
33
|
* OpenRouter, LiteLLM, vLLM serving several at once — and whether `chat_template_kwargs` reaches
|
|
@@ -38,7 +38,7 @@ const NO_THINKING = { chat_template_kwargs: { enable_thinking: false } };
|
|
|
38
38
|
* the same (endpoint, model) pair, where a run on that model can read it too.
|
|
39
39
|
*/
|
|
40
40
|
const noHints = new Set();
|
|
41
|
-
const hintKey = (
|
|
41
|
+
const hintKey = (config, model) => JSON.stringify([endpointKey(config), model]);
|
|
42
42
|
/** Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints. */
|
|
43
43
|
export const resetHints = () => noHints.clear();
|
|
44
44
|
/**
|
|
@@ -108,12 +108,12 @@ export async function ask(config, model, system, user, { maxTokens = 512, temper
|
|
|
108
108
|
// whether a run or a side task found it out, and the point of latching it is that only one of
|
|
109
109
|
// them has to pay for it. Nothing here sends tools or `stream_options`, so the two
|
|
110
110
|
// endpoint-level flags are not in play — the model's three are the whole of what this meets.
|
|
111
|
-
const supports = capabilitiesFor(config.baseUrl);
|
|
111
|
+
const supports = capabilitiesFor(config.baseUrl, config.apiKey);
|
|
112
112
|
const attempt = (hints) => negotiate(supports, (_supports, _produced, refused) => send(hints, refused), {
|
|
113
113
|
model,
|
|
114
114
|
onNotice,
|
|
115
115
|
});
|
|
116
|
-
const key = hintKey(config
|
|
116
|
+
const key = hintKey(config, model);
|
|
117
117
|
const hints = !noHints.has(key);
|
|
118
118
|
let response;
|
|
119
119
|
try {
|
package/dist/stream.d.ts
CHANGED
|
@@ -18,8 +18,26 @@ export interface TurnUsage {
|
|
|
18
18
|
/** One streamed turn, put back together into the shape a loop and a transcript work with. */
|
|
19
19
|
export interface Turn {
|
|
20
20
|
content: string;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The narrower of the SDK's two tool-call shapes, because it is the only one built here — a
|
|
23
|
+
* streamed `tool_calls` delta carries a function and nothing else. Typed as the union it
|
|
24
|
+
* belongs to, every caller had to narrow before it could read `.function`, to rule out a
|
|
25
|
+
* custom call that this loop cannot produce. Still assignable wherever the union is wanted.
|
|
26
|
+
*/
|
|
27
|
+
toolCalls: OpenAI.ChatCompletionMessageFunctionToolCall[];
|
|
22
28
|
usage: TurnUsage;
|
|
29
|
+
/**
|
|
30
|
+
* Why the model stopped, in the endpoint's own words — `stop`, `length`, `tool_calls`, or `""`
|
|
31
|
+
* where it never said.
|
|
32
|
+
*
|
|
33
|
+
* Reported because `length` is otherwise invisible. A turn cut off at the token ceiling comes
|
|
34
|
+
* back as a well-formed `Turn` with truncated `content`, or with a tool call whose `arguments`
|
|
35
|
+
* stop mid-JSON — so the caller meets a parse failure with nothing to attribute it to. Being
|
|
36
|
+
* cut off looking whole is the same trap `throwIfAborted` below answers for the abort; this
|
|
37
|
+
* half is not an error, because the tokens are real and a caller may still want them, so it is
|
|
38
|
+
* handed over rather than raised.
|
|
39
|
+
*/
|
|
40
|
+
finishReason: string;
|
|
23
41
|
}
|
|
24
42
|
/**
|
|
25
43
|
* Whether the model has said anything a second attempt would say twice.
|
package/dist/stream.js
CHANGED
|
@@ -51,6 +51,7 @@ export async function streamTurn(client, body, { signal, idleMs, produced, onThi
|
|
|
51
51
|
const content = [];
|
|
52
52
|
const calls = new Map();
|
|
53
53
|
const usage = { prompt: 0, completion: 0, total: 0 };
|
|
54
|
+
let finishReason = "";
|
|
54
55
|
for await (const chunk of stream) {
|
|
55
56
|
// Rearmed on every chunk, latched below on only some: a priming chunk is the endpoint
|
|
56
57
|
// being alive, which is all the watchdog is asking about.
|
|
@@ -64,7 +65,15 @@ export async function streamTurn(client, body, { signal, idleMs, produced, onThi
|
|
|
64
65
|
usage.completion = chunk.usage.completion_tokens ?? 0;
|
|
65
66
|
usage.total = chunk.usage.total_tokens ?? 0;
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
// One choice, because that is what an agent loop asks for. A body with `n` above one
|
|
69
|
+
// keeps only the first; nothing here is built to reassemble several at once.
|
|
70
|
+
const choice = chunk.choices[0];
|
|
71
|
+
// Read before the delta guard rather than beside the content. The chunk that carries the
|
|
72
|
+
// reason usually carries an empty delta, and some servers send it with no delta at all —
|
|
73
|
+
// either of which the guard below skips, taking the reason with it.
|
|
74
|
+
if (choice?.finish_reason)
|
|
75
|
+
finishReason = choice.finish_reason;
|
|
76
|
+
const delta = choice?.delta;
|
|
68
77
|
if (!delta)
|
|
69
78
|
continue;
|
|
70
79
|
const thinking = delta.reasoning_content || delta.reasoning || "";
|
|
@@ -112,6 +121,7 @@ export async function streamTurn(client, body, { signal, idleMs, produced, onThi
|
|
|
112
121
|
function: { name: call.name, arguments: call.arguments },
|
|
113
122
|
})),
|
|
114
123
|
usage,
|
|
124
|
+
finishReason,
|
|
115
125
|
};
|
|
116
126
|
}
|
|
117
127
|
}
|
package/dist/tool-loading.d.ts
CHANGED
|
@@ -52,6 +52,11 @@ export declare function catalogPrompt(catalog: CatalogServer[], loaded?: Readonl
|
|
|
52
52
|
* in the position on-demand loading exists to avoid — a tool array too large to choose from.
|
|
53
53
|
* Over-broad requests are refused with the matching names listed, so the next call can be
|
|
54
54
|
* precise.
|
|
55
|
+
*
|
|
56
|
+
* The default rather than the rule: twelve is what a small model chooses well from, and a caller
|
|
57
|
+
* running a large one against a large window can say otherwise to `expandNames`, `preselection`
|
|
58
|
+
* and `preselectSystem`. Whatever it says is carried on the resolution, so the refusal the model
|
|
59
|
+
* reads names the number it was actually held to.
|
|
55
60
|
*/
|
|
56
61
|
export declare const MAX_PER_LOAD = 12;
|
|
57
62
|
/**
|
|
@@ -59,7 +64,7 @@ export declare const MAX_PER_LOAD = 12;
|
|
|
59
64
|
* conversation runs; least-recently-used names fall off the front.
|
|
60
65
|
*
|
|
61
66
|
* Only a multi-turn caller needs this — a run that starts from nothing each time has nothing to
|
|
62
|
-
* carry. See `carryOver
|
|
67
|
+
* carry. See `carryOver`, which takes another number if this one is not yours.
|
|
63
68
|
*/
|
|
64
69
|
export declare const MAX_CARRIED = 16;
|
|
65
70
|
/**
|
|
@@ -67,8 +72,10 @@ export declare const MAX_CARRIED = 16;
|
|
|
67
72
|
*
|
|
68
73
|
* @param previous Last turn's names, oldest first.
|
|
69
74
|
* @param used What this turn called. Moved to the end, so the oldest unused fall off.
|
|
75
|
+
* @param max How many to carry, defaulting to `MAX_CARRIED`. At least one: a cap of zero is
|
|
76
|
+
* read as no cap, which is what `slice` does with it and not what anybody asking for zero meant.
|
|
70
77
|
*/
|
|
71
|
-
export declare const carryOver: (previous: string[], used: Set<string
|
|
78
|
+
export declare const carryOver: (previous: string[], used: Set<string>, max?: number) => string[];
|
|
72
79
|
/**
|
|
73
80
|
* Resolves requested names against the catalogue, expanding trailing `*` wildcards.
|
|
74
81
|
*
|
|
@@ -80,8 +87,10 @@ export declare const carryOver: (previous: string[], used: Set<string>) => strin
|
|
|
80
87
|
*
|
|
81
88
|
* @param requested What the model asked for. A trailing `*` expands.
|
|
82
89
|
* @param catalog The servers to resolve against.
|
|
90
|
+
* @param maxPerLoad The most this call may load, defaulting to `MAX_PER_LOAD`. It comes back on
|
|
91
|
+
* the resolution so `loadResult` reports the same number rather than a second opinion of it.
|
|
83
92
|
*/
|
|
84
|
-
export declare function expandNames(requested: string[], catalog: CatalogServer[]): {
|
|
93
|
+
export declare function expandNames(requested: string[], catalog: CatalogServer[], maxPerLoad?: number): {
|
|
85
94
|
matched: string[];
|
|
86
95
|
unknown: string[];
|
|
87
96
|
overBroad: {
|
|
@@ -89,6 +98,7 @@ export declare function expandNames(requested: string[], catalog: CatalogServer[
|
|
|
89
98
|
hits: string[];
|
|
90
99
|
}[];
|
|
91
100
|
deferred: string[];
|
|
101
|
+
maxPerLoad: number;
|
|
92
102
|
};
|
|
93
103
|
/**
|
|
94
104
|
* What `load_tools` reports back: the descriptions, now that they are worth their tokens.
|
|
@@ -96,7 +106,7 @@ export declare function expandNames(requested: string[], catalog: CatalogServer[
|
|
|
96
106
|
* @param expanded What `expandNames` resolved: the matches, the misses, and the over-broad asks.
|
|
97
107
|
* @param catalog The servers, read for the descriptions now worth their tokens.
|
|
98
108
|
*/
|
|
99
|
-
export declare function loadResult({ matched, unknown, overBroad, deferred }: ReturnType<typeof expandNames>, catalog: CatalogServer[]): string;
|
|
109
|
+
export declare function loadResult({ matched, unknown, overBroad, deferred, maxPerLoad }: ReturnType<typeof expandNames>, catalog: CatalogServer[]): string;
|
|
100
110
|
/**
|
|
101
111
|
* Whether the catalogue holds a tool by this name.
|
|
102
112
|
*
|
|
@@ -111,7 +121,7 @@ export declare const inCatalog: (catalog: CatalogServer[], name: string) => bool
|
|
|
111
121
|
*/
|
|
112
122
|
export declare function requestedNames(args: Record<string, unknown>): string[];
|
|
113
123
|
/**
|
|
114
|
-
*
|
|
124
|
+
* The system prompt a preselector is given, holding it to the cap its answer will be held to.
|
|
115
125
|
*
|
|
116
126
|
* On-demand loading otherwise costs a round trip every run: the model reads the catalogue,
|
|
117
127
|
* calls `load_tools`, and only then can do the work. A small model reading the same catalogue
|
|
@@ -119,22 +129,32 @@ export declare function requestedNames(args: Record<string, unknown>): string[];
|
|
|
119
129
|
* starts working on its first step.
|
|
120
130
|
*
|
|
121
131
|
* A wrong guess is cheap — an unused definition is a few hundred tokens for one run — but a
|
|
122
|
-
* broad guess is not, so the same
|
|
132
|
+
* broad guess is not, so the same cap applies here as to a `load_tools` call.
|
|
133
|
+
*
|
|
134
|
+
* @param maxPerLoad The most to ask for, defaulting to `MAX_PER_LOAD`. Give `preselection` the
|
|
135
|
+
* same number: this one is what the preselector is told, and that one is what it is held to.
|
|
123
136
|
*/
|
|
137
|
+
export declare const preselectSystem: (maxPerLoad?: number) => string;
|
|
138
|
+
/** The preselection system prompt at the default cap, for a caller that never changes it. */
|
|
124
139
|
export declare const PRESELECT_SYSTEM: string;
|
|
125
140
|
/**
|
|
126
141
|
* The user message for a preselection call: the catalogue, then the request.
|
|
127
142
|
*
|
|
128
143
|
* @param catalog The connected servers, rendered as the name-only listing.
|
|
129
|
-
* @param prompt The request being planned for, truncated
|
|
130
|
-
*
|
|
144
|
+
* @param prompt The request being planned for, truncated — choosing tools needs the shape of the
|
|
145
|
+
* ask, not all of it.
|
|
146
|
+
* @param maxPromptChars Where the request is cut, defaulting to 2000. A caller whose requests
|
|
147
|
+
* carry the part that names the work at the end wants a larger one, and pays for it in the
|
|
148
|
+
* preselector's prompt.
|
|
131
149
|
*/
|
|
132
|
-
export declare const preselectInput: (catalog: CatalogServer[], prompt: string) => string;
|
|
150
|
+
export declare const preselectInput: (catalog: CatalogServer[], prompt: string, maxPromptChars?: number) => string;
|
|
133
151
|
/**
|
|
134
152
|
* Resolves a preselection against the catalogue: unknown names dropped, count capped.
|
|
135
153
|
*
|
|
136
154
|
* @param names What the preselector replied. Unvalidated: a non-array gives none, and entries
|
|
137
155
|
* that are not strings are dropped.
|
|
138
156
|
* @param catalog The servers to resolve against.
|
|
157
|
+
* @param maxPerLoad The most to keep, defaulting to `MAX_PER_LOAD`. The same number
|
|
158
|
+
* `preselectSystem` was given, or the model is being held to a cap it was never told about.
|
|
139
159
|
*/
|
|
140
|
-
export declare function preselection(names: unknown, catalog: CatalogServer[]): string[];
|
|
160
|
+
export declare function preselection(names: unknown, catalog: CatalogServer[], maxPerLoad?: number): string[];
|
package/dist/tool-loading.js
CHANGED
|
@@ -103,6 +103,11 @@ const flatten = (catalog) => catalog.flatMap((server) => server.tools);
|
|
|
103
103
|
* in the position on-demand loading exists to avoid — a tool array too large to choose from.
|
|
104
104
|
* Over-broad requests are refused with the matching names listed, so the next call can be
|
|
105
105
|
* precise.
|
|
106
|
+
*
|
|
107
|
+
* The default rather than the rule: twelve is what a small model chooses well from, and a caller
|
|
108
|
+
* running a large one against a large window can say otherwise to `expandNames`, `preselection`
|
|
109
|
+
* and `preselectSystem`. Whatever it says is carried on the resolution, so the refusal the model
|
|
110
|
+
* reads names the number it was actually held to.
|
|
106
111
|
*/
|
|
107
112
|
export const MAX_PER_LOAD = 12;
|
|
108
113
|
/**
|
|
@@ -110,7 +115,7 @@ export const MAX_PER_LOAD = 12;
|
|
|
110
115
|
* conversation runs; least-recently-used names fall off the front.
|
|
111
116
|
*
|
|
112
117
|
* Only a multi-turn caller needs this — a run that starts from nothing each time has nothing to
|
|
113
|
-
* carry. See `carryOver
|
|
118
|
+
* carry. See `carryOver`, which takes another number if this one is not yours.
|
|
114
119
|
*/
|
|
115
120
|
export const MAX_CARRIED = 16;
|
|
116
121
|
/**
|
|
@@ -118,8 +123,10 @@ export const MAX_CARRIED = 16;
|
|
|
118
123
|
*
|
|
119
124
|
* @param previous Last turn's names, oldest first.
|
|
120
125
|
* @param used What this turn called. Moved to the end, so the oldest unused fall off.
|
|
126
|
+
* @param max How many to carry, defaulting to `MAX_CARRIED`. At least one: a cap of zero is
|
|
127
|
+
* read as no cap, which is what `slice` does with it and not what anybody asking for zero meant.
|
|
121
128
|
*/
|
|
122
|
-
export const carryOver = (previous, used) => [...previous.filter((name) => !used.has(name)), ...used].slice(-
|
|
129
|
+
export const carryOver = (previous, used, max = MAX_CARRIED) => [...previous.filter((name) => !used.has(name)), ...used].slice(-Math.max(1, max));
|
|
123
130
|
/**
|
|
124
131
|
* Resolves requested names against the catalogue, expanding trailing `*` wildcards.
|
|
125
132
|
*
|
|
@@ -131,8 +138,10 @@ export const carryOver = (previous, used) => [...previous.filter((name) => !used
|
|
|
131
138
|
*
|
|
132
139
|
* @param requested What the model asked for. A trailing `*` expands.
|
|
133
140
|
* @param catalog The servers to resolve against.
|
|
141
|
+
* @param maxPerLoad The most this call may load, defaulting to `MAX_PER_LOAD`. It comes back on
|
|
142
|
+
* the resolution so `loadResult` reports the same number rather than a second opinion of it.
|
|
134
143
|
*/
|
|
135
|
-
export function expandNames(requested, catalog) {
|
|
144
|
+
export function expandNames(requested, catalog, maxPerLoad = MAX_PER_LOAD) {
|
|
136
145
|
const all = flatten(catalog);
|
|
137
146
|
const matched = new Set();
|
|
138
147
|
const unknown = [];
|
|
@@ -179,15 +188,15 @@ export function expandNames(requested, catalog) {
|
|
|
179
188
|
// enough already — telling a model that asked for one exact tool that it matches one tool,
|
|
180
189
|
// "more than the twelve one call may load", and to choose from a list holding just that
|
|
181
190
|
// name, leaves it nothing to do but send the identical call again.
|
|
182
|
-
if (fresh.length >
|
|
191
|
+
if (fresh.length > maxPerLoad)
|
|
183
192
|
overBroad.push({ name, hits });
|
|
184
|
-
else if (matched.size + fresh.length >
|
|
193
|
+
else if (matched.size + fresh.length > maxPerLoad)
|
|
185
194
|
deferred.push(name);
|
|
186
195
|
else
|
|
187
196
|
for (const hit of hits)
|
|
188
197
|
matched.add(hit);
|
|
189
198
|
}
|
|
190
|
-
return { matched: [...matched], unknown, overBroad, deferred };
|
|
199
|
+
return { matched: [...matched], unknown, overBroad, deferred, maxPerLoad };
|
|
191
200
|
}
|
|
192
201
|
/**
|
|
193
202
|
* What `load_tools` reports back: the descriptions, now that they are worth their tokens.
|
|
@@ -195,7 +204,7 @@ export function expandNames(requested, catalog) {
|
|
|
195
204
|
* @param expanded What `expandNames` resolved: the matches, the misses, and the over-broad asks.
|
|
196
205
|
* @param catalog The servers, read for the descriptions now worth their tokens.
|
|
197
206
|
*/
|
|
198
|
-
export function loadResult({ matched, unknown, overBroad, deferred }, catalog) {
|
|
207
|
+
export function loadResult({ matched, unknown, overBroad, deferred, maxPerLoad }, catalog) {
|
|
199
208
|
const byName = new Map(flatten(catalog).map((tool) => [tool.name, tool.description]));
|
|
200
209
|
const lines = [];
|
|
201
210
|
if (matched.length) {
|
|
@@ -206,12 +215,12 @@ export function loadResult({ matched, unknown, overBroad, deferred }, catalog) {
|
|
|
206
215
|
for (const { name, hits } of overBroad) {
|
|
207
216
|
if (lines.length)
|
|
208
217
|
lines.push("");
|
|
209
|
-
lines.push(`\`${name}\` matches ${hits.length} tools, more than the ${
|
|
218
|
+
lines.push(`\`${name}\` matches ${hits.length} tools, more than the ${maxPerLoad} one call may load.`, "Name the ones you need from:", ...hits.map((hit) => ` ${hit}`));
|
|
210
219
|
}
|
|
211
220
|
if (deferred.length) {
|
|
212
221
|
if (lines.length)
|
|
213
222
|
lines.push("");
|
|
214
|
-
lines.push(`This call is full at ${
|
|
223
|
+
lines.push(`This call is full at ${maxPerLoad} tools, so these were not loaded: ${deferred.join(", ")}.`, "Ask for them on your next step.");
|
|
215
224
|
}
|
|
216
225
|
if (unknown.length) {
|
|
217
226
|
if (lines.length)
|
|
@@ -241,7 +250,15 @@ export function requestedNames(args) {
|
|
|
241
250
|
return [];
|
|
242
251
|
}
|
|
243
252
|
/**
|
|
244
|
-
*
|
|
253
|
+
* Where a request is cut for the preselector, in characters.
|
|
254
|
+
*
|
|
255
|
+
* A tool choice is made on what the work is, which is the top of a request rather than all of
|
|
256
|
+
* it — and the whole of a long one is paid for again in the preselection call. `preselectInput`
|
|
257
|
+
* takes another number for a caller whose requests are not shaped that way.
|
|
258
|
+
*/
|
|
259
|
+
const PRESELECT_PROMPT_CHARS = 2000;
|
|
260
|
+
/**
|
|
261
|
+
* The system prompt a preselector is given, holding it to the cap its answer will be held to.
|
|
245
262
|
*
|
|
246
263
|
* On-demand loading otherwise costs a round trip every run: the model reads the catalogue,
|
|
247
264
|
* calls `load_tools`, and only then can do the work. A small model reading the same catalogue
|
|
@@ -249,30 +266,40 @@ export function requestedNames(args) {
|
|
|
249
266
|
* starts working on its first step.
|
|
250
267
|
*
|
|
251
268
|
* A wrong guess is cheap — an unused definition is a few hundred tokens for one run — but a
|
|
252
|
-
* broad guess is not, so the same
|
|
269
|
+
* broad guess is not, so the same cap applies here as to a `load_tools` call.
|
|
270
|
+
*
|
|
271
|
+
* @param maxPerLoad The most to ask for, defaulting to `MAX_PER_LOAD`. Give `preselection` the
|
|
272
|
+
* same number: this one is what the preselector is told, and that one is what it is held to.
|
|
253
273
|
*/
|
|
254
|
-
export const
|
|
274
|
+
export const preselectSystem = (maxPerLoad = MAX_PER_LOAD) => "You choose tools. Below is a catalogue of tool names, then a request. Reply with a JSON " +
|
|
255
275
|
"array of the names the request is likely to need — exact names from the catalogue, at most " +
|
|
256
|
-
`${
|
|
276
|
+
`${maxPerLoad}, and as few as could do the job. Reply with \`[]\` if the request can be ` +
|
|
257
277
|
"answered without tools. Reply with the array alone — no prose, no explanation.";
|
|
278
|
+
/** The preselection system prompt at the default cap, for a caller that never changes it. */
|
|
279
|
+
export const PRESELECT_SYSTEM = preselectSystem();
|
|
258
280
|
/**
|
|
259
281
|
* The user message for a preselection call: the catalogue, then the request.
|
|
260
282
|
*
|
|
261
283
|
* @param catalog The connected servers, rendered as the name-only listing.
|
|
262
|
-
* @param prompt The request being planned for, truncated
|
|
263
|
-
*
|
|
284
|
+
* @param prompt The request being planned for, truncated — choosing tools needs the shape of the
|
|
285
|
+
* ask, not all of it.
|
|
286
|
+
* @param maxPromptChars Where the request is cut, defaulting to 2000. A caller whose requests
|
|
287
|
+
* carry the part that names the work at the end wants a larger one, and pays for it in the
|
|
288
|
+
* preselector's prompt.
|
|
264
289
|
*/
|
|
265
|
-
export const preselectInput = (catalog, prompt) => `# Tool catalogue\n\n${catalogList(catalog)}\n\n# Request\n\n${prompt.slice(0,
|
|
290
|
+
export const preselectInput = (catalog, prompt, maxPromptChars = PRESELECT_PROMPT_CHARS) => `# Tool catalogue\n\n${catalogList(catalog)}\n\n# Request\n\n${prompt.slice(0, maxPromptChars)}`;
|
|
266
291
|
/**
|
|
267
292
|
* Resolves a preselection against the catalogue: unknown names dropped, count capped.
|
|
268
293
|
*
|
|
269
294
|
* @param names What the preselector replied. Unvalidated: a non-array gives none, and entries
|
|
270
295
|
* that are not strings are dropped.
|
|
271
296
|
* @param catalog The servers to resolve against.
|
|
297
|
+
* @param maxPerLoad The most to keep, defaulting to `MAX_PER_LOAD`. The same number
|
|
298
|
+
* `preselectSystem` was given, or the model is being held to a cap it was never told about.
|
|
272
299
|
*/
|
|
273
|
-
export function preselection(names, catalog) {
|
|
300
|
+
export function preselection(names, catalog, maxPerLoad = MAX_PER_LOAD) {
|
|
274
301
|
if (!Array.isArray(names))
|
|
275
302
|
return [];
|
|
276
303
|
const wanted = names.filter((name) => typeof name === "string");
|
|
277
|
-
return expandNames(wanted, catalog).matched.slice(0,
|
|
304
|
+
return expandNames(wanted, catalog, maxPerLoad).matched.slice(0, maxPerLoad);
|
|
278
305
|
}
|
package/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 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.
|
|
4
4
|
|
|
5
|
-
What is here is everything that does not know what the agent is *for*: making a tool schema a strict server will accept, getting tool definitions in front of a model without paying for all of them, reading one streamed turn back into a message, answering an endpoint that refuses one of those, one-shot calls that support a run, the event bus a watcher reads, a pooled client, and the rules about retrying. What is not here is the work — orchestration, prompts, and whatever the run is about — because that is the caller's, and it is the part that differs between one server and the next.
|
|
5
|
+
What is here is everything that does not know what the agent is *for*: making a tool schema a strict server will accept, getting tool definitions in front of a model without paying for all of them, reading one streamed turn back into a message, answering an endpoint that refuses one of those, one-shot calls that support a run, the host's side of lifecycle hooks, the event bus a watcher reads, a pooled client, and the rules about retrying. What is not here is the work — orchestration, prompts, and whatever the run is about — because that is the caller's, and it is the part that differs between one server and the next.
|
|
6
6
|
|
|
7
7
|
Requires Node >=22, with `openai` >=6 as a peer dependency. ESM only.
|
|
8
8
|
Full prose, worked examples and the reasoning behind each seam are in README.md; this file is the index.
|
|
@@ -55,6 +55,8 @@ What this package needs to know about a caller's configuration.
|
|
|
55
55
|
|
|
56
56
|
What a run is doing, while it is doing it.
|
|
57
57
|
|
|
58
|
+
- `configureEvents` — Changes what the bus keeps, for a process whose runs are not shaped like the ones these defaults were chosen for.
|
|
59
|
+
- `EventBusOptions` (type) — What the bus keeps and for how long.
|
|
58
60
|
- `emit` — Records one event and hands it to everyone watching that run.
|
|
59
61
|
- `endRun` — Forgets a run that will not be emitting `done` — one whose process is tearing down, or whose loop threw where it could not be caught.
|
|
60
62
|
- `fold` — Consecutive tokens of one kind are one thing being said, not hundreds of things.
|
|
@@ -66,6 +68,28 @@ What a run is doing, while it is doing it.
|
|
|
66
68
|
- `resetEvents` — Test seam: forget every run, so one test's events cannot be read by the next.
|
|
67
69
|
- `watch` — Everything that has happened on a run, then everything that happens next, until it ends.
|
|
68
70
|
|
|
71
|
+
### hooks
|
|
72
|
+
|
|
73
|
+
Lifecycle hooks, from the host's side: what a session looks like to them, where their context lands in a request, and what is said about each one.
|
|
74
|
+
|
|
75
|
+
- `assembleContext` — Builds the context a set of outcomes adds and the notes that go with it.
|
|
76
|
+
- `Gathered` (type) — The context a set of outcomes adds to a request, and a note for each hook worth mentioning.
|
|
77
|
+
- `gather` — Runs the hooks ahead of a request and builds what they add to it.
|
|
78
|
+
- `HOOK_CONTEXT_TOKENS` — The most context all of a request's hooks add between them, in estimated tokens.
|
|
79
|
+
- `HOOK_EVENTS` — Every event a hook can be bound to, in the order a session meets them.
|
|
80
|
+
- `HOOK_PREFACE` — Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
81
|
+
- `HookContext` (type) — What a host knows at an event.
|
|
82
|
+
- `HookEvent` (type) — A point in a session a hook can be bound to.
|
|
83
|
+
- `HookMessage` (type) — One message of a session as a hook is handed it.
|
|
84
|
+
- `HookNote` (type) — One hook's line for whoever is watching: the context it added, or why it added none.
|
|
85
|
+
- `HookOutcome` (type) — What one hook did.
|
|
86
|
+
- `HookRunner` (type) — Runs one event's hooks.
|
|
87
|
+
- `INJECT_EVENTS` — The events whose hooks run before a request, and so the only ones whose output can reach it.
|
|
88
|
+
- `notify` — Runs the hooks for an event that reads what happened and adds nothing to a request.
|
|
89
|
+
- `turnIndex` — Which turn of a session begins at a point, from 0: the user messages ahead of it.
|
|
90
|
+
- `turnMessages` — A stretch of a transcript as a hook reads it: what the user and the assistant said, and nothing else.
|
|
91
|
+
- `withContext` — The request, with the hooks' context added to this turn's question.
|
|
92
|
+
|
|
69
93
|
### reset
|
|
70
94
|
|
|
71
95
|
- `resetAll` — Forgets everything this package remembers between calls.
|
|
@@ -137,7 +161,8 @@ Reading one streamed turn back into a message.
|
|
|
137
161
|
- `loadResult` — What `load_tools` reports back: the descriptions, now that they are worth their tokens.
|
|
138
162
|
- `MAX_CARRIED` — The most a conversation carries between turns.
|
|
139
163
|
- `MAX_PER_LOAD` — The most a single `load_tools` call may pull in.
|
|
140
|
-
- `PRESELECT_SYSTEM` —
|
|
164
|
+
- `PRESELECT_SYSTEM` — The preselection system prompt at the default cap, for a caller that never changes it.
|
|
141
165
|
- `preselectInput` — The user message for a preselection call: the catalogue, then the request.
|
|
142
166
|
- `preselection` — Resolves a preselection against the catalogue: unknown names dropped, count capped.
|
|
167
|
+
- `preselectSystem` — The system prompt a preselector is given, holding it to the cap its answer will be held to.
|
|
143
168
|
- `requestedNames` — `load_tools` arguments, defensively — a model may send a bare string or a nested object.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|