@cubicecho/agent-core 4.8.0 → 4.9.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 +24 -4
- package/dist/core/config.d.ts +1 -1
- package/dist/endpoint/client.d.ts +16 -0
- package/dist/endpoint/client.js +72 -2
- package/dist/endpoint/slots.d.ts +54 -0
- package/dist/endpoint/slots.js +88 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/run/agent-loop.d.ts +13 -0
- package/dist/run/agent-loop.js +65 -6
- package/dist/run/events.d.ts +5 -0
- package/dist/run/events.js +3 -0
- package/dist/runtime/reset.d.ts +6 -5
- package/dist/runtime/reset.js +8 -5
- package/dist/runtime/runtime.d.ts +5 -1
- package/dist/runtime/runtime.js +5 -1
- package/dist/wire/stream.d.ts +12 -1
- package/dist/wire/stream.js +2 -1
- package/llms.txt +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -867,10 +867,23 @@ again without it, with a notice naming it. A nested name is dropped at its top-l
|
|
|
867
867
|
refused field nobody said was droppable is passed on, since dropping it would change the request
|
|
868
868
|
behind the caller's back.
|
|
869
869
|
|
|
870
|
-
On a llama.cpp server started with `--parallel`,
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
870
|
+
On a llama.cpp server started with `--parallel`, give `runAgentLoop` a `session` — a run id, a
|
|
871
|
+
conversation id — and the run is pinned to a slot of its own. `servedSlots` reads `total_slots` off
|
|
872
|
+
`/props` (the request `servedWindow` already makes), `slotAllocator` hands each session a free
|
|
873
|
+
slot, the one it last held where that is still free, and every request of the run carries
|
|
874
|
+
`id_slot` and `cache_prompt: true` until the loop returns or throws. The slot keeps that session's
|
|
875
|
+
KV cache warm against every other run on the server, which is the difference between a cached
|
|
876
|
+
prefill on every turn and a full one — but only while the prefix stays the same from turn to turn;
|
|
877
|
+
see #63, and the caution on compaction below.
|
|
878
|
+
|
|
879
|
+
More sessions than slots run unpinned rather than wait, and so does a second loop under a key
|
|
880
|
+
that holds a slot already. A notice says `pinned to slot 0 of 4`, or that no slot was free, and
|
|
881
|
+
each turn of a session run on a server with slots reports `pinned`, which `runMetrics` counts as `unpinnedTurns`
|
|
882
|
+
beside `cacheHitRatio`. A server that refuses `id_slot` is latched off it like any other
|
|
883
|
+
`extraBody` field, and an `extraBody` that names `id_slot` itself is the host pinning by hand and
|
|
884
|
+
is left alone. On a server without `total_slots` none of this happens. Whatever the server, the
|
|
885
|
+
session also goes out as an `X-Session-Id` header (`SESSION_HEADER`), which llm-router's sticky
|
|
886
|
+
affinity reads first, so a run behind a router keeps both its provider and its slot.
|
|
874
887
|
|
|
875
888
|
Ollama's `options` object is not read on its OpenAI-compatible `/v1` route, so sampling set there
|
|
876
889
|
does nothing; send the fields at the top level.
|
|
@@ -1379,6 +1392,13 @@ OpenRouter `:free` suffix, a typo) would otherwise fetch the listing on every ca
|
|
|
1379
1392
|
same zero each time. Half a minute later it asks again, so a model pulled onto a box that has been
|
|
1380
1393
|
up a week is still picked up without a restart.
|
|
1381
1394
|
|
|
1395
|
+
Which session holds which llama.cpp slot is kept as well, keyed on the base URL and the model but
|
|
1396
|
+
not the key: two keys reaching one server under two allocators would hand one slot to two runs,
|
|
1397
|
+
which is the collision the allocator is there to stop. It is bounded by the slots the server
|
|
1398
|
+
reported, and the slot count itself is asked for again every half minute, so a server restarted
|
|
1399
|
+
with another `--parallel` is not sent a slot it no longer has. `resetSlots` clears it, and
|
|
1400
|
+
`resetAll` with the rest.
|
|
1401
|
+
|
|
1382
1402
|
Both numbers are defaults, as is the ten seconds a window lookup is given
|
|
1383
1403
|
(`lookupTimeoutMs`). `configureClients` moves them for the process, the way `configureEvents`
|
|
1384
1404
|
and `configureHooks` do — a multi-tenant host raising the pool so tenants stop evicting each other,
|
package/dist/core/config.d.ts
CHANGED
|
@@ -89,7 +89,7 @@ export interface ModelParams {
|
|
|
89
89
|
* What a model card asks for and nothing here names: `top_k`, `min_p`, `repeat_penalty` —
|
|
90
90
|
* what actually stops a small model looping — and a server's own fields, such as llama.cpp's
|
|
91
91
|
* `id_slot`, which pins a session to one slot of a `--parallel` server so its KV cache is
|
|
92
|
-
* still warm on the next turn. A local server ignores a field it does not know; OpenAI refuses
|
|
92
|
+
* still warm on the next turn — `runAgentLoop`'s `session` picks one itself. A local server ignores a field it does not know; OpenAI refuses
|
|
93
93
|
* one by name, and `negotiate` drops that name for the model and sends the request again.
|
|
94
94
|
* `model`, `messages`, `stream` and `tools` are the loop's and are not overridden from here.
|
|
95
95
|
* Ollama's `options` object is not read on its `/v1` route, so sampling there has to go in as
|
|
@@ -181,6 +181,22 @@ export interface LookupOptions {
|
|
|
181
181
|
export declare function servedWindow(config: Endpoint & {
|
|
182
182
|
model: string;
|
|
183
183
|
}, { signal }?: LookupOptions): Promise<number>;
|
|
184
|
+
/**
|
|
185
|
+
* How many slots a llama.cpp server runs a model in, for pinning a session to one of them.
|
|
186
|
+
*
|
|
187
|
+
* @param config - The endpoint, plus the model the requests will name.
|
|
188
|
+
* @param [options] - Cancellation. See `LookupOptions`.
|
|
189
|
+
* @returns `total_slots` from `/props`, or zero where the server has no such route or did not say.
|
|
190
|
+
* Rejects only with the reason of `options.signal`: a server that could not be reached answers zero.
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* The same request `servedWindow` makes, so a run that has sized its window has the count without
|
|
194
|
+
* a second one. A count stands for `listingMissMs`, and a server without `/props` is not asked again
|
|
195
|
+
* until `resetClients`. Zero is what every server but llama.cpp answers, and leaves a run unpinned.
|
|
196
|
+
*/
|
|
197
|
+
export declare function servedSlots(config: Endpoint & {
|
|
198
|
+
model: string;
|
|
199
|
+
}, { signal }?: LookupOptions): Promise<number>;
|
|
184
200
|
/**
|
|
185
201
|
* Asks an endpoint what it serves, and remembers the answer.
|
|
186
202
|
*
|
package/dist/endpoint/client.js
CHANGED
|
@@ -322,6 +322,32 @@ export const endpointId = (config) => digestOf(endpointKey(config));
|
|
|
322
322
|
* is asked again after `listingMissMs`, like a listing that did not name the model.
|
|
323
323
|
*/
|
|
324
324
|
const served = scoped(() => new Map());
|
|
325
|
+
/**
|
|
326
|
+
* Slot counts read off llama.cpp's `/props`, keyed on endpoint and model as `served` is.
|
|
327
|
+
*
|
|
328
|
+
* @remarks
|
|
329
|
+
* `gone` is a server without the route, which is not asked again until `resetClients`. Any other
|
|
330
|
+
* answer, a count included, stands for `listingMissMs`: a server restarted with another
|
|
331
|
+
* `--parallel` must not be sent a slot it no longer has for the life of the process.
|
|
332
|
+
*/
|
|
333
|
+
const slotCounts = scoped(() => new Map());
|
|
334
|
+
/**
|
|
335
|
+
* Remembers what a `/props` answer said of its slots.
|
|
336
|
+
*
|
|
337
|
+
* @param key - The endpoint and model, by `modelKey`.
|
|
338
|
+
* @param props - How `/props` answered, as `probe` hands it over.
|
|
339
|
+
*/
|
|
340
|
+
function noteSlots(key, props) {
|
|
341
|
+
const { total_slots } = (props.body ?? {});
|
|
342
|
+
slotCounts().set(key, { slots: positive(total_slots), at: Date.now(), gone: props.missing });
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The path `/props` is asked on, naming the model for a llama.cpp router serving several.
|
|
346
|
+
*
|
|
347
|
+
* @param model - The model's name as the endpoint knows it.
|
|
348
|
+
* @returns The path and its query. A single-model server ignores the query.
|
|
349
|
+
*/
|
|
350
|
+
const propsPath = (model) => `/props?model=${encodeURIComponent(model)}`;
|
|
325
351
|
/** Endpoints with neither native route, which are not asked for either again. */
|
|
326
352
|
const unserved = scoped(() => new Set());
|
|
327
353
|
/**
|
|
@@ -452,8 +478,8 @@ async function readServed(config, within) {
|
|
|
452
478
|
}
|
|
453
479
|
let window = 0;
|
|
454
480
|
try {
|
|
455
|
-
|
|
456
|
-
|
|
481
|
+
const props = await probe(config, propsPath(config.model), within);
|
|
482
|
+
noteSlots(key, props);
|
|
457
483
|
const settings = props.body?.default_generation_settings;
|
|
458
484
|
window = positive(settings?.n_ctx);
|
|
459
485
|
if (!window) {
|
|
@@ -483,6 +509,49 @@ async function readServed(config, within) {
|
|
|
483
509
|
served().set(key, { window, at: Date.now() });
|
|
484
510
|
return window;
|
|
485
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* How many slots a llama.cpp server runs a model in, for pinning a session to one of them.
|
|
514
|
+
*
|
|
515
|
+
* @param config - The endpoint, plus the model the requests will name.
|
|
516
|
+
* @param [options] - Cancellation. See `LookupOptions`.
|
|
517
|
+
* @returns `total_slots` from `/props`, or zero where the server has no such route or did not say.
|
|
518
|
+
* Rejects only with the reason of `options.signal`: a server that could not be reached answers zero.
|
|
519
|
+
*
|
|
520
|
+
* @remarks
|
|
521
|
+
* The same request `servedWindow` makes, so a run that has sized its window has the count without
|
|
522
|
+
* a second one. A count stands for `listingMissMs`, and a server without `/props` is not asked again
|
|
523
|
+
* until `resetClients`. Zero is what every server but llama.cpp answers, and leaves a run unpinned.
|
|
524
|
+
*/
|
|
525
|
+
export async function servedSlots(config, { signal } = {}) {
|
|
526
|
+
const slots = await readSlots(config, lookupSignal(config, signal));
|
|
527
|
+
signal?.throwIfAborted();
|
|
528
|
+
return slots;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* `servedSlots` under a lookup's signal.
|
|
532
|
+
*
|
|
533
|
+
* @param config - The endpoint, plus the model the requests will name.
|
|
534
|
+
* @param within - The lookup's signal. Fired, the answer is zero and nothing is remembered.
|
|
535
|
+
* @returns The slot count, or zero. Never rejects.
|
|
536
|
+
*/
|
|
537
|
+
async function readSlots(config, within) {
|
|
538
|
+
const endpoint = endpointKey(config);
|
|
539
|
+
if (unserved().has(endpoint)) {
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
const key = modelKey(endpoint, config.model);
|
|
543
|
+
const known = slotCounts().get(key);
|
|
544
|
+
if (known && (known.gone || Date.now() - known.at < poolLimits().listingMissMs)) {
|
|
545
|
+
return known.slots;
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
noteSlots(key, await probe(config, propsPath(config.model), within));
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
return slotCounts().get(key)?.slots ?? 0;
|
|
554
|
+
}
|
|
486
555
|
/**
|
|
487
556
|
* Asks an endpoint what it serves, and remembers the answer.
|
|
488
557
|
*
|
|
@@ -618,6 +687,7 @@ export function resetClients() {
|
|
|
618
687
|
misses().clear();
|
|
619
688
|
trainedOnly().clear();
|
|
620
689
|
served().clear();
|
|
690
|
+
slotCounts().clear();
|
|
621
691
|
unserved().clear();
|
|
622
692
|
refusals().clear();
|
|
623
693
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which slot of a multi-slot llama.cpp server each session is on, so concurrent runs stop
|
|
3
|
+
* evicting each other's prompt cache.
|
|
4
|
+
*
|
|
5
|
+
* `llama-server --parallel N` keeps one KV cache per slot, and a turn that lands on a slot holding
|
|
6
|
+
* another run's prefix is prefilled again from the top. The server's own choice is a guess by
|
|
7
|
+
* prompt similarity; this hands each session a slot of its own for as long as it holds it, and
|
|
8
|
+
* the same one again next time where it is still free.
|
|
9
|
+
*/
|
|
10
|
+
/** The request field that names a llama.cpp slot. */
|
|
11
|
+
export declare const SLOT_FIELD: 'id_slot';
|
|
12
|
+
/** The request field that asks llama.cpp to reuse the slot's cached prefix. On by default there. */
|
|
13
|
+
export declare const CACHE_PROMPT_FIELD: 'cache_prompt';
|
|
14
|
+
/** The header llm-router's sticky affinity reads first, which keeps a session on one provider. */
|
|
15
|
+
export declare const SESSION_HEADER: 'X-Session-Id';
|
|
16
|
+
/** Hands one endpoint's slots to sessions. See `slotAllocator`. */
|
|
17
|
+
export interface SlotAllocator {
|
|
18
|
+
/** How many slots the endpoint said it has. */
|
|
19
|
+
readonly total: number;
|
|
20
|
+
/**
|
|
21
|
+
* Gives a session a slot for as long as it holds it.
|
|
22
|
+
*
|
|
23
|
+
* @param session - The run's own key: a run id, a conversation id.
|
|
24
|
+
* @returns The slot, from zero: the one this session last held where it is free, else a free
|
|
25
|
+
* one. `undefined` where every slot is held, or this session holds one already.
|
|
26
|
+
*/
|
|
27
|
+
acquire(session: string): number | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Frees the slot a session holds. A session holding none is a no-op.
|
|
30
|
+
*
|
|
31
|
+
* @param session - The key it was acquired under.
|
|
32
|
+
*/
|
|
33
|
+
release(session: string): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The slot allocator for one endpoint and model, sized to what its `/props` said.
|
|
37
|
+
*
|
|
38
|
+
* @param endpoint - The server, read for `baseUrl` alone.
|
|
39
|
+
* @param model - The model the requests name.
|
|
40
|
+
* @param total - The slots it has, as `servedSlots` reads them. Zero or less leaves none to hand out.
|
|
41
|
+
* @returns The live allocator: two calls for one endpoint and model share every slot. Slots past a
|
|
42
|
+
* smaller `total` than last time are dropped, and a holder of one releases into nothing.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* A free slot is chosen in this order: the one the session last held, then one nobody has held,
|
|
46
|
+
* then the one released longest ago, so a session that has just finished keeps its cache for its
|
|
47
|
+
* next step as long as a free slot can be found elsewhere. More sessions than slots get no pin and
|
|
48
|
+
* are not queued; llama.cpp places them by its own guess, which is what every run had before.
|
|
49
|
+
*/
|
|
50
|
+
export declare function slotAllocator(endpoint: {
|
|
51
|
+
baseUrl: string;
|
|
52
|
+
}, model: string, total: number): SlotAllocator;
|
|
53
|
+
/** Forgets every endpoint's slots, held ones included. For tests, and `resetAll`. */
|
|
54
|
+
export declare function resetSlots(): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { scoped } from '../core/scope.js';
|
|
2
|
+
import { modelKey } from './client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Which slot of a multi-slot llama.cpp server each session is on, so concurrent runs stop
|
|
5
|
+
* evicting each other's prompt cache.
|
|
6
|
+
*
|
|
7
|
+
* `llama-server --parallel N` keeps one KV cache per slot, and a turn that lands on a slot holding
|
|
8
|
+
* another run's prefix is prefilled again from the top. The server's own choice is a guess by
|
|
9
|
+
* prompt similarity; this hands each session a slot of its own for as long as it holds it, and
|
|
10
|
+
* the same one again next time where it is still free.
|
|
11
|
+
*/
|
|
12
|
+
/** The request field that names a llama.cpp slot. */
|
|
13
|
+
export const SLOT_FIELD = 'id_slot';
|
|
14
|
+
/** The request field that asks llama.cpp to reuse the slot's cached prefix. On by default there. */
|
|
15
|
+
export const CACHE_PROMPT_FIELD = 'cache_prompt';
|
|
16
|
+
/** The header llm-router's sticky affinity reads first, which keeps a session on one provider. */
|
|
17
|
+
export const SESSION_HEADER = 'X-Session-Id';
|
|
18
|
+
/** Each endpoint and model's slots, and the counter releases are ordered by. */
|
|
19
|
+
const pools = scoped(() => ({ slots: new Map(), clock: 0 }));
|
|
20
|
+
/**
|
|
21
|
+
* The slots one endpoint has, as the allocator keeps them.
|
|
22
|
+
*
|
|
23
|
+
* @param endpoint - The server, read for `baseUrl` alone.
|
|
24
|
+
* @param model - The model, which a llama.cpp router serves from a process with slots of its own.
|
|
25
|
+
* @returns The key: the base URL less its trailing slashes, and the model.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* The API key is left out where every other per-endpoint cache keys on it. Two keys reaching
|
|
29
|
+
* different servers through one URL cost only a slot left unused; two keys reaching the same
|
|
30
|
+
* server under two allocators hand one slot to two runs, which is the collision this exists to stop.
|
|
31
|
+
*/
|
|
32
|
+
const poolKey = (endpoint, model) => modelKey(endpoint.baseUrl.trim().replace(/\/+$/, ''), model);
|
|
33
|
+
/**
|
|
34
|
+
* The slot allocator for one endpoint and model, sized to what its `/props` said.
|
|
35
|
+
*
|
|
36
|
+
* @param endpoint - The server, read for `baseUrl` alone.
|
|
37
|
+
* @param model - The model the requests name.
|
|
38
|
+
* @param total - The slots it has, as `servedSlots` reads them. Zero or less leaves none to hand out.
|
|
39
|
+
* @returns The live allocator: two calls for one endpoint and model share every slot. Slots past a
|
|
40
|
+
* smaller `total` than last time are dropped, and a holder of one releases into nothing.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* A free slot is chosen in this order: the one the session last held, then one nobody has held,
|
|
44
|
+
* then the one released longest ago, so a session that has just finished keeps its cache for its
|
|
45
|
+
* next step as long as a free slot can be found elsewhere. More sessions than slots get no pin and
|
|
46
|
+
* are not queued; llama.cpp places them by its own guess, which is what every run had before.
|
|
47
|
+
*/
|
|
48
|
+
export function slotAllocator(endpoint, model, total) {
|
|
49
|
+
const key = poolKey(endpoint, model);
|
|
50
|
+
const pool = pools();
|
|
51
|
+
const size = Math.max(0, Math.floor(total));
|
|
52
|
+
const known = pool.slots.get(key) ?? [];
|
|
53
|
+
// Resized onto the same slot objects, so an allocator handed out before the resize still
|
|
54
|
+
// releases what it acquired.
|
|
55
|
+
const slots = known.length === size ? known : Array.from({ length: size }, (_, index) => known[index] ?? { released: 0 });
|
|
56
|
+
pool.slots.set(key, slots);
|
|
57
|
+
const acquire = (session) => {
|
|
58
|
+
const holdsOne = slots.some((slot) => slot.holder === session);
|
|
59
|
+
if (holdsOne) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const free = slots.map((slot, index) => ({ slot, index })).filter(({ slot }) => slot.holder === undefined);
|
|
63
|
+
const own = free.find(({ slot }) => slot.last === session);
|
|
64
|
+
const unused = free.find(({ slot }) => slot.last === undefined);
|
|
65
|
+
const oldest = free.reduce((best, entry) => (best === undefined || entry.slot.released < best.slot.released ? entry : best), undefined);
|
|
66
|
+
const chosen = own ?? unused ?? oldest;
|
|
67
|
+
if (!chosen) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
chosen.slot.holder = session;
|
|
71
|
+
chosen.slot.last = session;
|
|
72
|
+
return chosen.index;
|
|
73
|
+
};
|
|
74
|
+
const release = (session) => {
|
|
75
|
+
const held = slots.find((slot) => slot.holder === session);
|
|
76
|
+
if (!held) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
held.holder = undefined;
|
|
80
|
+
pool.clock++;
|
|
81
|
+
held.released = pool.clock;
|
|
82
|
+
};
|
|
83
|
+
return { total: size, acquire, release };
|
|
84
|
+
}
|
|
85
|
+
/** Forgets every endpoint's slots, held ones included. For tests, and `resetAll`. */
|
|
86
|
+
export function resetSlots() {
|
|
87
|
+
pools.reset();
|
|
88
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,9 @@ export { estimateFrom, type LedgerEntry, type LedgerEstimateOptions, type Ledger
|
|
|
14
14
|
export { type AgentConfig, type Endpoint, type ModelParams, type RetryPolicy, ToolDiscovery, type ToolPolicy, } from './core/config.ts';
|
|
15
15
|
export { calibrate, charsPerTokenFor, resetCalibration } from './endpoint/calibration.ts';
|
|
16
16
|
export { type Capabilities, capabilitiesFor, EFFORT_LADDER, EFFORT_NONE, EFFORT_OFF, effortFor, expireCapabilities, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from './endpoint/capabilities.ts';
|
|
17
|
-
export { type ClientPoolOptions, configureClients, contextLimitFor, endpointId, endpointKey, firstTokenMs, getClient, type LookupOptions, listModels, type ModelInfo, NO_KEY, resetClients, resolveApiKey, servedWindow, timeoutMs, } from './endpoint/client.ts';
|
|
17
|
+
export { type ClientPoolOptions, configureClients, contextLimitFor, endpointId, endpointKey, firstTokenMs, getClient, type LookupOptions, listModels, type ModelInfo, NO_KEY, resetClients, resolveApiKey, servedSlots, servedWindow, timeoutMs, } from './endpoint/client.ts';
|
|
18
18
|
export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from './endpoint/schema-compat.ts';
|
|
19
|
+
export { CACHE_PROMPT_FIELD, resetSlots, SESSION_HEADER, SLOT_FIELD, type SlotAllocator, slotAllocator, } from './endpoint/slots.ts';
|
|
19
20
|
export { HOOK_EVENTS, HookEvent, INJECT_EVENTS } from './hooks/hook-events.ts';
|
|
20
21
|
export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, type HookContext, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from './hooks/hooks.ts';
|
|
21
22
|
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopRequest, type AgentLoopResult, PRESELECT_APPEND, PRESELECT_EXCLUSIVE, PRESELECTED_NOTICE, runAgentLoop, type StepWindow, } from './run/agent-loop.ts';
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,9 @@ export { estimateFrom, rebaseLedger, recordRequest, tokensBetween, } from './con
|
|
|
14
14
|
export { ToolDiscovery, } from './core/config.js';
|
|
15
15
|
export { calibrate, charsPerTokenFor, resetCalibration } from './endpoint/calibration.js';
|
|
16
16
|
export { capabilitiesFor, EFFORT_LADDER, EFFORT_NONE, EFFORT_OFF, effortFor, expireCapabilities, modelCapabilitiesFor, negotiate, resetCapabilities, } from './endpoint/capabilities.js';
|
|
17
|
-
export { configureClients, contextLimitFor, endpointId, endpointKey, firstTokenMs, getClient, listModels, NO_KEY, resetClients, resolveApiKey, servedWindow, timeoutMs, } from './endpoint/client.js';
|
|
17
|
+
export { configureClients, contextLimitFor, endpointId, endpointKey, firstTokenMs, getClient, listModels, NO_KEY, resetClients, resolveApiKey, servedSlots, servedWindow, timeoutMs, } from './endpoint/client.js';
|
|
18
18
|
export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from './endpoint/schema-compat.js';
|
|
19
|
+
export { CACHE_PROMPT_FIELD, resetSlots, SESSION_HEADER, SLOT_FIELD, slotAllocator, } from './endpoint/slots.js';
|
|
19
20
|
export { HOOK_EVENTS, HookEvent, INJECT_EVENTS } from './hooks/hook-events.js';
|
|
20
21
|
export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from './hooks/hooks.js';
|
|
21
22
|
export { PRESELECT_APPEND, PRESELECT_EXCLUSIVE, PRESELECTED_NOTICE, runAgentLoop, } from './run/agent-loop.js';
|
package/dist/run/agent-loop.d.ts
CHANGED
|
@@ -333,6 +333,19 @@ export interface AgentLoopOptions {
|
|
|
333
333
|
ledger?: TokenLedger;
|
|
334
334
|
/** Stops the run: the request in flight, and between steps and calls. */
|
|
335
335
|
signal?: AbortSignal;
|
|
336
|
+
/**
|
|
337
|
+
* The run's own key — a run id, a conversation id — which keeps its prompt cache warm across
|
|
338
|
+
* steps and beside other runs. Every request carries it as `SESSION_HEADER`, for a router's
|
|
339
|
+
* sticky affinity.
|
|
340
|
+
*
|
|
341
|
+
* @remarks
|
|
342
|
+
* On a llama.cpp server that reports `total_slots`, the run also holds one slot from its first
|
|
343
|
+
* request to its return or throw, sent as `id_slot` with `cache_prompt: true`, and a notice says
|
|
344
|
+
* which. The slot is the one this key held last where it is free; with every slot held, or one
|
|
345
|
+
* held by this key already, the run goes unpinned. An `extraBody` naming `id_slot` is the host
|
|
346
|
+
* pinning for itself and is left alone, and a server that refuses the field is latched off it.
|
|
347
|
+
*/
|
|
348
|
+
session?: string;
|
|
336
349
|
/** Told what the run is doing, as the events a watcher reads. */
|
|
337
350
|
onEvent?: (event: RunEventInput) => void;
|
|
338
351
|
/**
|
package/dist/run/agent-loop.js
CHANGED
|
@@ -4,7 +4,8 @@ import { ToolDiscovery } from '../core/config.js';
|
|
|
4
4
|
import { counted } from '../core/guards.js';
|
|
5
5
|
import { charsPerTokenFor } from '../endpoint/calibration.js';
|
|
6
6
|
import { capabilitiesFor, modelCapabilitiesFor, } from '../endpoint/capabilities.js';
|
|
7
|
-
import { firstTokenMs, getClient, loadingMs, timeoutMs } from '../endpoint/client.js';
|
|
7
|
+
import { firstTokenMs, getClient, loadingMs, servedSlots, timeoutMs } from '../endpoint/client.js';
|
|
8
|
+
import { CACHE_PROMPT_FIELD, SESSION_HEADER, SLOT_FIELD, slotAllocator } from '../endpoint/slots.js';
|
|
8
9
|
import { HookEvent } from '../hooks/hook-events.js';
|
|
9
10
|
import { configureHooks, gather, notify, turnIndex, turnMessages, withContext, } from '../hooks/hooks.js';
|
|
10
11
|
import { serverInstructionsPrompt } from '../tools/instructions.js';
|
|
@@ -111,6 +112,7 @@ function cacheDiagnosis(previous, next, usage) {
|
|
|
111
112
|
* run's `temperature` or `maxTokens`.
|
|
112
113
|
*/
|
|
113
114
|
export async function runAgentLoop(options) {
|
|
115
|
+
let pin;
|
|
114
116
|
// Until the steps have set themselves up there is only what the caller handed over.
|
|
115
117
|
const standing = {
|
|
116
118
|
read: () => ({
|
|
@@ -125,7 +127,8 @@ export async function runAgentLoop(options) {
|
|
|
125
127
|
};
|
|
126
128
|
let result;
|
|
127
129
|
try {
|
|
128
|
-
|
|
130
|
+
pin = await pinSession(options);
|
|
131
|
+
result = await runSteps(options, standing, pin);
|
|
129
132
|
}
|
|
130
133
|
catch (error) {
|
|
131
134
|
// Wrapped rather than annotated: an abort's reason is one object shared by every run under
|
|
@@ -135,11 +138,56 @@ export async function runAgentLoop(options) {
|
|
|
135
138
|
? new AgentLoopOverflow(error.message, standing.read(), { cause: error })
|
|
136
139
|
: new AgentLoopError(errorMessage(error), standing.read(), { cause: error });
|
|
137
140
|
}
|
|
141
|
+
finally {
|
|
142
|
+
pin?.release();
|
|
143
|
+
}
|
|
138
144
|
if (result) {
|
|
139
145
|
return result;
|
|
140
146
|
}
|
|
141
147
|
throw new ToolIterationLimit(`Stopped after ${options.config.maxToolIterations} tool iterations.`, standing.read());
|
|
142
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Pins a run to a llama.cpp slot where it has a session and the server has slots to hand out.
|
|
151
|
+
*
|
|
152
|
+
* @param options - The run's options. Read for `session`, the config and the signal; a notice goes
|
|
153
|
+
* to `onEvent`.
|
|
154
|
+
* @returns The pin, unpinned on a server without slots, one that refused `id_slot` or whose slots
|
|
155
|
+
* are all held. `undefined` without a session. Rejects only with the reason of `signal`.
|
|
156
|
+
*/
|
|
157
|
+
async function pinSession(options) {
|
|
158
|
+
const { config, session, signal } = options;
|
|
159
|
+
if (!session) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const headers = { [SESSION_HEADER]: session };
|
|
163
|
+
const unpinned = { fields: {}, headers, slotted: false, release: () => { } };
|
|
164
|
+
const pinsItself = config.extraBody !== undefined && SLOT_FIELD in config.extraBody;
|
|
165
|
+
if (pinsItself) {
|
|
166
|
+
return unpinned;
|
|
167
|
+
}
|
|
168
|
+
const total = await servedSlots(config, { signal });
|
|
169
|
+
if (total === 0) {
|
|
170
|
+
return unpinned;
|
|
171
|
+
}
|
|
172
|
+
const notice = (text) => options.onEvent?.({ kind: RunEventKind.Notice, text });
|
|
173
|
+
const refused = modelCapabilitiesFor(capabilitiesFor(config.baseUrl, config.apiKey), config.model);
|
|
174
|
+
if (refused.refusedFields.has(SLOT_FIELD)) {
|
|
175
|
+
return { ...unpinned, slotted: true };
|
|
176
|
+
}
|
|
177
|
+
const slots = slotAllocator(config, config.model, total);
|
|
178
|
+
const slot = slots.acquire(session);
|
|
179
|
+
if (slot === undefined) {
|
|
180
|
+
notice(`no free slot of ${total} for this session; running unpinned`);
|
|
181
|
+
return { ...unpinned, slotted: true };
|
|
182
|
+
}
|
|
183
|
+
notice(`pinned to slot ${slot} of ${total}`);
|
|
184
|
+
return {
|
|
185
|
+
fields: { [SLOT_FIELD]: slot, [CACHE_PROMPT_FIELD]: true },
|
|
186
|
+
headers,
|
|
187
|
+
slotted: true,
|
|
188
|
+
release: () => slots.release(session),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
143
191
|
/**
|
|
144
192
|
* A message's words, as the preselector is asked about them: its string, or its text parts.
|
|
145
193
|
*
|
|
@@ -422,10 +470,11 @@ function turnCalls(turn, recoverable, notice) {
|
|
|
422
470
|
* @param options - What `runAgentLoop` was given.
|
|
423
471
|
* @param standing - Written to: once the steps have set themselves up, its `read` is theirs, so the
|
|
424
472
|
* caller can read the run as it stands whether they return or throw.
|
|
473
|
+
* @param [pin] - What the run's session adds to each request. Absent adds nothing.
|
|
425
474
|
* @returns The result of the turn that asked for no tools, or `undefined` when `maxToolIterations`
|
|
426
475
|
* is spent.
|
|
427
476
|
*/
|
|
428
|
-
async function runSteps(options, standing) {
|
|
477
|
+
async function runSteps(options, standing, pin) {
|
|
429
478
|
const { config, system = '', tools = [], catalog = [], dispatch, hooks, signal } = options;
|
|
430
479
|
const { onRequest, onTurn, onMessage, onToolCall, onToolResult, beforeStep, parallel = false, recoverToolCalls: recover = true, maxContinuations = 0, toolOrder = true, dedupeToolCalls = true, preselectRouting = PRESELECT_EXCLUSIVE, serverInstructions = false, } = options;
|
|
431
480
|
const dedupable = typeof dedupeToolCalls === 'function' ? dedupeToolCalls : () => dedupeToolCalls;
|
|
@@ -435,6 +484,9 @@ async function runSteps(options, standing) {
|
|
|
435
484
|
const supports = capabilitiesFor(config.baseUrl, config.apiKey);
|
|
436
485
|
const maxRetries = Math.max(0, Number(config.maxRetries) || 0);
|
|
437
486
|
const notice = (text) => onEvent({ kind: RunEventKind.Notice, text });
|
|
487
|
+
// The host's own fields win over the pin's, so an explicit `cache_prompt: false` stays false.
|
|
488
|
+
const extraBody = pin ? { ...pin.fields, ...config.extraBody } : config.extraBody;
|
|
489
|
+
const sending = { ...config, extraBody };
|
|
438
490
|
const { proxied, onDemand, loaded, preselected } = await discovery(options, notice);
|
|
439
491
|
const instructions = serverInstructions === false
|
|
440
492
|
? ''
|
|
@@ -577,10 +629,11 @@ async function runSteps(options, standing) {
|
|
|
577
629
|
...(prompt ? [{ role: Role.System, content: prompt }] : []),
|
|
578
630
|
...withContext(messages, questionAt(), gathered.context, preface),
|
|
579
631
|
];
|
|
580
|
-
const build = (supported, refused) => buildBody(
|
|
632
|
+
const build = (supported, refused) => buildBody(sending, supported, refused, request, declared, toolOrder);
|
|
581
633
|
const turnOptions = {
|
|
582
634
|
model: config.model,
|
|
583
|
-
droppable: Object.keys(
|
|
635
|
+
droppable: Object.keys(extraBody ?? {}),
|
|
636
|
+
...(pin ? { headers: pin.headers } : {}),
|
|
584
637
|
maxRetries,
|
|
585
638
|
loadingTimeoutMs: loadingMs(config),
|
|
586
639
|
contextLimit: config.contextLength ?? 0,
|
|
@@ -604,7 +657,13 @@ async function runSteps(options, standing) {
|
|
|
604
657
|
// The breakdown measures the array `toolSchemaTokens` does, so where no prompt was reported
|
|
605
658
|
// the two give one number for the tool block rather than two.
|
|
606
659
|
const charsPerToken = charsPerTokenFor(supports, config.model);
|
|
607
|
-
|
|
660
|
+
// Read after the turn, so a refusal answered by dropping `id_slot` counts this turn unpinned.
|
|
661
|
+
const pinned = pin?.slotted
|
|
662
|
+
? {
|
|
663
|
+
pinned: SLOT_FIELD in pin.fields && !modelCapabilitiesFor(supports, config.model).refusedFields.has(SLOT_FIELD),
|
|
664
|
+
}
|
|
665
|
+
: {};
|
|
666
|
+
Object.assign(first.usage, cacheDiagnosis(previous, { messages: request, tools: names }, first.usage), pinned, {
|
|
608
667
|
toolsDeclared: declared.length,
|
|
609
668
|
toolSchemaTokens: Math.ceil(toolsChars(declared) / charsPerToken),
|
|
610
669
|
context: contextTokens({ model: config.model, stream: true, messages: request, tools: declared }, { charsPerToken, promptTokens: first.usage.prompt }),
|
package/dist/run/events.d.ts
CHANGED
|
@@ -337,6 +337,11 @@ export interface RunMetrics {
|
|
|
337
337
|
cacheBreakReasons: Partial<Record<CacheBreakReason, number>>;
|
|
338
338
|
/** Turns cut off at `maxTokens` after any continuation. */
|
|
339
339
|
truncatedTurns: number;
|
|
340
|
+
/**
|
|
341
|
+
* Turns of a `session` run served on no llama.cpp slot of their own, which `cacheHitRatio` reads
|
|
342
|
+
* against. Absent where no turn reported `pinned`.
|
|
343
|
+
*/
|
|
344
|
+
unpinnedTurns?: number;
|
|
340
345
|
/** From the first event to the last, which a still-running run keeps moving. */
|
|
341
346
|
wallMs?: number;
|
|
342
347
|
/** Prefill time summed over the turns. */
|
package/dist/run/events.js
CHANGED
|
@@ -576,6 +576,9 @@ export function runMetrics(events, { contextLength } = {}) {
|
|
|
576
576
|
if (turn.finishReason === FinishReason.Length) {
|
|
577
577
|
metrics.truncatedTurns++;
|
|
578
578
|
}
|
|
579
|
+
if (turn.pinned !== undefined) {
|
|
580
|
+
add('unpinnedTurns', turn.pinned ? 0 : 1);
|
|
581
|
+
}
|
|
579
582
|
if (turn.wallMs !== undefined) {
|
|
580
583
|
metrics.slowestTurnMs = Math.max(metrics.slowestTurnMs ?? 0, turn.wallMs);
|
|
581
584
|
}
|
package/dist/runtime/reset.d.ts
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Forgets everything this package remembers between calls.
|
|
3
3
|
*
|
|
4
4
|
* @remarks
|
|
5
|
-
*
|
|
5
|
+
* Seven modules here keep state for the life of the runtime — the process's, unless this is called
|
|
6
6
|
* as a method of one `createRuntime` made, which clears that one alone — each for a good reason
|
|
7
7
|
* and each with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
8
8
|
* out not to take `stream_options` or a grammar, each model's measured characters per token,
|
|
9
|
-
* the models that refused the no-thinking hints, the event bus,
|
|
10
|
-
* budget and preface
|
|
11
|
-
* `
|
|
9
|
+
* the models that refused the no-thinking hints, the event bus, the hooks' configured
|
|
10
|
+
* budget and preface, and which session holds which llama.cpp slot. `resetClients`,
|
|
11
|
+
* `resetCapabilities`, `resetCalibration`, `resetHints`, `resetEvents`, `resetHooks` and
|
|
12
|
+
* `resetSlots` stay exported, because a test that means to clear one thing
|
|
12
13
|
* should say so.
|
|
13
14
|
*
|
|
14
15
|
* This is for the other case, which is every teardown. What they hold is *latched
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
* one that reads the latch fails only when it happens to run second. `tests/side-task-hints.test.ts`
|
|
19
20
|
* was written that way and only passed because every case had been handed a hostname of its own.
|
|
20
21
|
*
|
|
21
|
-
* It is also the seam that does not need finding again.
|
|
22
|
+
* It is also the seam that does not need finding again. An eighth module with a cache is an eighth
|
|
22
23
|
* line here, rather than an edit to the teardown of three consumers who will not all notice.
|
|
23
24
|
*
|
|
24
25
|
* A test that builds a `createRuntime` per case has nothing to tear down, and is the better
|
package/dist/runtime/reset.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resetCalibration } from '../endpoint/calibration.js';
|
|
2
2
|
import { resetCapabilities } from '../endpoint/capabilities.js';
|
|
3
3
|
import { resetClients } from '../endpoint/client.js';
|
|
4
|
+
import { resetSlots } from '../endpoint/slots.js';
|
|
4
5
|
import { resetHooks } from '../hooks/hooks.js';
|
|
5
6
|
import { resetEvents } from '../run/events.js';
|
|
6
7
|
import { resetHints } from '../turn/side-task.js';
|
|
@@ -8,13 +9,14 @@ import { resetHints } from '../turn/side-task.js';
|
|
|
8
9
|
* Forgets everything this package remembers between calls.
|
|
9
10
|
*
|
|
10
11
|
* @remarks
|
|
11
|
-
*
|
|
12
|
+
* Seven modules here keep state for the life of the runtime — the process's, unless this is called
|
|
12
13
|
* as a method of one `createRuntime` made, which clears that one alone — each for a good reason
|
|
13
14
|
* and each with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
14
15
|
* out not to take `stream_options` or a grammar, each model's measured characters per token,
|
|
15
|
-
* the models that refused the no-thinking hints, the event bus,
|
|
16
|
-
* budget and preface
|
|
17
|
-
* `
|
|
16
|
+
* the models that refused the no-thinking hints, the event bus, the hooks' configured
|
|
17
|
+
* budget and preface, and which session holds which llama.cpp slot. `resetClients`,
|
|
18
|
+
* `resetCapabilities`, `resetCalibration`, `resetHints`, `resetEvents`, `resetHooks` and
|
|
19
|
+
* `resetSlots` stay exported, because a test that means to clear one thing
|
|
18
20
|
* should say so.
|
|
19
21
|
*
|
|
20
22
|
* This is for the other case, which is every teardown. What they hold is *latched
|
|
@@ -24,7 +26,7 @@ import { resetHints } from '../turn/side-task.js';
|
|
|
24
26
|
* one that reads the latch fails only when it happens to run second. `tests/side-task-hints.test.ts`
|
|
25
27
|
* was written that way and only passed because every case had been handed a hostname of its own.
|
|
26
28
|
*
|
|
27
|
-
* It is also the seam that does not need finding again.
|
|
29
|
+
* It is also the seam that does not need finding again. An eighth module with a cache is an eighth
|
|
28
30
|
* line here, rather than an edit to the teardown of three consumers who will not all notice.
|
|
29
31
|
*
|
|
30
32
|
* A test that builds a `createRuntime` per case has nothing to tear down, and is the better
|
|
@@ -37,4 +39,5 @@ export function resetAll() {
|
|
|
37
39
|
resetHints();
|
|
38
40
|
resetEvents();
|
|
39
41
|
resetHooks();
|
|
42
|
+
resetSlots();
|
|
40
43
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { compactTranscript, runCompaction, summariser } from '../context/compaction.ts';
|
|
2
2
|
import { calibrate, charsPerTokenFor, resetCalibration } from '../endpoint/calibration.ts';
|
|
3
3
|
import { capabilitiesFor, expireCapabilities, resetCapabilities } from '../endpoint/capabilities.ts';
|
|
4
|
-
import { type ClientPoolOptions, configureClients, contextLimitFor, getClient, listModels, resetClients, servedWindow } from '../endpoint/client.ts';
|
|
4
|
+
import { type ClientPoolOptions, configureClients, contextLimitFor, getClient, listModels, resetClients, servedSlots, servedWindow } from '../endpoint/client.ts';
|
|
5
|
+
import { resetSlots, slotAllocator } from '../endpoint/slots.ts';
|
|
5
6
|
import { assembleContext, configureHooks, consult, gather, type HookOptions, notify, resetHooks, withContext } from '../hooks/hooks.ts';
|
|
6
7
|
import { runAgentLoop } from '../run/agent-loop.ts';
|
|
7
8
|
import { configureEvents, type EventBusOptions, emit, endRun, history, resetEvents, watch } from '../run/events.ts';
|
|
@@ -31,6 +32,8 @@ declare const STATEFUL: {
|
|
|
31
32
|
listModels: typeof listModels;
|
|
32
33
|
contextLimitFor: typeof contextLimitFor;
|
|
33
34
|
servedWindow: typeof servedWindow;
|
|
35
|
+
servedSlots: typeof servedSlots;
|
|
36
|
+
slotAllocator: typeof slotAllocator;
|
|
34
37
|
configureClients: typeof configureClients;
|
|
35
38
|
capabilitiesFor: typeof capabilitiesFor;
|
|
36
39
|
expireCapabilities: typeof expireCapabilities;
|
|
@@ -55,6 +58,7 @@ declare const STATEFUL: {
|
|
|
55
58
|
resetHints: typeof resetHints;
|
|
56
59
|
resetEvents: typeof resetEvents;
|
|
57
60
|
resetHooks: typeof resetHooks;
|
|
61
|
+
resetSlots: typeof resetSlots;
|
|
58
62
|
resetAll: typeof resetAll;
|
|
59
63
|
};
|
|
60
64
|
/** What a runtime starts out configured with. Every field optional; see `createRuntime`. */
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -2,7 +2,8 @@ import { compactTranscript, runCompaction, summariser } from '../context/compact
|
|
|
2
2
|
import { inScope, rootScope } from '../core/scope.js';
|
|
3
3
|
import { calibrate, charsPerTokenFor, resetCalibration } from '../endpoint/calibration.js';
|
|
4
4
|
import { capabilitiesFor, expireCapabilities, resetCapabilities } from '../endpoint/capabilities.js';
|
|
5
|
-
import { configureClients, contextLimitFor, getClient, listModels, resetClients, servedWindow, } from '../endpoint/client.js';
|
|
5
|
+
import { configureClients, contextLimitFor, getClient, listModels, resetClients, servedSlots, servedWindow, } from '../endpoint/client.js';
|
|
6
|
+
import { resetSlots, slotAllocator } from '../endpoint/slots.js';
|
|
6
7
|
import { assembleContext, configureHooks, consult, gather, notify, resetHooks, withContext, } from '../hooks/hooks.js';
|
|
7
8
|
import { runAgentLoop } from '../run/agent-loop.js';
|
|
8
9
|
import { configureEvents, emit, endRun, history, resetEvents, watch } from '../run/events.js';
|
|
@@ -32,6 +33,8 @@ const STATEFUL = {
|
|
|
32
33
|
listModels,
|
|
33
34
|
contextLimitFor,
|
|
34
35
|
servedWindow,
|
|
36
|
+
servedSlots,
|
|
37
|
+
slotAllocator,
|
|
35
38
|
configureClients,
|
|
36
39
|
capabilitiesFor,
|
|
37
40
|
expireCapabilities,
|
|
@@ -56,6 +59,7 @@ const STATEFUL = {
|
|
|
56
59
|
resetHints,
|
|
57
60
|
resetEvents,
|
|
58
61
|
resetHooks,
|
|
62
|
+
resetSlots,
|
|
59
63
|
resetAll,
|
|
60
64
|
};
|
|
61
65
|
/**
|
package/dist/wire/stream.d.ts
CHANGED
|
@@ -112,6 +112,12 @@ export interface TurnUsage {
|
|
|
112
112
|
cacheBreakReason?: CacheBreakReason;
|
|
113
113
|
/** How many tools the request declared, filled by `runAgentLoop`. */
|
|
114
114
|
toolsDeclared?: number;
|
|
115
|
+
/**
|
|
116
|
+
* The turn was served on the llama.cpp slot its run holds, filled by `runAgentLoop`. Present only
|
|
117
|
+
* for a run given a `session` on a server that reports slots: `false` is one that could not be
|
|
118
|
+
* pinned, every slot being held or the server refusing `id_slot`.
|
|
119
|
+
*/
|
|
120
|
+
pinned?: boolean;
|
|
115
121
|
/**
|
|
116
122
|
* The declared tool block's estimated tokens. A chat template renders it ahead of the system
|
|
117
123
|
* prompt, so this is what every change to the tool list costs the cache.
|
|
@@ -250,6 +256,11 @@ export interface StreamTurnOptions {
|
|
|
250
256
|
onThinking?: (delta: string) => void;
|
|
251
257
|
/** The model's answer, as it arrives. */
|
|
252
258
|
onOutput?: (delta: string) => void;
|
|
259
|
+
/**
|
|
260
|
+
* Headers sent with the request, beside the client's own — `SESSION_HEADER`, for a router that
|
|
261
|
+
* keeps a session on one provider. Never refused, so never negotiated.
|
|
262
|
+
*/
|
|
263
|
+
headers?: Record<string, string>;
|
|
253
264
|
}
|
|
254
265
|
/**
|
|
255
266
|
* Runs one turn as a stream, reporting tokens as they arrive and assembling them back into a
|
|
@@ -271,4 +282,4 @@ export interface StreamTurnOptions {
|
|
|
271
282
|
* which run it is: `step` is the caller's flow concept, and wrapping these into an `emit` is one
|
|
272
283
|
* line at the call site.
|
|
273
284
|
*/
|
|
274
|
-
export declare function streamTurn(client: OpenAI, body: OpenAI.ChatCompletionCreateParamsStreaming, { signal, idleMs, firstChunkMs, produced, fences, startInReasoning, onThinking, onOutput, }?: StreamTurnOptions): Promise<Turn>;
|
|
285
|
+
export declare function streamTurn(client: OpenAI, body: OpenAI.ChatCompletionCreateParamsStreaming, { signal, idleMs, firstChunkMs, produced, fences, startInReasoning, onThinking, onOutput, headers, }?: StreamTurnOptions): Promise<Turn>;
|
package/dist/wire/stream.js
CHANGED
|
@@ -208,7 +208,7 @@ const NO_SDK_TIMEOUT = 2_147_483_647;
|
|
|
208
208
|
* which run it is: `step` is the caller's flow concept, and wrapping these into an `emit` is one
|
|
209
209
|
* line at the call site.
|
|
210
210
|
*/
|
|
211
|
-
export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, produced, fences = DEFAULT_FENCES, startInReasoning, onThinking, onOutput, } = {}) {
|
|
211
|
+
export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, produced, fences = DEFAULT_FENCES, startInReasoning, onThinking, onOutput, headers, } = {}) {
|
|
212
212
|
// Silence, not duration: the timer is rearmed on every chunk, so a model that is still
|
|
213
213
|
// talking is never cut off however long it takes, and one that has stopped talking does not
|
|
214
214
|
// hang the run until someone notices. A request that never answers at all is the same case
|
|
@@ -257,6 +257,7 @@ export async function streamTurn(client, body, { signal, idleMs, firstChunkMs, p
|
|
|
257
257
|
const stream = await client.chat.completions.create(body, {
|
|
258
258
|
signal: linked,
|
|
259
259
|
...(watched ? { timeout: NO_SDK_TIMEOUT } : {}),
|
|
260
|
+
...(headers ? { headers } : {}),
|
|
260
261
|
});
|
|
261
262
|
// The field's reasoning here, the fenced kind in the splitter, which can still move text
|
|
262
263
|
// already read as answer into reasoning when a closing tag turns up with no opening one.
|
package/llms.txt
CHANGED
|
@@ -94,6 +94,7 @@ What an endpoint turned out not to support, and answering it when it says so.
|
|
|
94
94
|
- `NO_KEY` — The SDK insists on a non-empty key even where the server will not look at it.
|
|
95
95
|
- `resetClients` — Forgets every cached client and listing, and puts `configureClients` back to the defaults.
|
|
96
96
|
- `resolveApiKey` — The key to send, where an endpoint may inherit one from the settings it overrides.
|
|
97
|
+
- `servedSlots` — How many slots a llama.cpp server runs a model in, for pinning a session to one of them.
|
|
97
98
|
- `servedWindow` — The window a local server's own API says it is serving a model in, for the servers whose listing does not say.
|
|
98
99
|
- `timeoutMs` — An endpoint's request timeout in the SDK's spelling: milliseconds, and `undefined` for no limit.
|
|
99
100
|
|
|
@@ -107,6 +108,17 @@ JSON Schema compatibility for llama.cpp-backed servers.
|
|
|
107
108
|
- `sanitizeSchema` — One JSON Schema as a strict server will accept it, for a schema that is not a tool's parameters.
|
|
108
109
|
- `sanitizeTools` — Tool definitions a strict server will accept, remembered per definition object.
|
|
109
110
|
|
|
111
|
+
### slots
|
|
112
|
+
|
|
113
|
+
Which slot of a multi-slot llama.cpp server each session is on, so concurrent runs stop evicting each other's prompt cache.
|
|
114
|
+
|
|
115
|
+
- `CACHE_PROMPT_FIELD` — The request field that asks llama.cpp to reuse the slot's cached prefix.
|
|
116
|
+
- `resetSlots` — Forgets every endpoint's slots, held ones included.
|
|
117
|
+
- `SESSION_HEADER` — The header llm-router's sticky affinity reads first, which keeps a session on one provider.
|
|
118
|
+
- `SLOT_FIELD` — The request field that names a llama.cpp slot.
|
|
119
|
+
- `SlotAllocator` (type) — Hands one endpoint's slots to sessions.
|
|
120
|
+
- `slotAllocator` — The slot allocator for one endpoint and model, sized to what its `/props` said.
|
|
121
|
+
|
|
110
122
|
### hook-events
|
|
111
123
|
|
|
112
124
|
The points in a session a hook can be bound to, apart from everything that runs one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cubicecho/agent-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.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",
|