@cubicecho/agent-core 2.10.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/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
- /** What a turn cost. Zero throughout means the server did not say. */
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 {
package/dist/stream.js CHANGED
@@ -1,5 +1,18 @@
1
1
  import { EndpointSilent } from "./retry.js";
2
2
  import { DEFAULT_FENCES, FenceSplitter } from "./thinking.js";
3
+ /**
4
+ * The fields of `timings` a turn reports, under the names `TurnUsage` gives them. A value that is
5
+ * not a finite number is left out rather than guessed at.
6
+ */
7
+ const TIMINGS = [
8
+ ["prompt_ms", "promptMs"],
9
+ ["prompt_per_second", "promptTokensPerSecond"],
10
+ ["predicted_ms", "predictedMs"],
11
+ ["predicted_per_second", "tokensPerSecond"],
12
+ ["draft_n", "draftTotal"],
13
+ ["draft_n_accepted", "draftAccepted"],
14
+ ];
15
+ const isCount = (value) => typeof value === "number" && Number.isFinite(value);
3
16
  /** The largest delay a timer takes, which is as close to none as the SDK's timeout option goes. */
4
17
  const NO_SDK_TIMEOUT = 2 ** 31 - 1;
5
18
  /**
@@ -40,7 +53,8 @@ export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, p
40
53
  };
41
54
  try {
42
55
  rearm(false);
43
- return await collect();
56
+ const started = Date.now();
57
+ return await collect(started);
44
58
  }
45
59
  catch (error) {
46
60
  // The caller's own stop has to stay distinguishable from ours: one is a run that was called
@@ -57,7 +71,7 @@ export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, p
57
71
  finally {
58
72
  clearTimeout(idle);
59
73
  }
60
- async function collect() {
74
+ async function collect(started) {
61
75
  // The SDK's own timer runs until the headers arrive, which for a stream is the end of
62
76
  // prefill, and it was set from the idle number. Where a watchdog is armed it covers that
63
77
  // wait already, with the allowance meant for it, so the SDK is told to leave it alone.
@@ -78,6 +92,10 @@ export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, p
78
92
  // its place in the order they arrived.
79
93
  const calls = [];
80
94
  const usage = { prompt: 0, completion: 0, total: 0, cached: 0 };
95
+ // The usage block's own cache count wins over llama.cpp's, which is the same number in the
96
+ // servers that send both; kept apart so the order the two arrive in does not decide.
97
+ let cacheReport;
98
+ let cacheTimings;
81
99
  let finishReason = "";
82
100
  for await (const chunk of stream) {
83
101
  // Rearmed on every chunk, latched below on only some: a priming chunk is the endpoint
@@ -93,8 +111,22 @@ export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, p
93
111
  usage.completion = chunk.usage.completion_tokens ?? 0;
94
112
  usage.total = chunk.usage.total_tokens ?? 0;
95
113
  const reported = chunk.usage;
96
- usage.cached =
97
- reported.prompt_tokens_details?.cached_tokens ?? reported.prompt_cache_hit_tokens ?? 0;
114
+ const hit = reported.prompt_tokens_details?.cached_tokens ?? reported.prompt_cache_hit_tokens;
115
+ if (isCount(hit))
116
+ cacheReport = hit;
117
+ const reasoned = reported.completion_tokens_details?.reasoning_tokens;
118
+ if (isCount(reasoned))
119
+ usage.reasoningTokens = reasoned;
120
+ }
121
+ const { timings } = chunk;
122
+ if (timings) {
123
+ for (const [from, to] of TIMINGS) {
124
+ const value = timings[from];
125
+ if (isCount(value))
126
+ usage[to] = value;
127
+ }
128
+ if (isCount(timings.cache_n))
129
+ cacheTimings = timings.cache_n;
98
130
  }
99
131
  // One choice, because that is what an agent loop asks for. A body with `n` above one
100
132
  // keeps only the first; nothing here is built to reassemble several at once.
@@ -116,8 +148,10 @@ export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, p
116
148
  // has accumulated, and losing a retry is the safer half of that trade. Set before the
117
149
  // callbacks, so a watcher that throws mid-token cannot be told the same token twice.
118
150
  const carried = Boolean(thinking || delta.content || delta.tool_calls?.length);
119
- if (carried && !talking)
151
+ if (carried && !talking) {
120
152
  rearm(true);
153
+ usage.firstTokenMs = Date.now() - started;
154
+ }
121
155
  if (produced && carried)
122
156
  produced.any = true;
123
157
  if (thinking) {
@@ -174,6 +208,12 @@ export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, p
174
208
  // cut off at the ceiling mid-scratchpad, it has no answer, and promoting the deliberation to
175
209
  // one is how a truncated turn gets recorded as output.
176
210
  report(splitter.finish());
211
+ const cached = cacheReport ?? cacheTimings;
212
+ if (cached !== undefined) {
213
+ usage.cached = cached;
214
+ if (usage.prompt > 0)
215
+ usage.uncached = Math.max(0, usage.prompt - cached);
216
+ }
177
217
  const minted = new Set();
178
218
  return {
179
219
  content: splitter.output,
package/llms.txt CHANGED
@@ -24,6 +24,14 @@ The loop above a turn: send, run the tools the model asked for, send again, unti
24
24
  - `ToolCallOutcome` (type) — What one tool call did, in the order the model asked.
25
25
  - `ToolCallRequest` (type) — One call the model made, as `dispatch` is handed it.
26
26
 
27
+ ### calibration
28
+
29
+ How many characters a token is worth on one model, learned from what its endpoint reports.
30
+
31
+ - `calibrate` — Takes one reading from a request that was answered, so the next one to this model is sized by it.
32
+ - `charsPerTokenFor` — The characters per token to size a request to this model with, `CHARS_PER_TOKEN` until a turn has reported one.
33
+ - `resetCalibration` — Forgets every reading, so the next request is sized at `CHARS_PER_TOKEN` again.
34
+
27
35
  ### capabilities
28
36
 
29
37
  What an endpoint turned out not to support, and answering it when it says so.
@@ -86,6 +94,14 @@ What this package needs to know about a caller's configuration.
86
94
  - `RetryPolicy` (type) — How many times a lost or refused request is worth sending again.
87
95
  - `ToolPolicy` (type) — How tools reach the model, and how long it may keep calling them.
88
96
 
97
+ ### continuation
98
+
99
+ Picking up an answer the token ceiling cut off, instead of keeping half of it.
100
+
101
+ - `ContinueTurnOptions` (type) — What `continueTurn` takes besides what `runTurn` does.
102
+ - `continueTurn` — Carries on an answer the token ceiling cut off, by sending the transcript again with the answer so far as a trailing assistant message, and joins the pieces into one turn.
103
+ - `isContinuable` — Whether a turn is one a continuation can finish: cut off at the ceiling, with an answer begun and no tool call in it.
104
+
89
105
  ### errors
90
106
 
91
107
  - `errorMessage` — What went wrong, as a string.
@@ -103,8 +119,12 @@ What a run is doing, while it is doing it.
103
119
  - `RunEvent` (type) — One thing that happened in a run, as a watcher receives it.
104
120
  - `RunEventInput` (type) — What `emit` is given: the run and the sequence are the bus's to assign.
105
121
  - `RunEventKind` (type) — Which kind of thing happened, and what `text`, `name`, `ok` and `usage` carry for it.
122
+ - `RunMetrics` (type) — A run summed and derived from its events: what it cost, where the time went, and why.
123
+ - `RunMetricsOptions` (type) — What `runMetrics` takes besides the events.
106
124
  - `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.
107
125
  - `resetEvents` — Test seam: forget every run, so one test's events cannot be read by the next.
126
+ - `runMetrics` — A run's totals, timings and cache findings, derived from the events it emitted.
127
+ - `TurnReport` (type) — One turn's usage and measurements as a `usage` event carries them.
108
128
  - `watch` — Everything that has happened on a run, then everything that happens next, until it ends.
109
129
 
110
130
  ### hooks
@@ -113,6 +133,7 @@ Lifecycle hooks, from the host's side: what a session looks like to them, where
113
133
 
114
134
  - `assembleContext` — Builds the context a set of outcomes adds and the notes that go with it.
115
135
  - `configureHooks` — Changes what hooks are held to, for a process whose windows are not the size these defaults were chosen for, or whose host wants its own name above the context.
136
+ - `consult` — Runs an event's hooks and waits for their say, for a host that will hold off when one of them vetoes.
116
137
  - `Gathered` (type) — The context a set of outcomes adds to a request, and a note for each hook worth mentioning.
117
138
  - `gather` — Runs the hooks ahead of a request and builds what they add to it.
118
139
  - `HOOK_CONTEXT_TOKENS` — The most context all of a request's hooks add between them by default, in estimated tokens.
@@ -143,6 +164,7 @@ Lifecycle hooks, from the host's side: what a session looks like to them, where
143
164
  Everything about a request failing that is not about what the request said.
144
165
 
145
166
  - `backoffMs` — Exponential, with jitter so several tasks failing at once do not return in lockstep.
167
+ - `CHARS_PER_TOKEN` — The divisor behind `estimateTokens`, applied here to a character count rather than a string.
146
168
  - `ContextOverflow` — The request was bigger than the model will read.
147
169
  - `compact` — 1234 → "1.2k".
148
170
  - `EndpointSilent` — The endpoint stopped answering mid-request.
@@ -152,9 +174,12 @@ Everything about a request failing that is not about what the request said.
152
174
  - `LOADING_POLL_MS` — How long to wait between asking a loading server again.
153
175
  - `LOADING_TIMEOUT_MS` — How long `runTurn` waits for a model to load unless told otherwise.
154
176
  - `messageTokens` — One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
177
+ - `requestChars` — How many characters a request is worth: the walk `requestTokens` divides, without the division.
155
178
  - `requestTokens` — What this request will cost the window, in tokens, near enough.
156
179
  - `SMALLEST_LIKELY_WINDOW` — The smallest window worth believing in, and the floor under `runTurn`'s guard.
157
180
  - `sleep` — A delay an abort cuts short, rejecting rather than resolving early.
181
+ - `TokenEstimateOptions` (type) — What the two token estimates below take besides what they measure.
182
+ - `toolsChars` — How many characters a tool array is worth, measured once per array.
158
183
 
159
184
  ### run-turn
160
185
 
@@ -182,6 +207,7 @@ One-shot calls that support a run without being one: picking tools, naming a ses
182
207
  - `listLines` — A list-shaped reply, one item per line, cleaned of the bullets and quotes models decorate them with.
183
208
  - `parseJson` — Models are asked for JSON and often answer with prose around it, or a fenced block.
184
209
  - `resetHints` — Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints.
210
+ - `SideTaskInput` (type) — The input a side task applies its instruction to: text, or the content parts a vision model reads.
185
211
  - `SideTaskOptions` (type) — What a side task may be given.
186
212
  - `tryAsk` — A side task is never worth failing the work it supports.
187
213
 
@@ -204,7 +230,7 @@ Reading one streamed turn back into a message.
204
230
  - `StreamTurnOptions` (type) — What `streamTurn` takes besides the request body.
205
231
  - `streamTurn` — Runs one turn as a stream, reporting tokens as they arrive and assembling them back into a message.
206
232
  - `Turn` (type) — One streamed turn, put back together into the shape a loop and a transcript work with.
207
- - `TurnUsage` (type) — What a turn cost.
233
+ - `TurnUsage` (type) — What a turn cost, and how it went.
208
234
 
209
235
  ### thinking
210
236
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
5
5
  "keywords": [
6
6
  "openai",