@cubicecho/agent-core 2.0.0 → 2.0.2
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 +38 -6
- package/dist/capabilities.d.ts +8 -0
- package/dist/capabilities.js +7 -0
- package/dist/client.d.ts +21 -2
- package/dist/client.js +21 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/events.d.ts +25 -2
- package/dist/events.js +33 -6
- package/dist/retry.d.ts +28 -2
- package/dist/retry.js +28 -2
- package/dist/run-turn.d.ts +5 -0
- package/dist/run-turn.js +5 -0
- package/dist/schema-compat.d.ts +13 -0
- package/dist/schema-compat.js +58 -0
- package/dist/side-task.d.ts +29 -3
- package/dist/side-task.js +25 -3
- package/dist/stream.d.ts +6 -0
- package/dist/stream.js +4 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +2 -0
- package/dist/tool-loading.d.ts +46 -4
- package/dist/tool-loading.js +46 -4
- package/llms.txt +141 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ only, Node >=22.
|
|
|
25
25
|
| `stream` | Reads one streamed turn back into a message: token callbacks, tool-call reassembly, and the idle watchdog that turns a silent endpoint into `EndpointSilent`. |
|
|
26
26
|
| `capabilities` | What an endpoint turned out not to support, per endpoint, and the loop that answers it when it says so. `capabilitiesFor`, `negotiate`. |
|
|
27
27
|
| `side-task` | One-shot calls that support a run without being one — small prompt, short answer, no tools, never worth failing the run over. |
|
|
28
|
-
| `events` | The in-memory bus a watcher reads while a run happens. |
|
|
28
|
+
| `events` | The in-memory bus a watcher reads while a run happens: `emit`, `watch`, `history`, `fold`. A watcher's backlog is capped and reports its own gaps. |
|
|
29
29
|
| `client` | A pooled `OpenAI` client per endpoint, plus the context-window listing and its cache. |
|
|
30
30
|
| `retry` | What to do when a request is lost, refused or too big: `isTransient`, `backoffMs`, `ContextOverflow`, `EndpointSilent`, `requestTokens`. |
|
|
31
31
|
| `config` | The structural interfaces every function here asks for. |
|
|
@@ -38,6 +38,10 @@ only, Node >=22.
|
|
|
38
38
|
What is **not** here is the work: orchestration, prompts, and whatever the run is about. That
|
|
39
39
|
is the caller's, and it is the part that actually differs between one server and the next.
|
|
40
40
|
|
|
41
|
+
`llms.txt` is the same surface as a flat index — every export with the first line of its own doc
|
|
42
|
+
comment. It is generated from `src/index.ts` by `npm run llms`, which the build runs, so it is
|
|
43
|
+
the exports rather than a second description of them; CI fails if the committed copy has drifted.
|
|
44
|
+
|
|
41
45
|
## A turn
|
|
42
46
|
|
|
43
47
|
`negotiate` wrapping `streamTurn` is the whole of one turn against an endpoint: the request is
|
|
@@ -46,7 +50,7 @@ and nothing is re-sent once it has started answering.
|
|
|
46
50
|
|
|
47
51
|
```ts
|
|
48
52
|
import {
|
|
49
|
-
capabilitiesFor, getClient, negotiate, relaxTools, sanitizeTools, streamTurn, timeoutMs,
|
|
53
|
+
capabilitiesFor, emit, getClient, negotiate, relaxTools, sanitizeTools, streamTurn, timeoutMs,
|
|
50
54
|
} from "@cubicecho/agent-core";
|
|
51
55
|
|
|
52
56
|
const declared = sanitizeTools(tools);
|
|
@@ -82,13 +86,14 @@ slowly needs.
|
|
|
82
86
|
`runTurn` will refuse a request that cannot fit rather than spending a round trip finding out:
|
|
83
87
|
|
|
84
88
|
```ts
|
|
85
|
-
import { contextLimitFor, runTurn } from "@cubicecho/agent-core";
|
|
89
|
+
import { contextLimitFor, emit, runTurn } from "@cubicecho/agent-core";
|
|
86
90
|
|
|
87
91
|
const turn = await runTurn(client, supports, build, {
|
|
88
92
|
maxRetries: 3,
|
|
89
|
-
// Opt-in: the number is the caller's to find
|
|
90
|
-
//
|
|
91
|
-
|
|
93
|
+
// Opt-in: the number is the caller's to find, because neither of these is network I/O a
|
|
94
|
+
// turn should be doing. The second argument is the operator's own number and it wins
|
|
95
|
+
// outright when set, so there is no need to check it yourself first.
|
|
96
|
+
contextLimit: await contextLimitFor({ ...settings, model }, settings.contextLength),
|
|
92
97
|
onNotice: (message) => emit(runId, { kind: "notice", text: message }),
|
|
93
98
|
});
|
|
94
99
|
```
|
|
@@ -98,6 +103,33 @@ before it and the transcript does not change between retries. A `ContextOverflow
|
|
|
98
103
|
neither a capability `negotiate` can answer nor something `isTransient` accepts, so it leaves
|
|
99
104
|
both loops on the first attempt.
|
|
100
105
|
|
|
106
|
+
## Watching a run
|
|
107
|
+
|
|
108
|
+
`watch` replays what the run has already emitted, then yields what happens next until `done`.
|
|
109
|
+
`emit` assigns `seq`, a per-run counter from 1, and that is what a client orders and
|
|
110
|
+
de-duplicates on.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { watch } from "@cubicecho/agent-core";
|
|
114
|
+
|
|
115
|
+
for await (const event of watch(runId)) {
|
|
116
|
+
render(event);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A watcher that stops keeping up is capped rather than left to grow. Its queue holds the most
|
|
121
|
+
recent 1000 events — trimmed in batches, so it runs a little over that before cutting back —
|
|
122
|
+
the oldest go, and what is dropped is released where it is dropped, not held until a consumer
|
|
123
|
+
that has already stalled next reads.
|
|
124
|
+
|
|
125
|
+
The gap is reported once, as a single `notice`, not once per lost event. It carries the `seq`
|
|
126
|
+
immediately before the event that follows it: inside the gap, where no real event will ever
|
|
127
|
+
appear. Sharing a `seq` with the event behind it would make the pair indistinguishable from a
|
|
128
|
+
repeat, and a client doing what `seq` is documented for would throw away one of the two.
|
|
129
|
+
|
|
130
|
+
`history` reads what a run has emitted without subscribing, `fold` collapses a token stream into
|
|
131
|
+
blocks for display, and `endRun` drops a finished run's buffer.
|
|
132
|
+
|
|
101
133
|
## The config seam
|
|
102
134
|
|
|
103
135
|
Nothing here imports a config type from a consumer, and no function asks for a whole
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -27,10 +27,13 @@ export interface Capabilities {
|
|
|
27
27
|
/**
|
|
28
28
|
* What this endpoint is known not to support. The same object every time, so what `negotiate`
|
|
29
29
|
* latches off stays off.
|
|
30
|
+
*
|
|
31
|
+
* @param baseUrl Identifies the endpoint. These are per-server, not per-model.
|
|
30
32
|
*/
|
|
31
33
|
export declare function capabilitiesFor(baseUrl: string): Capabilities;
|
|
32
34
|
/** Forgets every endpoint's capabilities. For tests, and for a settings change under test. */
|
|
33
35
|
export declare function resetCapabilities(): void;
|
|
36
|
+
/** What `negotiate` takes besides the request. Both optional, both about telling someone. */
|
|
34
37
|
export interface NegotiateOptions {
|
|
35
38
|
/**
|
|
36
39
|
* The flag `send` will be given, for a caller that has to read it after `negotiate` returns.
|
|
@@ -64,5 +67,10 @@ export interface NegotiateOptions {
|
|
|
64
67
|
* ever one flag in a turn — the same box `streamTurn` sets and the re-send below reads — and a
|
|
65
68
|
* caller that passed it to only one of the two got a turn that had already streamed tokens sent
|
|
66
69
|
* again, silently, with the watcher seeing every one of them twice.
|
|
70
|
+
*
|
|
71
|
+
* @param supports What this endpoint has already refused. Latched off further as it refuses more.
|
|
72
|
+
* @param send Builds and sends the request. Called again per downgrade, never once tokens
|
|
73
|
+
* have arrived.
|
|
74
|
+
* @param options `produced` for a caller with its own retry budget, `onNotice` for a watcher.
|
|
67
75
|
*/
|
|
68
76
|
export declare function negotiate<T>(supports: Capabilities, send: (supports: Capabilities, produced: Produced) => Promise<T>, { produced, onNotice }?: NegotiateOptions): Promise<T>;
|
package/dist/capabilities.js
CHANGED
|
@@ -15,6 +15,8 @@ const capabilities = new Map();
|
|
|
15
15
|
/**
|
|
16
16
|
* What this endpoint is known not to support. The same object every time, so what `negotiate`
|
|
17
17
|
* latches off stays off.
|
|
18
|
+
*
|
|
19
|
+
* @param baseUrl Identifies the endpoint. These are per-server, not per-model.
|
|
18
20
|
*/
|
|
19
21
|
export function capabilitiesFor(baseUrl) {
|
|
20
22
|
let known = capabilities.get(baseUrl);
|
|
@@ -51,6 +53,11 @@ const REJECTS_USAGE = /stream_options/i;
|
|
|
51
53
|
* ever one flag in a turn — the same box `streamTurn` sets and the re-send below reads — and a
|
|
52
54
|
* caller that passed it to only one of the two got a turn that had already streamed tokens sent
|
|
53
55
|
* again, silently, with the watcher seeing every one of them twice.
|
|
56
|
+
*
|
|
57
|
+
* @param supports What this endpoint has already refused. Latched off further as it refuses more.
|
|
58
|
+
* @param send Builds and sends the request. Called again per downgrade, never once tokens
|
|
59
|
+
* have arrived.
|
|
60
|
+
* @param options `produced` for a caller with its own retry budget, `onNotice` for a watcher.
|
|
54
61
|
*/
|
|
55
62
|
export async function negotiate(supports, send, { produced = { any: false }, onNotice } = {}) {
|
|
56
63
|
for (;;) {
|
package/dist/client.d.ts
CHANGED
|
@@ -6,15 +6,31 @@ import type { Endpoint } from "./config.ts";
|
|
|
6
6
|
* not — a caller must not let a local endpoint silently borrow the key meant for a paid one.
|
|
7
7
|
*/
|
|
8
8
|
export declare const NO_KEY = "agent-core";
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Zero, less, or absent means no limit, which the SDK spells as `undefined`.
|
|
11
|
+
*
|
|
12
|
+
* @param config Read for `requestTimeoutSeconds` alone.
|
|
13
|
+
*/
|
|
10
14
|
export declare const timeoutMs: (config: Pick<Endpoint, "requestTimeoutSeconds">) => number | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The client for an endpoint, built once and kept.
|
|
17
|
+
*
|
|
18
|
+
* Pooled on the three fields that change how a request is sent, so agents sharing a server share
|
|
19
|
+
* a connection and agents on different servers never share a client.
|
|
20
|
+
*
|
|
21
|
+
* @param config Where to send requests and how long to wait. An absent `apiKey` becomes `NO_KEY`.
|
|
22
|
+
*/
|
|
11
23
|
export declare function getClient(config: Endpoint): OpenAI;
|
|
12
24
|
/** A model an endpoint offers, and what it says the model will read. Zero means it did not say. */
|
|
13
25
|
export interface ModelInfo {
|
|
14
26
|
id: string;
|
|
15
27
|
contextLength: number;
|
|
16
28
|
}
|
|
17
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Asks an endpoint what it serves, and remembers the answer.
|
|
31
|
+
*
|
|
32
|
+
* @param config The endpoint to ask. Remembered per base URL and key, not per model.
|
|
33
|
+
*/
|
|
18
34
|
export declare function listModels(config: Endpoint): Promise<ModelInfo[]>;
|
|
19
35
|
/**
|
|
20
36
|
* How much a model will read, in tokens. Zero means nobody knows.
|
|
@@ -28,6 +44,9 @@ export declare function listModels(config: Endpoint): Promise<ModelInfo[]>;
|
|
|
28
44
|
* model, since a model can arrive after the first listing was taken. A server that will not
|
|
29
45
|
* list models still has to be able to run a turn: a failure here is an unknown window, not a
|
|
30
46
|
* failed run.
|
|
47
|
+
*
|
|
48
|
+
* @param config The endpoint, plus the model whose window is wanted.
|
|
49
|
+
* @param declared The operator's own number. Above zero it wins and the endpoint is not asked.
|
|
31
50
|
*/
|
|
32
51
|
export declare function contextLimitFor(config: Endpoint & {
|
|
33
52
|
model: string;
|
package/dist/client.js
CHANGED
|
@@ -5,7 +5,11 @@ import OpenAI from "openai";
|
|
|
5
5
|
* not — a caller must not let a local endpoint silently borrow the key meant for a paid one.
|
|
6
6
|
*/
|
|
7
7
|
export const NO_KEY = "agent-core";
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Zero, less, or absent means no limit, which the SDK spells as `undefined`.
|
|
10
|
+
*
|
|
11
|
+
* @param config Read for `requestTimeoutSeconds` alone.
|
|
12
|
+
*/
|
|
9
13
|
export const timeoutMs = (config) => {
|
|
10
14
|
const seconds = config.requestTimeoutSeconds ?? 0;
|
|
11
15
|
return seconds > 0 ? seconds * 1000 : undefined;
|
|
@@ -24,6 +28,14 @@ export const timeoutMs = (config) => {
|
|
|
24
28
|
* anything has been produced yet and the SDK does not. See `retry.ts`.
|
|
25
29
|
*/
|
|
26
30
|
const clients = new Map();
|
|
31
|
+
/**
|
|
32
|
+
* The client for an endpoint, built once and kept.
|
|
33
|
+
*
|
|
34
|
+
* Pooled on the three fields that change how a request is sent, so agents sharing a server share
|
|
35
|
+
* a connection and agents on different servers never share a client.
|
|
36
|
+
*
|
|
37
|
+
* @param config Where to send requests and how long to wait. An absent `apiKey` becomes `NO_KEY`.
|
|
38
|
+
*/
|
|
27
39
|
export function getClient(config) {
|
|
28
40
|
const apiKey = config.apiKey || NO_KEY;
|
|
29
41
|
const timeout = timeoutMs(config);
|
|
@@ -71,7 +83,11 @@ function contextLengthOf(model) {
|
|
|
71
83
|
*/
|
|
72
84
|
const listings = new Map();
|
|
73
85
|
const endpointKey = (config) => JSON.stringify([config.baseUrl, config.apiKey || NO_KEY]);
|
|
74
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Asks an endpoint what it serves, and remembers the answer.
|
|
88
|
+
*
|
|
89
|
+
* @param config The endpoint to ask. Remembered per base URL and key, not per model.
|
|
90
|
+
*/
|
|
75
91
|
export async function listModels(config) {
|
|
76
92
|
const { data } = await getClient(config).models.list();
|
|
77
93
|
const models = data
|
|
@@ -92,6 +108,9 @@ export async function listModels(config) {
|
|
|
92
108
|
* model, since a model can arrive after the first listing was taken. A server that will not
|
|
93
109
|
* list models still has to be able to run a turn: a failure here is an unknown window, not a
|
|
94
110
|
* failed run.
|
|
111
|
+
*
|
|
112
|
+
* @param config The endpoint, plus the model whose window is wanted.
|
|
113
|
+
* @param declared The operator's own number. Above zero it wins and the endpoint is not asked.
|
|
95
114
|
*/
|
|
96
115
|
export async function contextLimitFor(config, declared = 0) {
|
|
97
116
|
if (declared > 0)
|
package/dist/errors.d.ts
CHANGED
|
@@ -4,5 +4,7 @@
|
|
|
4
4
|
* Almost everything caught here ends up in a run row, a tool result or a log line, and a
|
|
5
5
|
* `catch` binds `unknown` — so the same three-branch ternary was being written at every site
|
|
6
6
|
* that had to say what happened.
|
|
7
|
+
*
|
|
8
|
+
* @param error Whatever a `catch` bound. Anything that is not an `Error` is stringified.
|
|
7
9
|
*/
|
|
8
10
|
export declare const errorMessage: (error: unknown) => string;
|
package/dist/errors.js
CHANGED
|
@@ -4,5 +4,7 @@
|
|
|
4
4
|
* Almost everything caught here ends up in a run row, a tool result or a log line, and a
|
|
5
5
|
* `catch` binds `unknown` — so the same three-branch ternary was being written at every site
|
|
6
6
|
* that had to say what happened.
|
|
7
|
+
*
|
|
8
|
+
* @param error Whatever a `catch` bound. Anything that is not an `Error` is stringified.
|
|
7
9
|
*/
|
|
8
10
|
export const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
package/dist/events.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* In memory on purpose: it is debugging output, worth nothing once the run has finished and its
|
|
10
10
|
* outcome is in the database. Nothing here survives a restart, and nothing here is the record.
|
|
11
11
|
*/
|
|
12
|
+
/** Which kind of thing happened, and what `text`, `name`, `ok` and `usage` carry for it. */
|
|
12
13
|
export type RunEventKind =
|
|
13
14
|
/** A step of a caller's own flow began. `name` is the step, `text` its kind. */
|
|
14
15
|
"step"
|
|
@@ -40,7 +41,14 @@ export interface RunUsage {
|
|
|
40
41
|
completionTokens: number;
|
|
41
42
|
totalTokens: number;
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* One thing that happened in a run, as a watcher receives it.
|
|
46
|
+
*
|
|
47
|
+
* Every field is always present — the empty ones are `""` or `null` rather than missing — so a
|
|
48
|
+
* client reads it without guarding each key.
|
|
49
|
+
*/
|
|
43
50
|
export interface RunEvent {
|
|
51
|
+
/** The run it belongs to. `emit` fills this in; a caller does not pass it. */
|
|
44
52
|
runId: string;
|
|
45
53
|
/** Per-run counter, from 1. Lets a client order and de-duplicate what it receives. */
|
|
46
54
|
seq: number;
|
|
@@ -81,18 +89,31 @@ export type RunEventInput = Pick<RunEvent, "kind"> & Partial<Omit<RunEvent, "kin
|
|
|
81
89
|
* Forgets a run that will not be emitting `done` — one whose process is tearing down, or whose
|
|
82
90
|
* loop threw where it could not be caught. The sweep gets there on its own; this is for a
|
|
83
91
|
* caller that already knows.
|
|
92
|
+
*
|
|
93
|
+
* @param runId The run to forget. An id nothing was emitted under is ignored.
|
|
84
94
|
*/
|
|
85
95
|
export declare function endRun(runId: string): void;
|
|
86
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Records one event and hands it to everyone watching that run. Never throws at the caller.
|
|
98
|
+
*
|
|
99
|
+
* @param runId The run this belongs to. Created on first use.
|
|
100
|
+
* @param input The event. `kind` is required; `runId` and `seq` are not a caller's to set.
|
|
101
|
+
*/
|
|
87
102
|
export declare function emit(runId: string, input: RunEventInput): RunEvent;
|
|
88
103
|
/**
|
|
89
104
|
* Everything that has happened on a run, then everything that happens next, until it ends.
|
|
90
105
|
*
|
|
91
106
|
* The backlog comes first so a watcher that joins halfway through — or after the run finished,
|
|
92
107
|
* inside the retention window — reads the same story as one that was there from the start.
|
|
108
|
+
*
|
|
109
|
+
* @param runId The run to follow. One that has not started yet is waited on, not refused.
|
|
93
110
|
*/
|
|
94
111
|
export declare function watch(runId: string): AsyncGenerator<RunEvent>;
|
|
95
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The backlog alone, for a caller that wants a snapshot rather than a subscription.
|
|
114
|
+
*
|
|
115
|
+
* @param runId The run to read. An unknown or already-swept run gives an empty array.
|
|
116
|
+
*/
|
|
96
117
|
export declare const history: (runId: string) => RunEvent[];
|
|
97
118
|
/**
|
|
98
119
|
* Test seam: forget every run, so one test's events cannot be read by the next.
|
|
@@ -109,5 +130,7 @@ export declare const resetEvents: () => void;
|
|
|
109
130
|
* reasoning model spends ten thousand deltas on a paragraph, and a paragraph is what it meant.
|
|
110
131
|
* Each block carries the `seq` of its last event, so asking for what came after one block
|
|
111
132
|
* picks up exactly where it left off.
|
|
133
|
+
*
|
|
134
|
+
* @param events Events in `seq` order, from `history` or collected from `watch`.
|
|
112
135
|
*/
|
|
113
136
|
export declare function fold(events: RunEvent[]): RunEvent[];
|
package/dist/events.js
CHANGED
|
@@ -69,11 +69,18 @@ function scheduleSweep() {
|
|
|
69
69
|
* Forgets a run that will not be emitting `done` — one whose process is tearing down, or whose
|
|
70
70
|
* loop threw where it could not be caught. The sweep gets there on its own; this is for a
|
|
71
71
|
* caller that already knows.
|
|
72
|
+
*
|
|
73
|
+
* @param runId The run to forget. An id nothing was emitted under is ignored.
|
|
72
74
|
*/
|
|
73
75
|
export function endRun(runId) {
|
|
74
76
|
streams.delete(runId);
|
|
75
77
|
}
|
|
76
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Records one event and hands it to everyone watching that run. Never throws at the caller.
|
|
80
|
+
*
|
|
81
|
+
* @param runId The run this belongs to. Created on first use.
|
|
82
|
+
* @param input The event. `kind` is required; `runId` and `seq` are not a caller's to set.
|
|
83
|
+
*/
|
|
77
84
|
export function emit(runId, input) {
|
|
78
85
|
const stream = streamFor(runId);
|
|
79
86
|
// One clock read, used for both the event and the sweep's bookkeeping.
|
|
@@ -115,6 +122,8 @@ export function emit(runId, input) {
|
|
|
115
122
|
*
|
|
116
123
|
* The backlog comes first so a watcher that joins halfway through — or after the run finished,
|
|
117
124
|
* inside the retention window — reads the same story as one that was there from the start.
|
|
125
|
+
*
|
|
126
|
+
* @param runId The run to follow. One that has not started yet is waited on, not refused.
|
|
118
127
|
*/
|
|
119
128
|
export async function* watch(runId) {
|
|
120
129
|
const stream = streamFor(runId);
|
|
@@ -131,9 +140,15 @@ export async function* watch(runId) {
|
|
|
131
140
|
// The bus caps its own backlog at `MAX_EVENTS`; without this the watcher downstream of it
|
|
132
141
|
// had no cap at all, so a client too slow to keep up held every delta a run ever emitted.
|
|
133
142
|
// The oldest go, which is what the backlog does, and the gap is reported once below.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
//
|
|
144
|
+
// Dropped here means released here. Advancing the cursor alone left the dropped events in
|
|
145
|
+
// the slots behind it, to be freed by the compaction in the drain below — which a consumer
|
|
146
|
+
// that has stalled does not reach, and a stalled consumer is the whole reason for the cap.
|
|
147
|
+
// It read as capped and held every event anyway: 16MB where the cap promises a third of one.
|
|
148
|
+
const cut = queue.length - head - MAX_EVENTS;
|
|
149
|
+
if (cut > TRIM_SLACK) {
|
|
150
|
+
queue = queue.slice(head + cut);
|
|
151
|
+
head = 0;
|
|
137
152
|
dropped += cut;
|
|
138
153
|
}
|
|
139
154
|
wake?.();
|
|
@@ -157,11 +172,17 @@ export async function* watch(runId) {
|
|
|
157
172
|
if (dropped > 0) {
|
|
158
173
|
// Said once per gap rather than per event, and before the event that follows it, so a
|
|
159
174
|
// client reading `seq` sees why the numbers jump instead of assuming it lost its place.
|
|
175
|
+
//
|
|
176
|
+
// One short of the event it precedes, which is the last seq that went missing. Sharing
|
|
177
|
+
// a seq with the event behind it made the notice indistinguishable from a duplicate,
|
|
178
|
+
// and de-duplicating on `seq` is the one thing the sequence is documented for — so a
|
|
179
|
+
// client doing exactly that dropped either the gap notice or the event it explains.
|
|
180
|
+
// Inside the gap there is nothing to collide with: those seqs reach no watcher.
|
|
160
181
|
const gap = dropped;
|
|
161
182
|
dropped = 0;
|
|
162
183
|
yield {
|
|
163
184
|
runId,
|
|
164
|
-
seq: event.seq,
|
|
185
|
+
seq: event.seq - 1,
|
|
165
186
|
at: event.at,
|
|
166
187
|
kind: "notice",
|
|
167
188
|
text: `${gap} event(s) dropped: this watcher fell too far behind`,
|
|
@@ -193,7 +214,11 @@ export async function* watch(runId) {
|
|
|
193
214
|
}
|
|
194
215
|
}
|
|
195
216
|
}
|
|
196
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* The backlog alone, for a caller that wants a snapshot rather than a subscription.
|
|
219
|
+
*
|
|
220
|
+
* @param runId The run to read. An unknown or already-swept run gives an empty array.
|
|
221
|
+
*/
|
|
197
222
|
export const history = (runId) => [...(streams.get(runId)?.events ?? [])];
|
|
198
223
|
/**
|
|
199
224
|
* Test seam: forget every run, so one test's events cannot be read by the next.
|
|
@@ -215,6 +240,8 @@ export const resetEvents = () => {
|
|
|
215
240
|
* reasoning model spends ten thousand deltas on a paragraph, and a paragraph is what it meant.
|
|
216
241
|
* Each block carries the `seq` of its last event, so asking for what came after one block
|
|
217
242
|
* picks up exactly where it left off.
|
|
243
|
+
*
|
|
244
|
+
* @param events Events in `seq` order, from `history` or collected from `watch`.
|
|
218
245
|
*/
|
|
219
246
|
export function fold(events) {
|
|
220
247
|
const blocks = [];
|
package/dist/retry.d.ts
CHANGED
|
@@ -18,7 +18,11 @@ export declare class EndpointSilent extends Error {
|
|
|
18
18
|
export declare class ContextOverflow extends Error {
|
|
19
19
|
readonly name = "ContextOverflow";
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* 1234 → "1.2k". The numbers in an overflow message are large and nobody reads the units digit.
|
|
23
|
+
*
|
|
24
|
+
* @param tokens The count to render.
|
|
25
|
+
*/
|
|
22
26
|
export declare const compact: (tokens: number) => string;
|
|
23
27
|
/**
|
|
24
28
|
* What this request will cost the window, in tokens, near enough.
|
|
@@ -31,8 +35,18 @@ export declare const compact: (tokens: number) => string;
|
|
|
31
35
|
* about to serialise again to send. What the walk misses is JSON's own punctuation and the keys,
|
|
32
36
|
* which `ENVELOPE` puts back approximately; the difference is a rounding error against an
|
|
33
37
|
* estimate that is already characters over four.
|
|
38
|
+
*
|
|
39
|
+
* @param body The request as it will be sent, tools included.
|
|
34
40
|
*/
|
|
35
41
|
export declare const requestTokens: (body: OpenAI.ChatCompletionCreateParamsStreaming) => number;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a refusal means the request was too big, rather than merely refused.
|
|
44
|
+
*
|
|
45
|
+
* Rate limits are ruled out first: they borrow the same words and mean the opposite, being worth
|
|
46
|
+
* another attempt where an overflow never is.
|
|
47
|
+
*
|
|
48
|
+
* @param detail The endpoint's own message.
|
|
49
|
+
*/
|
|
36
50
|
export declare const isOverflow: (detail: string) => boolean;
|
|
37
51
|
/**
|
|
38
52
|
* The smallest window worth believing in, and the floor under both of its uses.
|
|
@@ -57,8 +71,20 @@ export declare const SMALLEST_LIKELY_WINDOW = 8192;
|
|
|
57
71
|
* to answer, an endpoint that went quiet. A 400 for a malformed tool schema would fail exactly
|
|
58
72
|
* the same way on every attempt, and the two capability cases below are negotiated rather than
|
|
59
73
|
* retried blindly.
|
|
74
|
+
*
|
|
75
|
+
* @param error The rejection, as caught. What is not an SDK error is not transient.
|
|
60
76
|
*/
|
|
61
77
|
export declare function isTransient(error: unknown): boolean;
|
|
62
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Exponential, with jitter so several tasks failing at once do not return in lockstep.
|
|
80
|
+
*
|
|
81
|
+
* @param attempt Zero-based. Doubles from 500ms to a ceiling of eight seconds, before jitter.
|
|
82
|
+
*/
|
|
63
83
|
export declare const backoffMs: (attempt: number) => number;
|
|
84
|
+
/**
|
|
85
|
+
* A delay an abort cuts short, rejecting rather than resolving early.
|
|
86
|
+
*
|
|
87
|
+
* @param ms How long to wait.
|
|
88
|
+
* @param signal Abandons the wait. One already aborted rejects without waiting at all.
|
|
89
|
+
*/
|
|
64
90
|
export declare const sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
package/dist/retry.js
CHANGED
|
@@ -19,7 +19,11 @@ export class EndpointSilent extends Error {
|
|
|
19
19
|
export class ContextOverflow extends Error {
|
|
20
20
|
name = "ContextOverflow";
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* 1234 → "1.2k". The numbers in an overflow message are large and nobody reads the units digit.
|
|
24
|
+
*
|
|
25
|
+
* @param tokens The count to render.
|
|
26
|
+
*/
|
|
23
27
|
export const compact = (tokens) => tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
24
28
|
/** What `{"role":"","content":""},` costs around a message's own text, in characters. */
|
|
25
29
|
const ENVELOPE = 25;
|
|
@@ -84,6 +88,8 @@ function toolsCost(tools) {
|
|
|
84
88
|
* about to serialise again to send. What the walk misses is JSON's own punctuation and the keys,
|
|
85
89
|
* which `ENVELOPE` puts back approximately; the difference is a rounding error against an
|
|
86
90
|
* estimate that is already characters over four.
|
|
91
|
+
*
|
|
92
|
+
* @param body The request as it will be sent, tools included.
|
|
87
93
|
*/
|
|
88
94
|
export const requestTokens = (body) => {
|
|
89
95
|
// Characters first and the division once at the end, rather than a rounded count per message:
|
|
@@ -114,6 +120,14 @@ const OVERFLOW = [
|
|
|
114
120
|
* nothing retries it. The two classifiers in this file disagreed about one error.
|
|
115
121
|
*/
|
|
116
122
|
const RATE_LIMITED = /per (min|hour|day)|rate.?limit|\b[tr]pm\b|quota/i;
|
|
123
|
+
/**
|
|
124
|
+
* Whether a refusal means the request was too big, rather than merely refused.
|
|
125
|
+
*
|
|
126
|
+
* Rate limits are ruled out first: they borrow the same words and mean the opposite, being worth
|
|
127
|
+
* another attempt where an overflow never is.
|
|
128
|
+
*
|
|
129
|
+
* @param detail The endpoint's own message.
|
|
130
|
+
*/
|
|
117
131
|
export const isOverflow = (detail) => !RATE_LIMITED.test(detail) &&
|
|
118
132
|
OVERFLOW.some((pattern) => pattern.test(detail)) &&
|
|
119
133
|
/token|context/i.test(detail);
|
|
@@ -140,6 +154,8 @@ export const SMALLEST_LIKELY_WINDOW = 8192;
|
|
|
140
154
|
* to answer, an endpoint that went quiet. A 400 for a malformed tool schema would fail exactly
|
|
141
155
|
* the same way on every attempt, and the two capability cases below are negotiated rather than
|
|
142
156
|
* retried blindly.
|
|
157
|
+
*
|
|
158
|
+
* @param error The rejection, as caught. What is not an SDK error is not transient.
|
|
143
159
|
*/
|
|
144
160
|
export function isTransient(error) {
|
|
145
161
|
if (error instanceof EndpointSilent)
|
|
@@ -151,8 +167,18 @@ export function isTransient(error) {
|
|
|
151
167
|
const { status } = error;
|
|
152
168
|
return status === 408 || status === 409 || status === 429 || (status ?? 0) >= 500;
|
|
153
169
|
}
|
|
154
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Exponential, with jitter so several tasks failing at once do not return in lockstep.
|
|
172
|
+
*
|
|
173
|
+
* @param attempt Zero-based. Doubles from 500ms to a ceiling of eight seconds, before jitter.
|
|
174
|
+
*/
|
|
155
175
|
export const backoffMs = (attempt) => Math.min(8000, 2 ** attempt * 500) * (0.5 + Math.random() / 2);
|
|
176
|
+
/**
|
|
177
|
+
* A delay an abort cuts short, rejecting rather than resolving early.
|
|
178
|
+
*
|
|
179
|
+
* @param ms How long to wait.
|
|
180
|
+
* @param signal Abandons the wait. One already aborted rejects without waiting at all.
|
|
181
|
+
*/
|
|
156
182
|
export const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
157
183
|
const timer = setTimeout(() => {
|
|
158
184
|
signal?.removeEventListener("abort", onAbort);
|
package/dist/run-turn.d.ts
CHANGED
|
@@ -46,5 +46,10 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
|
|
46
46
|
* the last attempt latched off: the tools it sends depend on `strictSchemas`, and `relaxTools`
|
|
47
47
|
* has to apply to the schemas that were just sanitised. It is handed the same `Capabilities`
|
|
48
48
|
* object throughout, and a caller that reads those from its own closure can ignore the argument.
|
|
49
|
+
*
|
|
50
|
+
* @param client The pooled client for this endpoint.
|
|
51
|
+
* @param supports What the endpoint has already refused, threaded through the negotiation.
|
|
52
|
+
* @param request Builds the body. Called again per attempt, since a downgrade changes it.
|
|
53
|
+
* @param options Retry budget, context limit, notices, and the stream's own callbacks.
|
|
49
54
|
*/
|
|
50
55
|
export declare function runTurn(client: OpenAI, supports: Capabilities, request: (supports: Capabilities) => OpenAI.ChatCompletionCreateParamsStreaming, { maxRetries, onNotice, contextLimit, ...stream }?: RunTurnOptions): Promise<Turn>;
|
package/dist/run-turn.js
CHANGED
|
@@ -7,6 +7,11 @@ import { streamTurn } from "./stream.js";
|
|
|
7
7
|
* the last attempt latched off: the tools it sends depend on `strictSchemas`, and `relaxTools`
|
|
8
8
|
* has to apply to the schemas that were just sanitised. It is handed the same `Capabilities`
|
|
9
9
|
* object throughout, and a caller that reads those from its own closure can ignore the argument.
|
|
10
|
+
*
|
|
11
|
+
* @param client The pooled client for this endpoint.
|
|
12
|
+
* @param supports What the endpoint has already refused, threaded through the negotiation.
|
|
13
|
+
* @param request Builds the body. Called again per attempt, since a downgrade changes it.
|
|
14
|
+
* @param options Retry budget, context limit, notices, and the stream's own callbacks.
|
|
10
15
|
*/
|
|
11
16
|
export async function runTurn(client, supports, request, { maxRetries = 0, onNotice, contextLimit = 0, ...stream } = {}) {
|
|
12
17
|
// Sized once rather than per build. `request` is called again for every downgrade and every
|
package/dist/schema-compat.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type OpenAI from "openai";
|
|
2
|
+
/**
|
|
3
|
+
* Tool definitions a strict server will accept, remembered per definition object.
|
|
4
|
+
*
|
|
5
|
+
* The first call on a connection's tools does the work and every later one is a lookup, so
|
|
6
|
+
* calling this per request costs nothing.
|
|
7
|
+
*
|
|
8
|
+
* @param tools The definitions as the pool hands them over. Never mutated — where a schema
|
|
9
|
+
* changed, a new definition is returned in its place.
|
|
10
|
+
*/
|
|
2
11
|
export declare const sanitizeTools: (tools: OpenAI.ChatCompletionTool[]) => OpenAI.ChatCompletionTool[];
|
|
3
12
|
/**
|
|
4
13
|
* The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
|
|
5
14
|
* `pattern` and most `format` values, both of which only ever narrowed a string the tool
|
|
6
15
|
* re-validates anyway.
|
|
16
|
+
*
|
|
17
|
+
* @param tools Already sanitised. Relaxing is the retry, not a substitute for `sanitizeTools`.
|
|
7
18
|
*/
|
|
8
19
|
export declare const relaxTools: (tools: OpenAI.ChatCompletionTool[]) => OpenAI.ChatCompletionTool[];
|
|
9
20
|
/**
|
|
@@ -13,5 +24,7 @@ export declare const relaxTools: (tools: OpenAI.ChatCompletionTool[]) => OpenAI.
|
|
|
13
24
|
* says "Failed to initialize samplers: failed to parse grammar", others surface the converter
|
|
14
25
|
* by name. Since a grammar is only ever involved in constrained decoding, treat any mention of
|
|
15
26
|
* one as ours; the retry is cheap and latches after a single request.
|
|
27
|
+
*
|
|
28
|
+
* @param message The server's error text. Matched case-insensitively.
|
|
16
29
|
*/
|
|
17
30
|
export declare function isGrammarError(message: string): boolean;
|
package/dist/schema-compat.js
CHANGED
|
@@ -162,6 +162,50 @@ function mergeRootAllOf(out) {
|
|
|
162
162
|
if (required.size)
|
|
163
163
|
out.required = [...required];
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Folds a root `anyOf` or `oneOf` into the root itself.
|
|
167
|
+
*
|
|
168
|
+
* The third spelling of "the arguments are this named type", after `$ref` and `allOf`, and the
|
|
169
|
+
* one still going out empty: a union of real object shapes has no `null` branch for
|
|
170
|
+
* `collapseNullableUnion` to take apart, so it reached the delete below intact and every
|
|
171
|
+
* argument went with it. Properties are unioned because a caller satisfies any one branch;
|
|
172
|
+
* `required` keeps only the names every branch asks for, since one that a branch does without
|
|
173
|
+
* is one the model has to be free to omit. A branch that is a reference is not something to
|
|
174
|
+
* guess at — it cannot vouch for a name, so its presence alone empties `required`.
|
|
175
|
+
*/
|
|
176
|
+
function mergeRootUnion(out) {
|
|
177
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
178
|
+
const branches = out[key];
|
|
179
|
+
if (!Array.isArray(branches))
|
|
180
|
+
continue;
|
|
181
|
+
const properties = {};
|
|
182
|
+
// `null` until a branch has been read, which is what tells "no branches yet" apart from
|
|
183
|
+
// "the branches agreed on nothing".
|
|
184
|
+
let shared = null;
|
|
185
|
+
for (const branch of branches) {
|
|
186
|
+
const previous = shared;
|
|
187
|
+
if (!isObject(branch) || "$ref" in branch) {
|
|
188
|
+
shared = new Set();
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (isObject(branch.properties))
|
|
192
|
+
Object.assign(properties, branch.properties);
|
|
193
|
+
const names = Array.isArray(branch.required)
|
|
194
|
+
? branch.required.filter((name) => typeof name === "string")
|
|
195
|
+
: [];
|
|
196
|
+
shared = previous === null ? new Set(names) : new Set(names.filter((n) => previous.has(n)));
|
|
197
|
+
}
|
|
198
|
+
if (!Object.keys(properties).length)
|
|
199
|
+
continue;
|
|
200
|
+
out.properties = { ...(isObject(out.properties) ? out.properties : {}), ...properties };
|
|
201
|
+
if (shared?.size) {
|
|
202
|
+
const already = Array.isArray(out.required)
|
|
203
|
+
? out.required.filter((name) => typeof name === "string")
|
|
204
|
+
: [];
|
|
205
|
+
out.required = [...new Set([...already, ...shared])];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
165
209
|
/**
|
|
166
210
|
* A required argument that is not in `properties` is one no caller can supply and no strict
|
|
167
211
|
* validator will accept. Anything the rewrites above removed, `required` may still name.
|
|
@@ -181,6 +225,7 @@ function sanitizeParameters(parameters) {
|
|
|
181
225
|
return EMPTY_OBJECT();
|
|
182
226
|
const out = normalize(inlineRootRef(parameters));
|
|
183
227
|
mergeRootAllOf(out);
|
|
228
|
+
mergeRootUnion(out);
|
|
184
229
|
for (const key of TOP_LEVEL_COMBINATORS)
|
|
185
230
|
delete out[key];
|
|
186
231
|
if (out.type !== "object")
|
|
@@ -227,6 +272,15 @@ const through = (cache, tools, fn) => tools.map((tool) => {
|
|
|
227
272
|
cache.set(tool, built);
|
|
228
273
|
return built;
|
|
229
274
|
});
|
|
275
|
+
/**
|
|
276
|
+
* Tool definitions a strict server will accept, remembered per definition object.
|
|
277
|
+
*
|
|
278
|
+
* The first call on a connection's tools does the work and every later one is a lookup, so
|
|
279
|
+
* calling this per request costs nothing.
|
|
280
|
+
*
|
|
281
|
+
* @param tools The definitions as the pool hands them over. Never mutated — where a schema
|
|
282
|
+
* changed, a new definition is returned in its place.
|
|
283
|
+
*/
|
|
230
284
|
export const sanitizeTools = (tools) => through(sanitized, tools, sanitizeParameters);
|
|
231
285
|
/**
|
|
232
286
|
* Walked as a schema rather than as arbitrary JSON, because `pattern` and `format` are keyword
|
|
@@ -262,6 +316,8 @@ const strip = (node) => {
|
|
|
262
316
|
* The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
|
|
263
317
|
* `pattern` and most `format` values, both of which only ever narrowed a string the tool
|
|
264
318
|
* re-validates anyway.
|
|
319
|
+
*
|
|
320
|
+
* @param tools Already sanitised. Relaxing is the retry, not a substitute for `sanitizeTools`.
|
|
265
321
|
*/
|
|
266
322
|
export const relaxTools = (tools) => through(relaxed, tools, (parameters) => {
|
|
267
323
|
const stripped = strip(parameters);
|
|
@@ -280,6 +336,8 @@ const NO_USER_QUERY = "no user query found";
|
|
|
280
336
|
* says "Failed to initialize samplers: failed to parse grammar", others surface the converter
|
|
281
337
|
* by name. Since a grammar is only ever involved in constrained decoding, treat any mention of
|
|
282
338
|
* one as ours; the retry is cheap and latches after a single request.
|
|
339
|
+
*
|
|
340
|
+
* @param message The server's error text. Matched case-insensitively.
|
|
283
341
|
*/
|
|
284
342
|
export function isGrammarError(message) {
|
|
285
343
|
const text = message.toLowerCase();
|
package/dist/side-task.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { Endpoint } from "./config.ts";
|
|
2
|
-
/** Test seam, alongside `resetClients` and `
|
|
2
|
+
/** Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints. */
|
|
3
3
|
export declare const resetHints: () => void;
|
|
4
|
+
/** What a side task may be given. All optional — one given none of them still runs. */
|
|
4
5
|
export interface SideTaskOptions {
|
|
6
|
+
/** Ceiling on the reply, default 512. These answers are meant to be short. */
|
|
5
7
|
maxTokens?: number;
|
|
8
|
+
/** Sampling temperature, default 0.3. Naming and classifying want the same answer twice. */
|
|
6
9
|
temperature?: number;
|
|
10
|
+
/** Abandons the call, usually because the run it supports has gone away. */
|
|
7
11
|
signal?: AbortSignal;
|
|
8
12
|
/**
|
|
9
13
|
* Told what was given up on, the same way `runTurn` and `negotiate` tell a caller.
|
|
@@ -14,23 +18,45 @@ export interface SideTaskOptions {
|
|
|
14
18
|
*/
|
|
15
19
|
onNotice?: (message: string) => void;
|
|
16
20
|
}
|
|
17
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Runs a side task and returns the reply text, thinking stripped. Throws like any request.
|
|
23
|
+
*
|
|
24
|
+
* @param config Where to send it and how long to wait.
|
|
25
|
+
* @param model The model to ask, usually smaller than the one running the work.
|
|
26
|
+
* @param system The instruction.
|
|
27
|
+
* @param user The input it applies to.
|
|
28
|
+
* @param options Reply ceiling, temperature, cancellation, notices.
|
|
29
|
+
*/
|
|
18
30
|
export declare function ask(config: Endpoint, model: string, system: string, user: string, { maxTokens, temperature, signal, onNotice }?: SideTaskOptions): Promise<string>;
|
|
19
31
|
/**
|
|
20
32
|
* A side task is never worth failing the work it supports. Callers that can carry on without
|
|
21
33
|
* an answer use this and get `undefined` instead of an exception.
|
|
34
|
+
*
|
|
35
|
+
* @param label Names the task in the notice when it fails.
|
|
36
|
+
* @param run The call to attempt. Anything it throws becomes `undefined`, an abort excepted.
|
|
37
|
+
* @param options `onNotice`, told what was given up on.
|
|
22
38
|
*/
|
|
23
39
|
export declare function tryAsk<T>(label: string, run: () => Promise<T>, { onNotice }?: Pick<SideTaskOptions, "onNotice">): Promise<T | undefined>;
|
|
24
40
|
/**
|
|
25
41
|
* Models are asked for JSON and often answer with prose around it, or a fenced block. Pull out
|
|
26
42
|
* the first array or object rather than failing the task over a wrapper.
|
|
43
|
+
*
|
|
44
|
+
* @param text The reply, fences and prose included. Nothing parseable gives `undefined`.
|
|
27
45
|
*/
|
|
28
46
|
export declare function parseJson<T>(text: string): T | undefined;
|
|
29
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Strips the quoting and list punctuation models decorate short answers with.
|
|
49
|
+
*
|
|
50
|
+
* @param line One line of a reply.
|
|
51
|
+
*/
|
|
30
52
|
export declare const clean: (line: string) => string;
|
|
31
53
|
/**
|
|
32
54
|
* A list-shaped reply, one item per line, cleaned of the bullets and quotes models decorate
|
|
33
55
|
* them with. Overlong items are dropped rather than truncated — a suggestion that has to be
|
|
34
56
|
* squinted at is worse than one fewer suggestion.
|
|
57
|
+
*
|
|
58
|
+
* @param text The reply, one item per line.
|
|
59
|
+
* @param max How many items to keep.
|
|
60
|
+
* @param maxChars Longest item kept. Longer ones are dropped, not truncated.
|
|
35
61
|
*/
|
|
36
62
|
export declare const listLines: (text: string, max: number, maxChars: number) => string[];
|
package/dist/side-task.js
CHANGED
|
@@ -33,7 +33,7 @@ const NO_THINKING = {
|
|
|
33
33
|
*/
|
|
34
34
|
const noHints = new Set();
|
|
35
35
|
const hintKey = (baseUrl, model) => JSON.stringify([baseUrl, model]);
|
|
36
|
-
/** Test seam, alongside `resetClients` and `
|
|
36
|
+
/** Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints. */
|
|
37
37
|
export const resetHints = () => noHints.clear();
|
|
38
38
|
/**
|
|
39
39
|
* Whether a failure is the server complaining about the request, rather than failing to answer.
|
|
@@ -62,7 +62,15 @@ function rejectedTheRequest(error) {
|
|
|
62
62
|
* arrives — and the whole deliberation was then returned to the caller as the answer.
|
|
63
63
|
*/
|
|
64
64
|
const stripThinking = (text) => text.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<think>[\s\S]*$/i, "");
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Runs a side task and returns the reply text, thinking stripped. Throws like any request.
|
|
67
|
+
*
|
|
68
|
+
* @param config Where to send it and how long to wait.
|
|
69
|
+
* @param model The model to ask, usually smaller than the one running the work.
|
|
70
|
+
* @param system The instruction.
|
|
71
|
+
* @param user The input it applies to.
|
|
72
|
+
* @param options Reply ceiling, temperature, cancellation, notices.
|
|
73
|
+
*/
|
|
66
74
|
export async function ask(config, model, system, user, { maxTokens = 512, temperature = 0.3, signal, onNotice } = {}) {
|
|
67
75
|
const send = (hints) => getClient(config).chat.completions.create({
|
|
68
76
|
model,
|
|
@@ -99,6 +107,10 @@ export async function ask(config, model, system, user, { maxTokens = 512, temper
|
|
|
99
107
|
/**
|
|
100
108
|
* A side task is never worth failing the work it supports. Callers that can carry on without
|
|
101
109
|
* an answer use this and get `undefined` instead of an exception.
|
|
110
|
+
*
|
|
111
|
+
* @param label Names the task in the notice when it fails.
|
|
112
|
+
* @param run The call to attempt. Anything it throws becomes `undefined`, an abort excepted.
|
|
113
|
+
* @param options `onNotice`, told what was given up on.
|
|
102
114
|
*/
|
|
103
115
|
export async function tryAsk(label, run, { onNotice } = {}) {
|
|
104
116
|
try {
|
|
@@ -116,6 +128,8 @@ export async function tryAsk(label, run, { onNotice } = {}) {
|
|
|
116
128
|
/**
|
|
117
129
|
* Models are asked for JSON and often answer with prose around it, or a fenced block. Pull out
|
|
118
130
|
* the first array or object rather than failing the task over a wrapper.
|
|
131
|
+
*
|
|
132
|
+
* @param text The reply, fences and prose included. Nothing parseable gives `undefined`.
|
|
119
133
|
*/
|
|
120
134
|
export function parseJson(text) {
|
|
121
135
|
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
@@ -133,7 +147,11 @@ export function parseJson(text) {
|
|
|
133
147
|
return undefined;
|
|
134
148
|
}
|
|
135
149
|
}
|
|
136
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Strips the quoting and list punctuation models decorate short answers with.
|
|
152
|
+
*
|
|
153
|
+
* @param line One line of a reply.
|
|
154
|
+
*/
|
|
137
155
|
export const clean = (line) => line
|
|
138
156
|
.trim()
|
|
139
157
|
.replace(/^(?:[-*•]|\d+[.)])\s*/, "")
|
|
@@ -143,6 +161,10 @@ export const clean = (line) => line
|
|
|
143
161
|
* A list-shaped reply, one item per line, cleaned of the bullets and quotes models decorate
|
|
144
162
|
* them with. Overlong items are dropped rather than truncated — a suggestion that has to be
|
|
145
163
|
* squinted at is worse than one fewer suggestion.
|
|
164
|
+
*
|
|
165
|
+
* @param text The reply, one item per line.
|
|
166
|
+
* @param max How many items to keep.
|
|
167
|
+
* @param maxChars Longest item kept. Longer ones are dropped, not truncated.
|
|
146
168
|
*/
|
|
147
169
|
export const listLines = (text, max, maxChars) => text
|
|
148
170
|
.split("\n")
|
package/dist/stream.d.ts
CHANGED
|
@@ -33,7 +33,9 @@ export interface Turn {
|
|
|
33
33
|
export interface Produced {
|
|
34
34
|
any: boolean;
|
|
35
35
|
}
|
|
36
|
+
/** What `streamTurn` takes besides the request body. */
|
|
36
37
|
export interface StreamTurnOptions {
|
|
38
|
+
/** Cancels the request and the stream being read from it. */
|
|
37
39
|
signal?: AbortSignal;
|
|
38
40
|
/**
|
|
39
41
|
* Silence allowed before the request is given up on. Zero or undefined waits forever, which
|
|
@@ -59,5 +61,9 @@ export interface StreamTurnOptions {
|
|
|
59
61
|
* Two token callbacks rather than an event input, because a turn does not know which step of
|
|
60
62
|
* which run it is: `step` is the caller's flow concept, and wrapping these into an `emit` is one
|
|
61
63
|
* line at the call site.
|
|
64
|
+
*
|
|
65
|
+
* @param client The pooled client for this endpoint.
|
|
66
|
+
* @param body The request, which must set `stream: true`.
|
|
67
|
+
* @param options Cancellation, the idle watchdog, and the token callbacks.
|
|
62
68
|
*/
|
|
63
69
|
export declare function streamTurn(client: OpenAI, body: OpenAI.ChatCompletionCreateParamsStreaming, { signal, idleMs, produced, onThinking, onOutput }?: StreamTurnOptions): Promise<Turn>;
|
package/dist/stream.js
CHANGED
|
@@ -10,6 +10,10 @@ import { EndpointSilent } from "./retry.js";
|
|
|
10
10
|
* Two token callbacks rather than an event input, because a turn does not know which step of
|
|
11
11
|
* which run it is: `step` is the caller's flow concept, and wrapping these into an `emit` is one
|
|
12
12
|
* line at the call site.
|
|
13
|
+
*
|
|
14
|
+
* @param client The pooled client for this endpoint.
|
|
15
|
+
* @param body The request, which must set `stream: true`.
|
|
16
|
+
* @param options Cancellation, the idle watchdog, and the token callbacks.
|
|
13
17
|
*/
|
|
14
18
|
export async function streamTurn(client, body, { signal, idleMs, produced, onThinking, onOutput } = {}) {
|
|
15
19
|
// Silence, not duration: the timer is rearmed on every chunk, so a model that is still
|
package/dist/tokens.d.ts
CHANGED
|
@@ -12,5 +12,7 @@
|
|
|
12
12
|
* owns it should not be one that also does something. It was extracted from `side-task` to
|
|
13
13
|
* break a cycle with `retry`; `side-task` no longer reads it, but `retry` and any consumer
|
|
14
14
|
* sizing its own prompt still do, and a leaf with no imports is the right home for it.
|
|
15
|
+
*
|
|
16
|
+
* @param text Prose or serialised JSON — both counted the same way, which is why JSON reads low.
|
|
15
17
|
*/
|
|
16
18
|
export declare const estimateTokens: (text: string) => number;
|
package/dist/tokens.js
CHANGED
|
@@ -12,5 +12,7 @@
|
|
|
12
12
|
* owns it should not be one that also does something. It was extracted from `side-task` to
|
|
13
13
|
* break a cycle with `retry`; `side-task` no longer reads it, but `retry` and any consumer
|
|
14
14
|
* sizing its own prompt still do, and a leaf with no imports is the right home for it.
|
|
15
|
+
*
|
|
16
|
+
* @param text Prose or serialised JSON — both counted the same way, which is why JSON reads low.
|
|
15
17
|
*/
|
|
16
18
|
export const estimateTokens = (text) => Math.ceil(text.length / 4);
|
package/dist/tool-loading.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export declare const LOAD_TOOLS_DEFINITION: OpenAI.ChatCompletionTool;
|
|
|
28
28
|
* A server with no tools is dropped rather than titled: a pool hands one over whenever a
|
|
29
29
|
* server is connected but has nothing to offer, and a label with nothing under it reads as a
|
|
30
30
|
* listing that got cut off.
|
|
31
|
+
*
|
|
32
|
+
* @param catalog The connected servers. Ones with no tools are dropped.
|
|
33
|
+
* @param loaded Names already loaded, marked in the listing rather than removed from it.
|
|
31
34
|
*/
|
|
32
35
|
export declare function catalogList(catalog: CatalogServer[], loaded?: ReadonlySet<string>): string;
|
|
33
36
|
/**
|
|
@@ -37,6 +40,9 @@ export declare function catalogList(catalog: CatalogServer[], loaded?: ReadonlyS
|
|
|
37
40
|
* moment it was loaded, and the model loads again to get it back; hoisting them into a separate
|
|
38
41
|
* "already loaded" section splits a server's tools apart, and the model picks a sibling from
|
|
39
42
|
* the longer list instead.
|
|
43
|
+
*
|
|
44
|
+
* @param catalog The connected servers. A catalogue with no tools in it produces an empty string.
|
|
45
|
+
* @param loaded Names already loaded, marked in the listing.
|
|
40
46
|
*/
|
|
41
47
|
export declare function catalogPrompt(catalog: CatalogServer[], loaded?: ReadonlySet<string>): string;
|
|
42
48
|
/**
|
|
@@ -56,7 +62,12 @@ export declare const MAX_PER_LOAD = 12;
|
|
|
56
62
|
* carry. See `carryOver`.
|
|
57
63
|
*/
|
|
58
64
|
export declare const MAX_CARRIED = 16;
|
|
59
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* The tools to start the next turn with: recently used, newest last, capped.
|
|
67
|
+
*
|
|
68
|
+
* @param previous Last turn's names, oldest first.
|
|
69
|
+
* @param used What this turn called. Moved to the end, so the oldest unused fall off.
|
|
70
|
+
*/
|
|
60
71
|
export declare const carryOver: (previous: string[], used: Set<string>) => string[];
|
|
61
72
|
/**
|
|
62
73
|
* Resolves requested names against the catalogue, expanding trailing `*` wildcards.
|
|
@@ -66,6 +77,9 @@ export declare const carryOver: (previous: string[], used: Set<string>) => strin
|
|
|
66
77
|
* on the `__` boundary — accepted only when it is unambiguous. Rejecting those outright just
|
|
67
78
|
* buys a wasted round trip while the model guesses the prefix, and pushes it toward
|
|
68
79
|
* shotgunning wildcards.
|
|
80
|
+
*
|
|
81
|
+
* @param requested What the model asked for. A trailing `*` expands.
|
|
82
|
+
* @param catalog The servers to resolve against.
|
|
69
83
|
*/
|
|
70
84
|
export declare function expandNames(requested: string[], catalog: CatalogServer[]): {
|
|
71
85
|
matched: string[];
|
|
@@ -75,10 +89,25 @@ export declare function expandNames(requested: string[], catalog: CatalogServer[
|
|
|
75
89
|
hits: string[];
|
|
76
90
|
}[];
|
|
77
91
|
};
|
|
78
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* What `load_tools` reports back: the descriptions, now that they are worth their tokens.
|
|
94
|
+
*
|
|
95
|
+
* @param expanded What `expandNames` resolved: the matches, the misses, and the over-broad asks.
|
|
96
|
+
* @param catalog The servers, read for the descriptions now worth their tokens.
|
|
97
|
+
*/
|
|
79
98
|
export declare function loadResult({ matched, unknown, overBroad }: ReturnType<typeof expandNames>, catalog: CatalogServer[]): string;
|
|
99
|
+
/**
|
|
100
|
+
* Whether the catalogue holds a tool by this name.
|
|
101
|
+
*
|
|
102
|
+
* @param catalog The connected servers and the tools each one offers.
|
|
103
|
+
* @param name An exact name. Nothing is prefixed, trimmed or fuzzily matched.
|
|
104
|
+
*/
|
|
80
105
|
export declare const inCatalog: (catalog: CatalogServer[], name: string) => boolean;
|
|
81
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* `load_tools` arguments, defensively — a model may send a bare string or a nested object.
|
|
108
|
+
*
|
|
109
|
+
* @param args The tool call's arguments, exactly as the model sent them.
|
|
110
|
+
*/
|
|
82
111
|
export declare function requestedNames(args: Record<string, unknown>): string[];
|
|
83
112
|
/**
|
|
84
113
|
* Tool preselection.
|
|
@@ -92,6 +121,19 @@ export declare function requestedNames(args: Record<string, unknown>): string[];
|
|
|
92
121
|
* broad guess is not, so the same `MAX_PER_LOAD` cap applies here as to a `load_tools` call.
|
|
93
122
|
*/
|
|
94
123
|
export declare const PRESELECT_SYSTEM: string;
|
|
124
|
+
/**
|
|
125
|
+
* The user message for a preselection call: the catalogue, then the request.
|
|
126
|
+
*
|
|
127
|
+
* @param catalog The connected servers, rendered as the name-only listing.
|
|
128
|
+
* @param prompt The request being planned for, truncated at 2000 characters — choosing tools
|
|
129
|
+
* needs the shape of the ask, not all of it.
|
|
130
|
+
*/
|
|
95
131
|
export declare const preselectInput: (catalog: CatalogServer[], prompt: string) => string;
|
|
96
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Resolves a preselection against the catalogue: unknown names dropped, count capped.
|
|
134
|
+
*
|
|
135
|
+
* @param names What the preselector replied. Unvalidated: a non-array gives none, and entries
|
|
136
|
+
* that are not strings are dropped.
|
|
137
|
+
* @param catalog The servers to resolve against.
|
|
138
|
+
*/
|
|
97
139
|
export declare function preselection(names: unknown, catalog: CatalogServer[]): string[];
|
package/dist/tool-loading.js
CHANGED
|
@@ -54,6 +54,9 @@ export const LOAD_TOOLS_DEFINITION = deepFreeze({
|
|
|
54
54
|
* A server with no tools is dropped rather than titled: a pool hands one over whenever a
|
|
55
55
|
* server is connected but has nothing to offer, and a label with nothing under it reads as a
|
|
56
56
|
* listing that got cut off.
|
|
57
|
+
*
|
|
58
|
+
* @param catalog The connected servers. Ones with no tools are dropped.
|
|
59
|
+
* @param loaded Names already loaded, marked in the listing rather than removed from it.
|
|
57
60
|
*/
|
|
58
61
|
export function catalogList(catalog, loaded) {
|
|
59
62
|
return catalog
|
|
@@ -71,6 +74,9 @@ export function catalogList(catalog, loaded) {
|
|
|
71
74
|
* moment it was loaded, and the model loads again to get it back; hoisting them into a separate
|
|
72
75
|
* "already loaded" section splits a server's tools apart, and the model picks a sibling from
|
|
73
76
|
* the longer list instead.
|
|
77
|
+
*
|
|
78
|
+
* @param catalog The connected servers. A catalogue with no tools in it produces an empty string.
|
|
79
|
+
* @param loaded Names already loaded, marked in the listing.
|
|
74
80
|
*/
|
|
75
81
|
export function catalogPrompt(catalog, loaded) {
|
|
76
82
|
const list = catalogList(catalog, loaded);
|
|
@@ -107,7 +113,12 @@ export const MAX_PER_LOAD = 12;
|
|
|
107
113
|
* carry. See `carryOver`.
|
|
108
114
|
*/
|
|
109
115
|
export const MAX_CARRIED = 16;
|
|
110
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* The tools to start the next turn with: recently used, newest last, capped.
|
|
118
|
+
*
|
|
119
|
+
* @param previous Last turn's names, oldest first.
|
|
120
|
+
* @param used What this turn called. Moved to the end, so the oldest unused fall off.
|
|
121
|
+
*/
|
|
111
122
|
export const carryOver = (previous, used) => [...previous.filter((name) => !used.has(name)), ...used].slice(-MAX_CARRIED);
|
|
112
123
|
/**
|
|
113
124
|
* Resolves requested names against the catalogue, expanding trailing `*` wildcards.
|
|
@@ -117,6 +128,9 @@ export const carryOver = (previous, used) => [...previous.filter((name) => !used
|
|
|
117
128
|
* on the `__` boundary — accepted only when it is unambiguous. Rejecting those outright just
|
|
118
129
|
* buys a wasted round trip while the model guesses the prefix, and pushes it toward
|
|
119
130
|
* shotgunning wildcards.
|
|
131
|
+
*
|
|
132
|
+
* @param requested What the model asked for. A trailing `*` expands.
|
|
133
|
+
* @param catalog The servers to resolve against.
|
|
120
134
|
*/
|
|
121
135
|
export function expandNames(requested, catalog) {
|
|
122
136
|
const all = flatten(catalog);
|
|
@@ -166,7 +180,12 @@ export function expandNames(requested, catalog) {
|
|
|
166
180
|
}
|
|
167
181
|
return { matched: [...matched], unknown, overBroad };
|
|
168
182
|
}
|
|
169
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* What `load_tools` reports back: the descriptions, now that they are worth their tokens.
|
|
185
|
+
*
|
|
186
|
+
* @param expanded What `expandNames` resolved: the matches, the misses, and the over-broad asks.
|
|
187
|
+
* @param catalog The servers, read for the descriptions now worth their tokens.
|
|
188
|
+
*/
|
|
170
189
|
export function loadResult({ matched, unknown, overBroad }, catalog) {
|
|
171
190
|
const byName = new Map(flatten(catalog).map((tool) => [tool.name, tool.description]));
|
|
172
191
|
const lines = [];
|
|
@@ -187,8 +206,18 @@ export function loadResult({ matched, unknown, overBroad }, catalog) {
|
|
|
187
206
|
}
|
|
188
207
|
return lines.join("\n") || "No tool names were given.";
|
|
189
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Whether the catalogue holds a tool by this name.
|
|
211
|
+
*
|
|
212
|
+
* @param catalog The connected servers and the tools each one offers.
|
|
213
|
+
* @param name An exact name. Nothing is prefixed, trimmed or fuzzily matched.
|
|
214
|
+
*/
|
|
190
215
|
export const inCatalog = (catalog, name) => catalog.some((server) => server.tools.some((tool) => tool.name === name));
|
|
191
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* `load_tools` arguments, defensively — a model may send a bare string or a nested object.
|
|
218
|
+
*
|
|
219
|
+
* @param args The tool call's arguments, exactly as the model sent them.
|
|
220
|
+
*/
|
|
192
221
|
export function requestedNames(args) {
|
|
193
222
|
const value = args.names ?? args.tools ?? args.name;
|
|
194
223
|
if (typeof value === "string")
|
|
@@ -212,8 +241,21 @@ export const PRESELECT_SYSTEM = "You choose tools. Below is a catalogue of tool
|
|
|
212
241
|
"array of the names the request is likely to need — exact names from the catalogue, at most " +
|
|
213
242
|
`${MAX_PER_LOAD}, and as few as could do the job. Reply with \`[]\` if the request can be ` +
|
|
214
243
|
"answered without tools. Reply with the array alone — no prose, no explanation.";
|
|
244
|
+
/**
|
|
245
|
+
* The user message for a preselection call: the catalogue, then the request.
|
|
246
|
+
*
|
|
247
|
+
* @param catalog The connected servers, rendered as the name-only listing.
|
|
248
|
+
* @param prompt The request being planned for, truncated at 2000 characters — choosing tools
|
|
249
|
+
* needs the shape of the ask, not all of it.
|
|
250
|
+
*/
|
|
215
251
|
export const preselectInput = (catalog, prompt) => `# Tool catalogue\n\n${catalogList(catalog)}\n\n# Request\n\n${prompt.slice(0, 2000)}`;
|
|
216
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* Resolves a preselection against the catalogue: unknown names dropped, count capped.
|
|
254
|
+
*
|
|
255
|
+
* @param names What the preselector replied. Unvalidated: a non-array gives none, and entries
|
|
256
|
+
* that are not strings are dropped.
|
|
257
|
+
* @param catalog The servers to resolve against.
|
|
258
|
+
*/
|
|
217
259
|
export function preselection(names, catalog) {
|
|
218
260
|
if (!Array.isArray(names))
|
|
219
261
|
return [];
|
package/llms.txt
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# @cubicecho/agent-core
|
|
2
|
+
|
|
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
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
Requires Node >=22, with `openai` >=6 as a peer dependency. ESM only.
|
|
8
|
+
Full prose, worked examples and the reasoning behind each seam are in README.md; this file is the index.
|
|
9
|
+
|
|
10
|
+
## Exports
|
|
11
|
+
|
|
12
|
+
### capabilities
|
|
13
|
+
|
|
14
|
+
What an endpoint turned out not to support, and answering it when it says so.
|
|
15
|
+
|
|
16
|
+
- `Capabilities` (type) — What one endpoint turned out not to support.
|
|
17
|
+
- `capabilitiesFor` — What this endpoint is known not to support.
|
|
18
|
+
- `NegotiateOptions` (type) — What `negotiate` takes besides the request.
|
|
19
|
+
- `negotiate` — Sends a request, re-sending it each time the answer is this endpoint refusing something the request can do without.
|
|
20
|
+
- `resetCapabilities` — Forgets every endpoint's capabilities.
|
|
21
|
+
|
|
22
|
+
### catalog
|
|
23
|
+
|
|
24
|
+
The contract between whatever holds the tools and the loop that offers them to a model.
|
|
25
|
+
|
|
26
|
+
- `CatalogServer` (type) — One server's tools, without their JSON schemas — the cheap half of a tool definition.
|
|
27
|
+
|
|
28
|
+
### client
|
|
29
|
+
|
|
30
|
+
- `contextLimitFor` — How much a model will read, in tokens.
|
|
31
|
+
- `getClient` — The client for an endpoint, built once and kept.
|
|
32
|
+
- `listModels` — Asks an endpoint what it serves, and remembers the answer.
|
|
33
|
+
- `ModelInfo` (type) — A model an endpoint offers, and what it says the model will read.
|
|
34
|
+
- `NO_KEY` — The SDK insists on a non-empty key even where the server will not look at it.
|
|
35
|
+
- `resetClients` — Forgets every cached client and listing.
|
|
36
|
+
- `timeoutMs` — Zero, less, or absent means no limit, which the SDK spells as `undefined`.
|
|
37
|
+
|
|
38
|
+
### config
|
|
39
|
+
|
|
40
|
+
What this package needs to know about a caller's configuration.
|
|
41
|
+
|
|
42
|
+
- `AgentConfig` (type) — A whole agent configuration — every part, plus the two fields that belong to no group.
|
|
43
|
+
- `Endpoint` (type) — Where to send a request and how long to wait.
|
|
44
|
+
- `ModelParams` (type) — What to ask the model for.
|
|
45
|
+
- `RetryPolicy` (type) — How many times a lost or refused request is worth sending again.
|
|
46
|
+
- `ToolPolicy` (type) — How tools reach the model, and how long it may keep calling them.
|
|
47
|
+
|
|
48
|
+
### errors
|
|
49
|
+
|
|
50
|
+
- `errorMessage` — What went wrong, as a string.
|
|
51
|
+
|
|
52
|
+
### events
|
|
53
|
+
|
|
54
|
+
What a run is doing, while it is doing it.
|
|
55
|
+
|
|
56
|
+
- `emit` — Records one event and hands it to everyone watching that run.
|
|
57
|
+
- `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.
|
|
58
|
+
- `fold` — Consecutive tokens of one kind are one thing being said, not hundreds of things.
|
|
59
|
+
- `history` — The backlog alone, for a caller that wants a snapshot rather than a subscription.
|
|
60
|
+
- `RunEvent` (type) — One thing that happened in a run, as a watcher receives it.
|
|
61
|
+
- `RunEventInput` (type) — What `emit` is given: the run and the sequence are the bus's to assign.
|
|
62
|
+
- `RunEventKind` (type) — Which kind of thing happened, and what `text`, `name`, `ok` and `usage` carry for it.
|
|
63
|
+
- `RunUsage` (type) — What a run has spent, counted from the start of the run rather than for the turn that carried it: a client draws the latest one it has seen and needs no arithmetic of its own, and one lost to the backlog cap costs nothing because the next supersedes it.
|
|
64
|
+
- `resetEvents` — Test seam: forget every run, so one test's events cannot be read by the next.
|
|
65
|
+
- `watch` — Everything that has happened on a run, then everything that happens next, until it ends.
|
|
66
|
+
|
|
67
|
+
### reset
|
|
68
|
+
|
|
69
|
+
- `resetAll` — Forgets everything this package remembers between calls.
|
|
70
|
+
|
|
71
|
+
### retry
|
|
72
|
+
|
|
73
|
+
Everything about a request failing that is not about what the request said.
|
|
74
|
+
|
|
75
|
+
- `backoffMs` — Exponential, with jitter so several tasks failing at once do not return in lockstep.
|
|
76
|
+
- `ContextOverflow` — The request was bigger than the model will read.
|
|
77
|
+
- `compact` — 1234 → "1.2k".
|
|
78
|
+
- `EndpointSilent` — The endpoint stopped answering mid-request.
|
|
79
|
+
- `isOverflow` — Whether a refusal means the request was too big, rather than merely refused.
|
|
80
|
+
- `isTransient` — Whether a failed request is worth trying again.
|
|
81
|
+
- `requestTokens` — What this request will cost the window, in tokens, near enough.
|
|
82
|
+
- `SMALLEST_LIKELY_WINDOW` — The smallest window worth believing in, and the floor under both of its uses.
|
|
83
|
+
- `sleep` — A delay an abort cuts short, rejecting rather than resolving early.
|
|
84
|
+
|
|
85
|
+
### run-turn
|
|
86
|
+
|
|
87
|
+
One turn, given as many attempts as the caller allows.
|
|
88
|
+
|
|
89
|
+
- `RunTurnOptions` (type) — A retry is not the same event as a downgrade, but a watcher wants to be told about both.
|
|
90
|
+
- `runTurn` — `request` is a callback rather than a body because the body has to be rebuilt from whatever the last attempt latched off: the tools it sends depend on `strictSchemas`, and `relaxTools` has to apply to the schemas that were just sanitised.
|
|
91
|
+
|
|
92
|
+
### schema-compat
|
|
93
|
+
|
|
94
|
+
JSON Schema compatibility for llama.cpp-backed servers.
|
|
95
|
+
|
|
96
|
+
- `isGrammarError` — Does this failure look like the server could not build a grammar from our tool schemas?
|
|
97
|
+
- `relaxTools` — The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in `pattern` and most `format` values, both of which only ever narrowed a string the tool re-validates anyway.
|
|
98
|
+
- `sanitizeTools` — Tool definitions a strict server will accept, remembered per definition object.
|
|
99
|
+
|
|
100
|
+
### side-task
|
|
101
|
+
|
|
102
|
+
One-shot calls that support a run without being one: picking tools, naming a session, summarising a transcript, proposing follow-ups.
|
|
103
|
+
|
|
104
|
+
- `ask` — Runs a side task and returns the reply text, thinking stripped.
|
|
105
|
+
- `clean` — Strips the quoting and list punctuation models decorate short answers with.
|
|
106
|
+
- `listLines` — A list-shaped reply, one item per line, cleaned of the bullets and quotes models decorate them with.
|
|
107
|
+
- `parseJson` — Models are asked for JSON and often answer with prose around it, or a fenced block.
|
|
108
|
+
- `resetHints` — Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints.
|
|
109
|
+
- `SideTaskOptions` (type) — What a side task may be given.
|
|
110
|
+
- `tryAsk` — A side task is never worth failing the work it supports.
|
|
111
|
+
|
|
112
|
+
### stream
|
|
113
|
+
|
|
114
|
+
Reading one streamed turn back into a message.
|
|
115
|
+
|
|
116
|
+
- `Produced` (type) — Whether the server has started answering.
|
|
117
|
+
- `StreamTurnOptions` (type) — What `streamTurn` takes besides the request body.
|
|
118
|
+
- `streamTurn` — Runs one turn as a stream, reporting tokens as they arrive and assembling them back into a message.
|
|
119
|
+
- `Turn` (type) — One streamed turn, put back together into the shape a loop and a transcript work with.
|
|
120
|
+
- `TurnUsage` (type) — What a turn cost.
|
|
121
|
+
|
|
122
|
+
### tokens
|
|
123
|
+
|
|
124
|
+
- `estimateTokens` — Rough token count.
|
|
125
|
+
|
|
126
|
+
### tool-loading
|
|
127
|
+
|
|
128
|
+
- `carryOver` — The tools to start the next turn with: recently used, newest last, capped.
|
|
129
|
+
- `catalogList` — The catalogue as a plain grouped listing of names, loaded ones marked.
|
|
130
|
+
- `catalogPrompt` — The catalogue block appended to the system prompt.
|
|
131
|
+
- `expandNames` — Resolves requested names against the catalogue, expanding trailing `*` wildcards.
|
|
132
|
+
- `inCatalog` — Whether the catalogue holds a tool by this name.
|
|
133
|
+
- `LOAD_TOOLS` — On-demand tool loading.
|
|
134
|
+
- `LOAD_TOOLS_DEFINITION` — One object for the life of the process — the agent loop asks for it on every iteration.
|
|
135
|
+
- `loadResult` — What `load_tools` reports back: the descriptions, now that they are worth their tokens.
|
|
136
|
+
- `MAX_CARRIED` — The most a conversation carries between turns.
|
|
137
|
+
- `MAX_PER_LOAD` — The most a single `load_tools` call may pull in.
|
|
138
|
+
- `PRESELECT_SYSTEM` — Tool preselection.
|
|
139
|
+
- `preselectInput` — The user message for a preselection call: the catalogue, then the request.
|
|
140
|
+
- `preselection` — Resolves a preselection against the catalogue: unknown names dropped, count capped.
|
|
141
|
+
- `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.0.
|
|
3
|
+
"version": "2.0.2",
|
|
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",
|
|
@@ -34,17 +34,20 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
|
-
"dist"
|
|
37
|
+
"dist",
|
|
38
|
+
"llms.txt"
|
|
38
39
|
],
|
|
39
40
|
"publishConfig": {
|
|
40
41
|
"access": "public"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
|
-
"build": "tsc -p tsconfig.build.json",
|
|
44
|
+
"build": "tsc -p tsconfig.build.json && node scripts/llms-txt.mjs",
|
|
44
45
|
"prepare": "npm run build",
|
|
45
46
|
"typecheck": "tsc -p tsconfig.tests.json",
|
|
46
47
|
"test": "vitest run",
|
|
47
48
|
"test:watch": "vitest",
|
|
49
|
+
"llms": "node scripts/llms-txt.mjs",
|
|
50
|
+
"llms:check": "node scripts/llms-txt.mjs --check",
|
|
48
51
|
"lint": "biome check .",
|
|
49
52
|
"format": "biome check --write .",
|
|
50
53
|
"bench": "vitest bench --run",
|
|
@@ -54,7 +57,7 @@
|
|
|
54
57
|
"openai": ">=6"
|
|
55
58
|
},
|
|
56
59
|
"devDependencies": {
|
|
57
|
-
"@biomejs/biome": "
|
|
60
|
+
"@biomejs/biome": "2.5.12",
|
|
58
61
|
"@semantic-release/changelog": "^7.0.0",
|
|
59
62
|
"@semantic-release/git": "^11.0.1",
|
|
60
63
|
"@types/node": "^26.4.0",
|