@cubicecho/agent-core 2.11.0 → 2.12.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 +114 -4
- package/dist/agent-loop.d.ts +15 -2
- package/dist/agent-loop.js +131 -22
- package/dist/calibration.d.ts +33 -0
- package/dist/calibration.js +87 -0
- package/dist/capabilities.d.ts +8 -0
- package/dist/capabilities.js +1 -0
- package/dist/compaction.d.ts +26 -8
- package/dist/compaction.js +36 -17
- package/dist/continuation.d.ts +59 -0
- package/dist/continuation.js +159 -0
- package/dist/events.d.ts +112 -1
- package/dist/events.js +111 -11
- package/dist/hooks.d.ts +29 -1
- package/dist/hooks.js +36 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/reset.d.ts +7 -6
- package/dist/reset.js +9 -6
- package/dist/retry.d.ts +36 -3
- package/dist/retry.js +49 -16
- package/dist/run-turn.d.ts +4 -0
- package/dist/run-turn.js +19 -3
- package/dist/snapshot.d.ts +5 -0
- package/dist/snapshot.js +5 -0
- package/dist/stream.d.ts +74 -2
- package/dist/stream.js +45 -5
- package/llms.txt +26 -1
- package/package.json +1 -1
package/dist/hooks.d.ts
CHANGED
|
@@ -89,6 +89,12 @@ export interface HookOutcome {
|
|
|
89
89
|
inject: boolean;
|
|
90
90
|
/** The most of `text` that is injected, in estimated tokens. */
|
|
91
91
|
maxTokens: number;
|
|
92
|
+
/**
|
|
93
|
+
* Asks that what the event announces not happen. Only a `beforeCompact` hook's is read, and
|
|
94
|
+
* only by a host that waits for it — see `consult` — and never on an outcome that is not `ok`,
|
|
95
|
+
* since a hook that crashed has not said anything.
|
|
96
|
+
*/
|
|
97
|
+
veto?: boolean;
|
|
92
98
|
}
|
|
93
99
|
/**
|
|
94
100
|
* One hook's line for whoever is watching: the context it added, or why it added none. A hook
|
|
@@ -105,6 +111,8 @@ export interface HookNote {
|
|
|
105
111
|
text?: string;
|
|
106
112
|
/** Why it added nothing: it failed, timed out, or never ran. */
|
|
107
113
|
error?: string;
|
|
114
|
+
/** Set when the hook vetoed what the event announced, and was waited for. See `consult`. */
|
|
115
|
+
veto?: true;
|
|
108
116
|
}
|
|
109
117
|
/**
|
|
110
118
|
* Runs one event's hooks. Should resolve rather than reject — a hook failing is an outcome — but
|
|
@@ -297,7 +305,8 @@ export declare function gather(run: HookRunner, events: readonly HookEvent[], co
|
|
|
297
305
|
* rejects, so a host can fire it without awaiting it.
|
|
298
306
|
*
|
|
299
307
|
* No signal: these run once the turn has been answered, and a reader who stops listening at that
|
|
300
|
-
* point has not asked for the turn not to be remembered.
|
|
308
|
+
* point has not asked for the turn not to be remembered. Nor is a `veto` read, since whatever it
|
|
309
|
+
* would stop is already under way; `consult` is the one that waits for it.
|
|
301
310
|
*
|
|
302
311
|
* @param run Runs the event's hooks.
|
|
303
312
|
* @param event `afterTurn`, `beforeCompact`, `sessionEnd` or `sessionDelete`. An injecting event
|
|
@@ -307,3 +316,22 @@ export declare function gather(run: HookRunner, events: readonly HookEvent[], co
|
|
|
307
316
|
* @returns The same notes.
|
|
308
317
|
*/
|
|
309
318
|
export declare function notify(run: HookRunner, event: HookEvent, context: HookContext, onNote?: (note: HookNote) => void): Promise<HookNote[]>;
|
|
319
|
+
/**
|
|
320
|
+
* Runs an event's hooks and waits for their say, for a host that will hold off when one of them
|
|
321
|
+
* vetoes.
|
|
322
|
+
*
|
|
323
|
+
* `notify` runs beside the thing it announces and cannot stop it; this runs ahead of it, so each
|
|
324
|
+
* hook's time is added to whatever waits on the answer. A host that does not mean to act on a
|
|
325
|
+
* veto should call `notify` instead. Never rejects: a runner that throws is noted as a failure,
|
|
326
|
+
* and a failure is not a veto — a memory server that is down has not asked for anything.
|
|
327
|
+
*
|
|
328
|
+
* @param run Runs the event's hooks.
|
|
329
|
+
* @param event What is about to happen. Only `beforeCompact` has anything a veto can stop.
|
|
330
|
+
* @param context What the hooks are told.
|
|
331
|
+
* @param onNote Hears each note: every failure, and every veto, naming the hook that made it.
|
|
332
|
+
* @returns The same notes, and `vetoed` when any `ok` outcome carried `veto`.
|
|
333
|
+
*/
|
|
334
|
+
export declare function consult(run: HookRunner, event: HookEvent, context: HookContext, onNote?: (note: HookNote) => void): Promise<{
|
|
335
|
+
notes: HookNote[];
|
|
336
|
+
vetoed: boolean;
|
|
337
|
+
}>;
|
package/dist/hooks.js
CHANGED
|
@@ -279,7 +279,8 @@ export async function gather(run, events, context, { signal, onNote, maxTokens,
|
|
|
279
279
|
* rejects, so a host can fire it without awaiting it.
|
|
280
280
|
*
|
|
281
281
|
* No signal: these run once the turn has been answered, and a reader who stops listening at that
|
|
282
|
-
* point has not asked for the turn not to be remembered.
|
|
282
|
+
* point has not asked for the turn not to be remembered. Nor is a `veto` read, since whatever it
|
|
283
|
+
* would stop is already under way; `consult` is the one that waits for it.
|
|
283
284
|
*
|
|
284
285
|
* @param run Runs the event's hooks.
|
|
285
286
|
* @param event `afterTurn`, `beforeCompact`, `sessionEnd` or `sessionDelete`. An injecting event
|
|
@@ -297,3 +298,37 @@ export async function notify(run, event, context, onNote) {
|
|
|
297
298
|
onNote?.(note);
|
|
298
299
|
return notes;
|
|
299
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Runs an event's hooks and waits for their say, for a host that will hold off when one of them
|
|
303
|
+
* vetoes.
|
|
304
|
+
*
|
|
305
|
+
* `notify` runs beside the thing it announces and cannot stop it; this runs ahead of it, so each
|
|
306
|
+
* hook's time is added to whatever waits on the answer. A host that does not mean to act on a
|
|
307
|
+
* veto should call `notify` instead. Never rejects: a runner that throws is noted as a failure,
|
|
308
|
+
* and a failure is not a veto — a memory server that is down has not asked for anything.
|
|
309
|
+
*
|
|
310
|
+
* @param run Runs the event's hooks.
|
|
311
|
+
* @param event What is about to happen. Only `beforeCompact` has anything a veto can stop.
|
|
312
|
+
* @param context What the hooks are told.
|
|
313
|
+
* @param onNote Hears each note: every failure, and every veto, naming the hook that made it.
|
|
314
|
+
* @returns The same notes, and `vetoed` when any `ok` outcome carried `veto`.
|
|
315
|
+
*/
|
|
316
|
+
export async function consult(run, event, context, onNote) {
|
|
317
|
+
const outcomes = await runSafely(run, event, context);
|
|
318
|
+
const notes = [];
|
|
319
|
+
for (const outcome of outcomes) {
|
|
320
|
+
if (!outcome.ok)
|
|
321
|
+
notes.push(assembleContext([outcome]).notes[0]);
|
|
322
|
+
else if (outcome.veto === true) {
|
|
323
|
+
notes.push({
|
|
324
|
+
event: outcome.event,
|
|
325
|
+
source: outcome.label,
|
|
326
|
+
hookId: outcome.hookId,
|
|
327
|
+
veto: true,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const note of notes)
|
|
332
|
+
onNote?.(note);
|
|
333
|
+
return { notes, vetoed: notes.some((note) => note.veto) };
|
|
334
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,16 +10,18 @@
|
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
12
12
|
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopResult, buildBody, preselect, preview, resolveApiKey, runAgentLoop, type ToolCallOutcome, type ToolCallRequest, } from "./agent-loop.ts";
|
|
13
|
+
export { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.ts";
|
|
13
14
|
export { type Capabilities, capabilitiesFor, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
14
15
|
export type { CatalogServer } from "./catalog.ts";
|
|
15
16
|
export { type ClientPoolOptions, configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, type ModelInfo, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.ts";
|
|
16
17
|
export { COMPACT_AT, type CompactionOptions, type CompactionPlan, compactTranscript, KEEP_RATIO, type PruneOptions, planCompaction, pruneToolResults, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.ts";
|
|
17
18
|
export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } from "./config.ts";
|
|
19
|
+
export { type ContinueTurnOptions, continueTurn, isContinuable, } from "./continuation.ts";
|
|
18
20
|
export { errorMessage } from "./errors.ts";
|
|
19
|
-
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunUsage, resetEvents, watch, } from "./events.ts";
|
|
20
|
-
export { assembleContext, configureHooks, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, type HookContext, type HookEvent, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.ts";
|
|
21
|
+
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunMetrics, type RunMetricsOptions, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from "./events.ts";
|
|
22
|
+
export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, type HookContext, type HookEvent, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.ts";
|
|
21
23
|
export { resetAll } from "./reset.ts";
|
|
22
|
-
export { backoffMs, ContextOverflow, compact, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.ts";
|
|
24
|
+
export { backoffMs, CHARS_PER_TOKEN, ContextOverflow, compact, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, type TokenEstimateOptions, toolsChars, } from "./retry.ts";
|
|
23
25
|
export { type RunTurnOptions, runTurn } from "./run-turn.ts";
|
|
24
26
|
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.ts";
|
|
25
27
|
export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTaskInput, type SideTaskOptions, tryAsk, } from "./side-task.ts";
|
package/dist/index.js
CHANGED
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
12
12
|
export { buildBody, preselect, preview, resolveApiKey, runAgentLoop, } from "./agent-loop.js";
|
|
13
|
+
export { calibrate, charsPerTokenFor, resetCalibration } from "./calibration.js";
|
|
13
14
|
export { capabilitiesFor, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
14
15
|
export { configureClients, contextLimitFor, endpointId, endpointKey, FIRST_TOKEN_FACTOR, firstTokenMs, getClient, listModels, NO_KEY, resetClients, servedWindow, timeoutMs, } from "./client.js";
|
|
15
16
|
export { COMPACT_AT, compactTranscript, KEEP_RATIO, planCompaction, pruneToolResults, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.js";
|
|
17
|
+
export { continueTurn, isContinuable, } from "./continuation.js";
|
|
16
18
|
export { errorMessage } from "./errors.js";
|
|
17
|
-
export { configureEvents, emit, endRun, fold, history, resetEvents, watch, } from "./events.js";
|
|
18
|
-
export { assembleContext, configureHooks, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.js";
|
|
19
|
+
export { configureEvents, emit, endRun, fold, history, resetEvents, runMetrics, watch, } from "./events.js";
|
|
20
|
+
export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.js";
|
|
19
21
|
export { resetAll } from "./reset.js";
|
|
20
|
-
export { backoffMs, ContextOverflow, compact, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
|
|
22
|
+
export { backoffMs, CHARS_PER_TOKEN, ContextOverflow, compact, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, toolsChars, } from "./retry.js";
|
|
21
23
|
export { runTurn } from "./run-turn.js";
|
|
22
24
|
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.js";
|
|
23
25
|
export { ask, askJson, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
|
package/dist/reset.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Forgets everything this package remembers between calls.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Six modules here keep state for the life of the process, each for a good reason and each
|
|
5
5
|
* with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
6
|
-
* out not to take `stream_options` or a grammar,
|
|
7
|
-
* hints, the event bus, and the hooks' configured
|
|
8
|
-
* `
|
|
9
|
-
* test that means to clear one thing
|
|
6
|
+
* out not to take `stream_options` or a grammar, each model's measured characters per token,
|
|
7
|
+
* the models that refused the no-thinking hints, the event bus, and the hooks' configured
|
|
8
|
+
* budget and preface. `resetClients`, `resetCapabilities`, `resetCalibration`, `resetHints`,
|
|
9
|
+
* `resetEvents` and `resetHooks` stay exported, because a test that means to clear one thing
|
|
10
|
+
* should say so.
|
|
10
11
|
*
|
|
11
12
|
* This is for the other case, which is every teardown. What they hold is *latched
|
|
12
13
|
* refusals* — a fact one test taught the process about an endpoint, still true as far as the
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
* one that reads the latch fails only when it happens to run second. `tests/side-task-hints.test.ts`
|
|
16
17
|
* was written that way and only passed because every case had been handed a hostname of its own.
|
|
17
18
|
*
|
|
18
|
-
* It is also the seam that does not need finding again. A
|
|
19
|
+
* It is also the seam that does not need finding again. A seventh module with a cache is a seventh
|
|
19
20
|
* line here, rather than an edit to the teardown of three consumers who will not all notice.
|
|
20
21
|
*/
|
|
21
22
|
export declare function resetAll(): void;
|
package/dist/reset.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resetCalibration } from "./calibration.js";
|
|
1
2
|
import { resetCapabilities } from "./capabilities.js";
|
|
2
3
|
import { resetClients } from "./client.js";
|
|
3
4
|
import { resetEvents } from "./events.js";
|
|
@@ -6,12 +7,13 @@ import { resetHints } from "./side-task.js";
|
|
|
6
7
|
/**
|
|
7
8
|
* Forgets everything this package remembers between calls.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* Six modules here keep state for the life of the process, each for a good reason and each
|
|
10
11
|
* with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
11
|
-
* out not to take `stream_options` or a grammar,
|
|
12
|
-
* hints, the event bus, and the hooks' configured
|
|
13
|
-
* `
|
|
14
|
-
* test that means to clear one thing
|
|
12
|
+
* out not to take `stream_options` or a grammar, each model's measured characters per token,
|
|
13
|
+
* the models that refused the no-thinking hints, the event bus, and the hooks' configured
|
|
14
|
+
* budget and preface. `resetClients`, `resetCapabilities`, `resetCalibration`, `resetHints`,
|
|
15
|
+
* `resetEvents` and `resetHooks` stay exported, because a test that means to clear one thing
|
|
16
|
+
* should say so.
|
|
15
17
|
*
|
|
16
18
|
* This is for the other case, which is every teardown. What they hold is *latched
|
|
17
19
|
* refusals* — a fact one test taught the process about an endpoint, still true as far as the
|
|
@@ -20,12 +22,13 @@ import { resetHints } from "./side-task.js";
|
|
|
20
22
|
* one that reads the latch fails only when it happens to run second. `tests/side-task-hints.test.ts`
|
|
21
23
|
* was written that way and only passed because every case had been handed a hostname of its own.
|
|
22
24
|
*
|
|
23
|
-
* It is also the seam that does not need finding again. A
|
|
25
|
+
* It is also the seam that does not need finding again. A seventh module with a cache is a seventh
|
|
24
26
|
* line here, rather than an edit to the teardown of three consumers who will not all notice.
|
|
25
27
|
*/
|
|
26
28
|
export function resetAll() {
|
|
27
29
|
resetClients();
|
|
28
30
|
resetCapabilities();
|
|
31
|
+
resetCalibration();
|
|
29
32
|
resetHints();
|
|
30
33
|
resetEvents();
|
|
31
34
|
resetHooks();
|
package/dist/retry.d.ts
CHANGED
|
@@ -29,10 +29,41 @@ export declare class ContextOverflow extends Error {
|
|
|
29
29
|
* @param tokens The count to render.
|
|
30
30
|
*/
|
|
31
31
|
export declare const compact: (tokens: number) => string;
|
|
32
|
+
/**
|
|
33
|
+
* The divisor behind `estimateTokens`, applied here to a character count rather than a string.
|
|
34
|
+
*
|
|
35
|
+
* The fallback, not the rule: once a turn has come back with a reported prompt count, `runTurn`
|
|
36
|
+
* divides by what that endpoint's model was measured at instead. See `charsPerTokenFor`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CHARS_PER_TOKEN = 4;
|
|
39
|
+
/** What the two token estimates below take besides what they measure. */
|
|
40
|
+
export interface TokenEstimateOptions {
|
|
41
|
+
/**
|
|
42
|
+
* The divisor, `CHARS_PER_TOKEN` unless given — `charsPerTokenFor` for a model whose reported
|
|
43
|
+
* usage has calibrated it. A value that is not a number above zero is ignored.
|
|
44
|
+
*/
|
|
45
|
+
charsPerToken?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* How many characters a tool array is worth, measured once per array.
|
|
49
|
+
*
|
|
50
|
+
* @param tools The tool definitions as they will be sent. An empty array is worth nothing.
|
|
51
|
+
*/
|
|
52
|
+
export declare function toolsChars(tools: OpenAI.ChatCompletionTool[]): number;
|
|
53
|
+
/**
|
|
54
|
+
* How many characters a request is worth: the walk `requestTokens` divides, without the division.
|
|
55
|
+
*
|
|
56
|
+
* What calibration reads a reported prompt count against, since a ratio is only as good as the
|
|
57
|
+
* character count it was taken over agreeing with the one it is later applied to.
|
|
58
|
+
*
|
|
59
|
+
* @param body The request as it was sent, tools included.
|
|
60
|
+
*/
|
|
61
|
+
export declare function requestChars(body: OpenAI.ChatCompletionCreateParamsStreaming): number;
|
|
32
62
|
/**
|
|
33
63
|
* What this request will cost the window, in tokens, near enough.
|
|
34
64
|
*
|
|
35
|
-
* See `estimateTokens` for why it is characters over four and which way it is wrong on purpose
|
|
65
|
+
* See `estimateTokens` for why it is characters over four and which way it is wrong on purpose,
|
|
66
|
+
* and `charsPerTokenFor` for the divisor a model's own reported usage has measured instead.
|
|
36
67
|
*
|
|
37
68
|
* Summed by walking the body rather than by serialising it. `JSON.stringify` on the messages
|
|
38
69
|
* built the entire transcript into a string on every call and threw it away having read nothing
|
|
@@ -45,8 +76,9 @@ export declare const compact: (tokens: number) => string;
|
|
|
45
76
|
* over four.
|
|
46
77
|
*
|
|
47
78
|
* @param body The request as it will be sent, tools included.
|
|
79
|
+
* @param options The divisor, `CHARS_PER_TOKEN` when none is given.
|
|
48
80
|
*/
|
|
49
|
-
export declare const requestTokens: (body: OpenAI.ChatCompletionCreateParamsStreaming) => number;
|
|
81
|
+
export declare const requestTokens: (body: OpenAI.ChatCompletionCreateParamsStreaming, { charsPerToken }?: TokenEstimateOptions) => number;
|
|
50
82
|
/**
|
|
51
83
|
* One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
|
|
52
84
|
*
|
|
@@ -54,8 +86,9 @@ export declare const requestTokens: (body: OpenAI.ChatCompletionCreateParamsStre
|
|
|
54
86
|
* tail — where `estimateTokens` on the text alone would leave out the calls and the envelope.
|
|
55
87
|
*
|
|
56
88
|
* @param message The message as it will be sent.
|
|
89
|
+
* @param options The divisor, `CHARS_PER_TOKEN` when none is given.
|
|
57
90
|
*/
|
|
58
|
-
export declare const messageTokens: (message: OpenAI.ChatCompletionMessageParam) => number;
|
|
91
|
+
export declare const messageTokens: (message: OpenAI.ChatCompletionMessageParam, { charsPerToken }?: TokenEstimateOptions) => number;
|
|
59
92
|
/**
|
|
60
93
|
* Whether a refusal means the request was too big, rather than merely refused.
|
|
61
94
|
*
|
package/dist/retry.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
-
import { estimateTokens } from "./tokens.js";
|
|
3
2
|
/**
|
|
4
3
|
* Everything about a request failing that is not about what the request said.
|
|
5
4
|
*
|
|
@@ -63,8 +62,15 @@ const REFUSAL_PART = 32;
|
|
|
63
62
|
const REASONING_KEY = 23;
|
|
64
63
|
/** The same for `"reasoning":"",`, OpenRouter's spelling of it. */
|
|
65
64
|
const REASONING_ALT_KEY = 15;
|
|
66
|
-
/**
|
|
67
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The divisor behind `estimateTokens`, applied here to a character count rather than a string.
|
|
67
|
+
*
|
|
68
|
+
* The fallback, not the rule: once a turn has come back with a reported prompt count, `runTurn`
|
|
69
|
+
* divides by what that endpoint's model was measured at instead. See `charsPerTokenFor`.
|
|
70
|
+
*/
|
|
71
|
+
export const CHARS_PER_TOKEN = 4;
|
|
72
|
+
/** The divisor an option asked for, or the fallback when it asked for nothing usable. */
|
|
73
|
+
const divisor = (charsPerToken) => charsPerToken !== undefined && charsPerToken > 0 ? charsPerToken : CHARS_PER_TOKEN;
|
|
68
74
|
/** How many characters one message is worth: its keys, and its content in whichever shape. */
|
|
69
75
|
function messageChars(message) {
|
|
70
76
|
let chars = message.role.length + ENVELOPE;
|
|
@@ -114,22 +120,47 @@ function messageChars(message) {
|
|
|
114
120
|
* the array-level memoisation `tests/retry.test.ts` pins, which deliberately holds a mutated array
|
|
115
121
|
* to its first reading. Sizing happens once per turn either way, so the miss costs one walk of the
|
|
116
122
|
* schemas rather than a walk per attempt.
|
|
123
|
+
*
|
|
124
|
+
* Characters rather than tokens, so a calibrated divisor applies to the schemas without walking
|
|
125
|
+
* them again.
|
|
126
|
+
*/
|
|
127
|
+
const toolLengths = new WeakMap();
|
|
128
|
+
/**
|
|
129
|
+
* How many characters a tool array is worth, measured once per array.
|
|
130
|
+
*
|
|
131
|
+
* @param tools The tool definitions as they will be sent. An empty array is worth nothing.
|
|
117
132
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
export function toolsChars(tools) {
|
|
134
|
+
if (!tools.length)
|
|
135
|
+
return 0;
|
|
136
|
+
const hit = toolLengths.get(tools);
|
|
121
137
|
if (hit !== undefined)
|
|
122
138
|
return hit;
|
|
123
139
|
// Schemas are arbitrarily shaped, so this one really is a serialisation — but it happens once
|
|
124
140
|
// per tool array rather than once per turn.
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
return
|
|
141
|
+
const length = JSON.stringify(tools).length;
|
|
142
|
+
toolLengths.set(tools, length);
|
|
143
|
+
return length;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* How many characters a request is worth: the walk `requestTokens` divides, without the division.
|
|
147
|
+
*
|
|
148
|
+
* What calibration reads a reported prompt count against, since a ratio is only as good as the
|
|
149
|
+
* character count it was taken over agreeing with the one it is later applied to.
|
|
150
|
+
*
|
|
151
|
+
* @param body The request as it was sent, tools included.
|
|
152
|
+
*/
|
|
153
|
+
export function requestChars(body) {
|
|
154
|
+
let chars = 0;
|
|
155
|
+
for (const message of body.messages)
|
|
156
|
+
chars += messageChars(message);
|
|
157
|
+
return chars;
|
|
128
158
|
}
|
|
129
159
|
/**
|
|
130
160
|
* What this request will cost the window, in tokens, near enough.
|
|
131
161
|
*
|
|
132
|
-
* See `estimateTokens` for why it is characters over four and which way it is wrong on purpose
|
|
162
|
+
* See `estimateTokens` for why it is characters over four and which way it is wrong on purpose,
|
|
163
|
+
* and `charsPerTokenFor` for the divisor a model's own reported usage has measured instead.
|
|
133
164
|
*
|
|
134
165
|
* Summed by walking the body rather than by serialising it. `JSON.stringify` on the messages
|
|
135
166
|
* built the entire transcript into a string on every call and threw it away having read nothing
|
|
@@ -142,14 +173,15 @@ function toolsCost(tools) {
|
|
|
142
173
|
* over four.
|
|
143
174
|
*
|
|
144
175
|
* @param body The request as it will be sent, tools included.
|
|
176
|
+
* @param options The divisor, `CHARS_PER_TOKEN` when none is given.
|
|
145
177
|
*/
|
|
146
|
-
export const requestTokens = (body) => {
|
|
178
|
+
export const requestTokens = (body, { charsPerToken } = {}) => {
|
|
147
179
|
// Characters first and the division once at the end, rather than a rounded count per message:
|
|
148
180
|
// `Math.ceil` on every one of a few hundred messages is a few hundred tokens of pure rounding.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return Math.ceil(
|
|
181
|
+
// The tools are divided on their own, as they were when their tokens were cached, so the
|
|
182
|
+
// uncalibrated count is the one it always was.
|
|
183
|
+
const per = divisor(charsPerToken);
|
|
184
|
+
return Math.ceil(requestChars(body) / per) + Math.ceil(toolsChars(body.tools ?? []) / per);
|
|
153
185
|
};
|
|
154
186
|
/**
|
|
155
187
|
* One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
|
|
@@ -158,8 +190,9 @@ export const requestTokens = (body) => {
|
|
|
158
190
|
* tail — where `estimateTokens` on the text alone would leave out the calls and the envelope.
|
|
159
191
|
*
|
|
160
192
|
* @param message The message as it will be sent.
|
|
193
|
+
* @param options The divisor, `CHARS_PER_TOKEN` when none is given.
|
|
161
194
|
*/
|
|
162
|
-
export const messageTokens = (message) => Math.ceil(messageChars(message) /
|
|
195
|
+
export const messageTokens = (message, { charsPerToken } = {}) => Math.ceil(messageChars(message) / divisor(charsPerToken));
|
|
163
196
|
/**
|
|
164
197
|
* Servers refuse an over-long request in their own words; these are the ones worth reading as
|
|
165
198
|
* that rather than as a broken request. Matched loosely — every one of them is some
|
package/dist/run-turn.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ import { type StreamTurnOptions, type Turn } from "./stream.ts";
|
|
|
22
22
|
* attempt would say everything twice. That is what `produced` is, one box per attempt — set by
|
|
23
23
|
* a chunk that carried something rather than by a chunk arriving, so the empty opening chunk
|
|
24
24
|
* most servers send does not cost the retry.
|
|
25
|
+
*
|
|
26
|
+
* What the turn cost in attempts comes back on its usage — `wallMs`, `retries`, `timeouts` — and
|
|
27
|
+
* what its prompt was reported at calibrates the ratio the next request to that model is sized
|
|
28
|
+
* with. See `calibrate`.
|
|
25
29
|
*/
|
|
26
30
|
/** A retry is not the same event as a downgrade, but a watcher wants to be told about both. */
|
|
27
31
|
export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
package/dist/run-turn.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { calibrate, charsPerTokenFor } from "./calibration.js";
|
|
1
2
|
import { negotiate } from "./capabilities.js";
|
|
2
3
|
import { errorMessage } from "./errors.js";
|
|
3
|
-
import { backoffMs, ContextOverflow, compact, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
|
|
4
|
+
import { backoffMs, ContextOverflow, compact, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
|
|
4
5
|
import { streamTurn } from "./stream.js";
|
|
5
6
|
/**
|
|
6
7
|
* `request` is a callback rather than a body because the body has to be rebuilt from whatever
|
|
@@ -21,11 +22,16 @@ export async function runTurn(client, supports, request, { maxRetries = 0, onNot
|
|
|
21
22
|
// does not change between attempts — so the first body is the one worth measuring, and
|
|
22
23
|
// measuring the rest would only spend the walk again to reach the same answer.
|
|
23
24
|
let sized = false;
|
|
25
|
+
// The body the answer was given to, for the calibration once it is in hand.
|
|
26
|
+
let sent;
|
|
24
27
|
const measured = (capabilities, forModel) => {
|
|
25
28
|
const body = request(capabilities, forModel);
|
|
29
|
+
sent = body;
|
|
26
30
|
if (!sized && contextLimit >= SMALLEST_LIKELY_WINDOW) {
|
|
27
31
|
sized = true;
|
|
28
|
-
const needed = requestTokens(body
|
|
32
|
+
const needed = requestTokens(body, {
|
|
33
|
+
charsPerToken: charsPerTokenFor(supports, body.model),
|
|
34
|
+
});
|
|
29
35
|
// The endpoint refuses on the prompt plus the reply — llama.cpp sizes the slot with
|
|
30
36
|
// `n_predict` in, OpenAI with the ceiling — so a prompt that fits the window but not the
|
|
31
37
|
// window less the ceiling was let through here to be refused one round trip later, which
|
|
@@ -45,10 +51,17 @@ export async function runTurn(client, supports, request, { maxRetries = 0, onNot
|
|
|
45
51
|
// When the server first said it was loading. Unset until then, and never reset: a model that
|
|
46
52
|
// loads, fails and loads again has had its allowance.
|
|
47
53
|
let loadingSince;
|
|
54
|
+
const started = Date.now();
|
|
55
|
+
let retries = 0;
|
|
56
|
+
let timeouts = 0;
|
|
48
57
|
for (let attempt = 0;; attempt++) {
|
|
49
58
|
const produced = { any: false };
|
|
50
59
|
try {
|
|
51
|
-
|
|
60
|
+
const turn = await negotiate(supports, (capabilities, box, forModel) => streamTurn(client, measured(capabilities, forModel), { ...stream, produced: box }), { produced, onNotice, model, droppable });
|
|
61
|
+
if (sent)
|
|
62
|
+
calibrate(supports, sent, turn.usage.prompt);
|
|
63
|
+
Object.assign(turn.usage, { wallMs: Date.now() - started, retries, timeouts });
|
|
64
|
+
return turn;
|
|
52
65
|
}
|
|
53
66
|
catch (error) {
|
|
54
67
|
// The abort is read before the classification, not after. A run stopped by its operator
|
|
@@ -85,6 +98,9 @@ export async function runTurn(client, supports, request, { maxRetries = 0, onNot
|
|
|
85
98
|
}
|
|
86
99
|
if (attempt >= maxRetries || !isTransient(error))
|
|
87
100
|
throw error;
|
|
101
|
+
retries++;
|
|
102
|
+
if (error instanceof EndpointSilent)
|
|
103
|
+
timeouts++;
|
|
88
104
|
const wait = backoffMs(attempt);
|
|
89
105
|
// Reported in whatever unit reads as a number: the first backoff is under a second, and
|
|
90
106
|
// "retrying in 0s" is what rounding it to seconds says.
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export interface ModelSnapshot {
|
|
|
15
15
|
chosenTemperature: boolean;
|
|
16
16
|
refusedFields: string[];
|
|
17
17
|
structuredOutput: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Continues a trailing assistant message. Absent in a snapshot taken before it was latched, which
|
|
20
|
+
* reads as not refused.
|
|
21
|
+
*/
|
|
22
|
+
assistantPrefill: boolean;
|
|
18
23
|
/** Takes the no-thinking hints `ask` sends. */
|
|
19
24
|
thinkingHints: boolean;
|
|
20
25
|
}
|
package/dist/snapshot.js
CHANGED
|
@@ -16,6 +16,7 @@ const optimisticModel = () => ({
|
|
|
16
16
|
chosenTemperature: true,
|
|
17
17
|
refusedFields: [],
|
|
18
18
|
structuredOutput: true,
|
|
19
|
+
assistantPrefill: true,
|
|
19
20
|
thinkingHints: true,
|
|
20
21
|
});
|
|
21
22
|
const refusedAnything = (model) => !model.reasoningEffort ||
|
|
@@ -23,6 +24,7 @@ const refusedAnything = (model) => !model.reasoningEffort ||
|
|
|
23
24
|
!model.chosenTemperature ||
|
|
24
25
|
!model.thinkingHints ||
|
|
25
26
|
!model.structuredOutput ||
|
|
27
|
+
!model.assistantPrefill ||
|
|
26
28
|
model.refusedFields.length > 0;
|
|
27
29
|
/**
|
|
28
30
|
* Every refusal this process has latched, as a JSON-safe blob to store and hand back on boot.
|
|
@@ -48,6 +50,7 @@ export function exportCapabilities() {
|
|
|
48
50
|
chosenTemperature: refused.chosenTemperature,
|
|
49
51
|
refusedFields: [...refused.refusedFields].sort(),
|
|
50
52
|
structuredOutput: refused.structuredOutput,
|
|
53
|
+
assistantPrefill: refused.assistantPrefill,
|
|
51
54
|
};
|
|
52
55
|
if (refusedAnything(model))
|
|
53
56
|
models[name] = model;
|
|
@@ -109,6 +112,8 @@ export function importCapabilities(snapshot) {
|
|
|
109
112
|
refused.chosenTemperature = false;
|
|
110
113
|
if (model.structuredOutput === false)
|
|
111
114
|
refused.structuredOutput = false;
|
|
115
|
+
if (model.assistantPrefill === false)
|
|
116
|
+
refused.assistantPrefill = false;
|
|
112
117
|
if (Array.isArray(model.refusedFields)) {
|
|
113
118
|
for (const field of model.refusedFields) {
|
|
114
119
|
if (typeof field === "string")
|
package/dist/stream.d.ts
CHANGED
|
@@ -10,7 +10,16 @@ import { type Fence } from "./thinking.ts";
|
|
|
10
10
|
* hangs the turn until somebody presses stop. `EndpointSilent` and `timeoutMs` were exported for
|
|
11
11
|
* this loop long before the loop itself was.
|
|
12
12
|
*/
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* What a turn cost, and how it went. Zero throughout the four counts means the server did not say.
|
|
15
|
+
*
|
|
16
|
+
* The four counts are always there, as they always were. Everything after them is absent rather
|
|
17
|
+
* than zero when nothing reported or measured it, because a zero cache hit and a server that says
|
|
18
|
+
* nothing about its cache are different findings, and a consumer drawing one as the other is
|
|
19
|
+
* explaining a slow turn with a number nobody sent. Which layer fills each is said on the field:
|
|
20
|
+
* `streamTurn` reads what the endpoint sends, `runTurn` measures the attempts, and
|
|
21
|
+
* `runAgentLoop` compares one request with the one before it.
|
|
22
|
+
*/
|
|
14
23
|
export interface TurnUsage {
|
|
15
24
|
prompt: number;
|
|
16
25
|
completion: number;
|
|
@@ -20,9 +29,72 @@ export interface TurnUsage {
|
|
|
20
29
|
*
|
|
21
30
|
* The only way a caller can tell whether the prefix it is careful to keep still is actually
|
|
22
31
|
* being reused: a prefix that stops hitting the cache otherwise shows up as a bill and nothing
|
|
23
|
-
* else. Zero is also what a server that does not report it sends
|
|
32
|
+
* else. Zero is also what a server that does not report it sends; `uncached` is the field whose
|
|
33
|
+
* presence says the count was reported. llama.cpp's `timings.cache_n` stands in for it where the
|
|
34
|
+
* usage block has none.
|
|
24
35
|
*/
|
|
25
36
|
cached: number;
|
|
37
|
+
/**
|
|
38
|
+
* `prompt` less `cached`: the tokens actually prefilled, and the number a cache miss moves.
|
|
39
|
+
* Present only where both a cache count and the prompt were reported, so its absence is how a
|
|
40
|
+
* zero `cached` reads as unknown.
|
|
41
|
+
*/
|
|
42
|
+
uncached?: number;
|
|
43
|
+
/** How much of `completion` was reasoning, where the usage block breaks it out. Never estimated. */
|
|
44
|
+
reasoningTokens?: number;
|
|
45
|
+
/** Prefill time, from llama.cpp's `timings`. On a local box it dominates once the cache misses. */
|
|
46
|
+
promptMs?: number;
|
|
47
|
+
/** Prefill speed, from the same `timings`. */
|
|
48
|
+
promptTokensPerSecond?: number;
|
|
49
|
+
/** Decode time, from the same `timings`. */
|
|
50
|
+
predictedMs?: number;
|
|
51
|
+
/** Decode speed, from the same `timings`. */
|
|
52
|
+
tokensPerSecond?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Tokens a draft model or MTP head proposed, from the same `timings`. Against `draftAccepted`,
|
|
55
|
+
* whether speculative decoding is paying for itself.
|
|
56
|
+
*/
|
|
57
|
+
draftTotal?: number;
|
|
58
|
+
/** How many of `draftTotal` the target model kept. */
|
|
59
|
+
draftAccepted?: number;
|
|
60
|
+
/**
|
|
61
|
+
* From sending the request to the first chunk that carried anything, measured by `streamTurn`.
|
|
62
|
+
* The latency a watcher feels, and absent on a turn that said nothing at all.
|
|
63
|
+
*/
|
|
64
|
+
firstTokenMs?: number;
|
|
65
|
+
/** The whole turn as `runTurn` saw it, queue, retries, downgrades and loading wait included. */
|
|
66
|
+
wallMs?: number;
|
|
67
|
+
/** How many times `runTurn` sent a lost request again. A downgrade is not a retry. */
|
|
68
|
+
retries?: number;
|
|
69
|
+
/** How many of those were the endpoint going silent, rather than refusing or dropping it. */
|
|
70
|
+
timeouts?: number;
|
|
71
|
+
/** How many requests `continueTurn` joined onto the first. Absent on a turn not continued. */
|
|
72
|
+
continuations?: number;
|
|
73
|
+
/**
|
|
74
|
+
* What this request should have reused of the one before it in the loop: that request's prompt
|
|
75
|
+
* and its reply, which is what a cache holds once the reply is generated. Filled by
|
|
76
|
+
* `runAgentLoop` from the second request on, where the previous prompt was reported.
|
|
77
|
+
*/
|
|
78
|
+
cacheExpected?: number;
|
|
79
|
+
/**
|
|
80
|
+
* `cached` came in under nine tenths of the previous request's prompt — the part of
|
|
81
|
+
* `cacheExpected` no re-rendering of the reply can disturb. Present only where both sides were
|
|
82
|
+
* reported, so a server that says nothing about its cache is never accused of losing it.
|
|
83
|
+
*/
|
|
84
|
+
cacheBroken?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* What the loop changed that would explain `cacheBroken`, the earliest in the prompt first,
|
|
87
|
+
* since a change there is the one that costs the rest. `none-known` is the server's doing — an
|
|
88
|
+
* eviction, another client on the slot — or a change the loop cannot see. Only on a broken turn.
|
|
89
|
+
*/
|
|
90
|
+
cacheBreakReason?: "tools-changed" | "system-changed" | "history-rewritten" | "none-known";
|
|
91
|
+
/** How many tools the request declared, filled by `runAgentLoop`. */
|
|
92
|
+
toolsDeclared?: number;
|
|
93
|
+
/**
|
|
94
|
+
* The declared tool block's estimated tokens. A chat template renders it ahead of the system
|
|
95
|
+
* prompt, so this is what every change to the tool list costs the cache.
|
|
96
|
+
*/
|
|
97
|
+
toolSchemaTokens?: number;
|
|
26
98
|
}
|
|
27
99
|
/** One streamed turn, put back together into the shape a loop and a transcript work with. */
|
|
28
100
|
export interface Turn {
|