theorum 0.1.10 → 0.1.13
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 +25 -2
- package/docs/COMPACTION.md +227 -0
- package/docs/SECRETS.md +6 -1
- package/docs/STOP.md +85 -0
- package/esm/mod.d.ts +6 -2
- package/esm/mod.js +3 -1
- package/esm/src/cli/commands/bench.js +2 -4
- package/esm/src/cli/commands/fuzz-guardrails.js +195 -48
- package/esm/src/guardrails/injection.js +13 -8
- package/esm/src/guardrails/normalize.js +65 -38
- package/esm/src/guardrails/sensitive.js +1 -1
- package/esm/src/kernel/engine/compaction.d.ts +69 -0
- package/esm/src/kernel/engine/compaction.js +141 -0
- package/esm/src/kernel/engine/delta.js +30 -7
- package/esm/src/kernel/engine/history-tokens.d.ts +43 -0
- package/esm/src/kernel/engine/history-tokens.js +100 -0
- package/esm/src/kernel/engine/runner/mod.js +164 -62
- package/esm/src/kernel/engine/runner/state.d.ts +3 -1
- package/esm/src/kernel/engine/runner/steps.js +3 -0
- package/esm/src/kernel/mod.d.ts +4 -0
- package/esm/src/kernel/mod.js +2 -0
- package/esm/src/kernel/registry/profiles.js +37 -0
- package/esm/src/kernel/stop.d.ts +75 -0
- package/esm/src/kernel/stop.js +120 -0
- package/esm/src/kernel/types.d.ts +117 -1
- package/esm/src/providers/create-provider.d.ts +7 -0
- package/esm/src/providers/create-provider.js +24 -4
- package/esm/src/providers/expose-for-tests.js +5 -1
- package/esm/src/providers/local.d.ts +29 -0
- package/esm/src/providers/local.js +259 -0
- package/esm/src/providers/mod.d.ts +2 -0
- package/esm/src/providers/mod.js +1 -0
- package/esm/src/providers/openrouter.js +32 -13
- package/esm/src/providers/provider.js +1 -1
- package/esm/src/providers/speech.js +1 -1
- package/esm/src/streaming/mod.d.ts +3 -1
- package/esm/src/streaming/mod.js +2 -1
- package/package.json +8 -3
- package/docs/AGENT_PROFILE_CONTRACT.md +0 -189
- package/docs/CLI_SPEC.md +0 -183
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction helpers: history split + threshold metering.
|
|
3
|
+
*
|
|
4
|
+
* Pure and stateless. History lives on the request; the kernel does not store
|
|
5
|
+
* cross-turn session state.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { estimateHistoryTokens } from './history-tokens.js';
|
|
10
|
+
export { estimateHistoryTokens, HISTORY_MEDIA_TOKENS, HISTORY_TEXT_ENCODING, } from './history-tokens.js';
|
|
11
|
+
/** Effective meter; defaults to `'history'`. */
|
|
12
|
+
export function compactionMeter(spec) {
|
|
13
|
+
return spec.meter ?? 'history';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Find exchange boundaries in a history array.
|
|
17
|
+
*
|
|
18
|
+
* An exchange starts at each `user` message and includes all subsequent
|
|
19
|
+
* messages until the next `user` message. System messages before the first
|
|
20
|
+
* user message are not part of any exchange.
|
|
21
|
+
*
|
|
22
|
+
* Returns the indices of each `user` message that starts an exchange.
|
|
23
|
+
*/
|
|
24
|
+
function findExchangeBoundaries(history) {
|
|
25
|
+
const boundaries = [];
|
|
26
|
+
for (let i = 0; i < history.length; i++) {
|
|
27
|
+
if (history[i].role === 'user') {
|
|
28
|
+
boundaries.push(i);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return boundaries;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* History token count for `meter: 'history'`.
|
|
35
|
+
*
|
|
36
|
+
* Prefers host-supplied `input.historyTokens`. Otherwise estimates from
|
|
37
|
+
* `input.history` (tiktoken `o200k_base` + media stubs). Empty/missing → 0.
|
|
38
|
+
* The BPE import runs only when an estimate needs text encoding.
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveHistoryTokens(input) {
|
|
41
|
+
if (input?.historyTokens != null) {
|
|
42
|
+
return input.historyTokens;
|
|
43
|
+
}
|
|
44
|
+
return await estimateHistoryTokens(input?.history ?? []);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the token count used for the compaction threshold.
|
|
48
|
+
*
|
|
49
|
+
* - `meter: 'history'` (default) — `historyTokens` or estimate of `history`.
|
|
50
|
+
* - `meter: 'input'` — prefer `promptTokens` (this turn's provider
|
|
51
|
+
* `tokens.input`, for `timing: 'after'`), else host `input.inputTokens`
|
|
52
|
+
* (previous turn, for `timing: 'before'`). Missing/non-positive → undefined
|
|
53
|
+
* (do not fire).
|
|
54
|
+
*
|
|
55
|
+
* `meter: 'input'` never loads the history tokenizer.
|
|
56
|
+
*/
|
|
57
|
+
export async function resolveCompactionTokens(args) {
|
|
58
|
+
const meter = compactionMeter(args.spec);
|
|
59
|
+
if (meter === 'history') {
|
|
60
|
+
return { meter, tokens: await resolveHistoryTokens(args.input) };
|
|
61
|
+
}
|
|
62
|
+
const fromPrompt = args.promptTokens != null && args.promptTokens > 0 ? args.promptTokens : undefined;
|
|
63
|
+
const fromHost = args.input?.inputTokens != null && args.input.inputTokens > 0
|
|
64
|
+
? args.input.inputTokens
|
|
65
|
+
: undefined;
|
|
66
|
+
const tokens = fromPrompt ?? fromHost;
|
|
67
|
+
if (tokens == null)
|
|
68
|
+
return undefined;
|
|
69
|
+
return { meter, tokens };
|
|
70
|
+
}
|
|
71
|
+
/** Whether compaction should fire for a resolved token count (token-threshold only). */
|
|
72
|
+
export function compactionNeeded(tokens, spec) {
|
|
73
|
+
return tokens > spec.compactAt * spec.maxTokens;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether compaction should fire, respecting a custom trigger when provided.
|
|
77
|
+
*
|
|
78
|
+
* When `spec.trigger` is set, it is called with full context and its result
|
|
79
|
+
* is returned directly. Otherwise falls back to `compactionNeeded`.
|
|
80
|
+
*/
|
|
81
|
+
export async function shouldCompact(resolved, spec) {
|
|
82
|
+
if (spec.trigger) {
|
|
83
|
+
const ctx = {
|
|
84
|
+
tokens: resolved.tokens,
|
|
85
|
+
maxTokens: spec.maxTokens,
|
|
86
|
+
compactAt: spec.compactAt,
|
|
87
|
+
meter: resolved.meter,
|
|
88
|
+
};
|
|
89
|
+
return await spec.trigger(ctx);
|
|
90
|
+
}
|
|
91
|
+
return compactionNeeded(resolved.tokens, spec);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Split history into compactable and retained segments.
|
|
95
|
+
*
|
|
96
|
+
* `previousExchanges` semantics:
|
|
97
|
+
* - `0` — compact everything, retain nothing.
|
|
98
|
+
* - `≥ 1` (integer) — retain the last N exchanges.
|
|
99
|
+
* - `(0, 1)` — retain exchanges that fit within this fraction of `maxTokens`,
|
|
100
|
+
* walking backwards from the most recent (always uses the history estimator).
|
|
101
|
+
*/
|
|
102
|
+
export async function splitForCompaction(history, spec) {
|
|
103
|
+
if (history.length === 0) {
|
|
104
|
+
return { toCompact: [], toRetain: [] };
|
|
105
|
+
}
|
|
106
|
+
if (spec.previousExchanges === 0) {
|
|
107
|
+
return { toCompact: [...history], toRetain: [] };
|
|
108
|
+
}
|
|
109
|
+
const boundaries = findExchangeBoundaries(history);
|
|
110
|
+
if (boundaries.length === 0) {
|
|
111
|
+
return { toCompact: [...history], toRetain: [] };
|
|
112
|
+
}
|
|
113
|
+
let cutIndex;
|
|
114
|
+
if (spec.previousExchanges >= 1) {
|
|
115
|
+
const keep = Math.min(spec.previousExchanges, boundaries.length);
|
|
116
|
+
cutIndex = boundaries[boundaries.length - keep];
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const budget = spec.previousExchanges * spec.maxTokens;
|
|
120
|
+
let accumulated = 0;
|
|
121
|
+
cutIndex = history.length;
|
|
122
|
+
for (let i = boundaries.length - 1; i >= 0; i--) {
|
|
123
|
+
const exchangeStart = boundaries[i];
|
|
124
|
+
const exchangeEnd = i < boundaries.length - 1 ? boundaries[i + 1] : history.length;
|
|
125
|
+
const exchangeMessages = history.slice(exchangeStart, exchangeEnd);
|
|
126
|
+
const exchangeTokens = await estimateHistoryTokens(exchangeMessages);
|
|
127
|
+
if (accumulated + exchangeTokens > budget) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
accumulated += exchangeTokens;
|
|
131
|
+
cutIndex = exchangeStart;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (cutIndex <= 0) {
|
|
135
|
+
return { toCompact: [], toRetain: [...history] };
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
toCompact: history.slice(0, cutIndex),
|
|
139
|
+
toRetain: history.slice(cutIndex),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { turnStopFromInteractionStatus } from '../stop.js';
|
|
1
2
|
import { asRecord } from './record.js';
|
|
2
3
|
function deltaText(delta) {
|
|
3
4
|
if (typeof delta.text === 'string') {
|
|
@@ -359,25 +360,47 @@ function extractTokenEvent(event, interaction) {
|
|
|
359
360
|
}
|
|
360
361
|
function eventsFromComplete(event, alreadyText) {
|
|
361
362
|
const interaction = asRecord(event.interaction) ?? event;
|
|
362
|
-
const outputText = interaction.output_text;
|
|
363
363
|
const events = [];
|
|
364
|
+
const outputText = interaction.output_text;
|
|
364
365
|
if (!alreadyText && typeof outputText === 'string' && outputText) {
|
|
365
366
|
events.push({ type: 'text', text: outputText });
|
|
366
367
|
}
|
|
367
368
|
const media = mediaFromComplete(event);
|
|
368
|
-
if (media)
|
|
369
|
+
if (media)
|
|
369
370
|
events.push(media);
|
|
370
|
-
}
|
|
371
371
|
const tokenEvent = extractTokenEvent(event, interaction);
|
|
372
|
-
if (tokenEvent)
|
|
372
|
+
if (tokenEvent)
|
|
373
373
|
events.push(tokenEvent);
|
|
374
|
-
}
|
|
375
374
|
const groundingEvent = groundingFromEvent(event);
|
|
376
|
-
if (groundingEvent)
|
|
375
|
+
if (groundingEvent)
|
|
377
376
|
events.push(groundingEvent);
|
|
378
|
-
|
|
377
|
+
const done = doneFromInteractionStatus(interaction, event);
|
|
378
|
+
if (done)
|
|
379
|
+
events.push(done);
|
|
379
380
|
return events;
|
|
380
381
|
}
|
|
382
|
+
const TERMINAL_INTERACTION_STATUSES = new Set([
|
|
383
|
+
'completed',
|
|
384
|
+
'incomplete',
|
|
385
|
+
'budget_exceeded',
|
|
386
|
+
'failed',
|
|
387
|
+
'cancelled',
|
|
388
|
+
'requires_action',
|
|
389
|
+
]);
|
|
390
|
+
function doneFromInteractionStatus(interaction, event) {
|
|
391
|
+
const status = typeof interaction.status === 'string'
|
|
392
|
+
? interaction.status
|
|
393
|
+
: typeof event.status === 'string'
|
|
394
|
+
? event.status
|
|
395
|
+
: undefined;
|
|
396
|
+
if (!status || !TERMINAL_INTERACTION_STATUSES.has(status.toLowerCase()))
|
|
397
|
+
return undefined;
|
|
398
|
+
return {
|
|
399
|
+
type: 'done',
|
|
400
|
+
stop: turnStopFromInteractionStatus(status),
|
|
401
|
+
...(typeof interaction.id === 'string' ? { interactionId: interaction.id } : {}),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
381
404
|
function tryStructured(text) {
|
|
382
405
|
try {
|
|
383
406
|
return { type: 'structured', structured: JSON.parse(text) };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local estimate of conversational history tokens (compaction `meter: 'history'`).
|
|
3
|
+
*
|
|
4
|
+
* **Text** — tiktoken `o200k_base` via `gpt-tokenizer` (GPT-4o / GPT-5 / o-series
|
|
5
|
+
* default encoding). This is THEORUM's declared local BPE counter. Gemini has
|
|
6
|
+
* no open JS tokenizer; hosts that need Gemini `countTokens` pass
|
|
7
|
+
* `TurnInput.historyTokens`.
|
|
8
|
+
*
|
|
9
|
+
* The BPE ranks are loaded lazily on first text encode — not at module import.
|
|
10
|
+
* Hosts that pass `historyTokens`, use `meter: 'input'`, or never estimate
|
|
11
|
+
* history text never pay for the import.
|
|
12
|
+
*
|
|
13
|
+
* **Media** — not payload bytes. Current-turn files belong on `attachments` /
|
|
14
|
+
* `voice` and are outside this meter. History media parts use published
|
|
15
|
+
* Gemini multimodal *rates as minimum stubs* when dimensions / duration are
|
|
16
|
+
* not on the part:
|
|
17
|
+
* - image / document: one still-image tile (258) — Gemini 2.x small-image /
|
|
18
|
+
* one-page unit. Larger images are 258×tiles; Gemini 3 uses
|
|
19
|
+
* `media_resolution` budgets (often 560–1120), not a flat 258.
|
|
20
|
+
* - audio: 32 tokens (1s @ 32/s). Longer clips scale with seconds.
|
|
21
|
+
* - video: 263 tokens (1s @ 263/s). Longer clips scale with seconds.
|
|
22
|
+
*
|
|
23
|
+
* These stubs are intentional minima for “media is present in history,” not
|
|
24
|
+
* billing-grade multimodal accounting. Prefer `historyTokens` when the host
|
|
25
|
+
* knows better.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import type { TurnHistoryMessage } from '../types.js';
|
|
30
|
+
/** Tiktoken encoding used for history text. */
|
|
31
|
+
export declare const HISTORY_TEXT_ENCODING = "o200k_base";
|
|
32
|
+
/**
|
|
33
|
+
* Minimum media stubs when size/duration are unknown.
|
|
34
|
+
* See module doc — not “every image costs 258 forever.”
|
|
35
|
+
*/
|
|
36
|
+
export declare const HISTORY_MEDIA_TOKENS: {
|
|
37
|
+
readonly image: 258;
|
|
38
|
+
readonly document: 258;
|
|
39
|
+
readonly audio: 32;
|
|
40
|
+
readonly video: 263;
|
|
41
|
+
};
|
|
42
|
+
/** BPE-count conversational history (text + media stubs). Lazy-loads o200k. */
|
|
43
|
+
export declare function estimateHistoryTokens(messages: TurnHistoryMessage[]): Promise<number>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local estimate of conversational history tokens (compaction `meter: 'history'`).
|
|
3
|
+
*
|
|
4
|
+
* **Text** — tiktoken `o200k_base` via `gpt-tokenizer` (GPT-4o / GPT-5 / o-series
|
|
5
|
+
* default encoding). This is THEORUM's declared local BPE counter. Gemini has
|
|
6
|
+
* no open JS tokenizer; hosts that need Gemini `countTokens` pass
|
|
7
|
+
* `TurnInput.historyTokens`.
|
|
8
|
+
*
|
|
9
|
+
* The BPE ranks are loaded lazily on first text encode — not at module import.
|
|
10
|
+
* Hosts that pass `historyTokens`, use `meter: 'input'`, or never estimate
|
|
11
|
+
* history text never pay for the import.
|
|
12
|
+
*
|
|
13
|
+
* **Media** — not payload bytes. Current-turn files belong on `attachments` /
|
|
14
|
+
* `voice` and are outside this meter. History media parts use published
|
|
15
|
+
* Gemini multimodal *rates as minimum stubs* when dimensions / duration are
|
|
16
|
+
* not on the part:
|
|
17
|
+
* - image / document: one still-image tile (258) — Gemini 2.x small-image /
|
|
18
|
+
* one-page unit. Larger images are 258×tiles; Gemini 3 uses
|
|
19
|
+
* `media_resolution` budgets (often 560–1120), not a flat 258.
|
|
20
|
+
* - audio: 32 tokens (1s @ 32/s). Longer clips scale with seconds.
|
|
21
|
+
* - video: 263 tokens (1s @ 263/s). Longer clips scale with seconds.
|
|
22
|
+
*
|
|
23
|
+
* These stubs are intentional minima for “media is present in history,” not
|
|
24
|
+
* billing-grade multimodal accounting. Prefer `historyTokens` when the host
|
|
25
|
+
* knows better.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
/** Tiktoken encoding used for history text. */
|
|
30
|
+
export const HISTORY_TEXT_ENCODING = 'o200k_base';
|
|
31
|
+
/**
|
|
32
|
+
* Minimum media stubs when size/duration are unknown.
|
|
33
|
+
* See module doc — not “every image costs 258 forever.”
|
|
34
|
+
*/
|
|
35
|
+
export const HISTORY_MEDIA_TOKENS = {
|
|
36
|
+
image: 258,
|
|
37
|
+
document: 258,
|
|
38
|
+
audio: 32,
|
|
39
|
+
video: 263,
|
|
40
|
+
};
|
|
41
|
+
let encodePromise = null;
|
|
42
|
+
function loadEncode() {
|
|
43
|
+
encodePromise ??= import('gpt-tokenizer/encoding/o200k_base').then((m) => m.encode);
|
|
44
|
+
return encodePromise;
|
|
45
|
+
}
|
|
46
|
+
function mediaTokens(kind) {
|
|
47
|
+
return HISTORY_MEDIA_TOKENS[kind];
|
|
48
|
+
}
|
|
49
|
+
function partNeedsText(part) {
|
|
50
|
+
return part.type === 'text' && Boolean(part.text);
|
|
51
|
+
}
|
|
52
|
+
function messageNeedsText(msg) {
|
|
53
|
+
if (msg.content)
|
|
54
|
+
return true;
|
|
55
|
+
if (msg.parts?.some(partNeedsText))
|
|
56
|
+
return true;
|
|
57
|
+
return Boolean(msg.tool_calls?.some((tc) => tc.function.arguments));
|
|
58
|
+
}
|
|
59
|
+
function needsTextEncode(messages) {
|
|
60
|
+
return messages.some(messageNeedsText);
|
|
61
|
+
}
|
|
62
|
+
function sumMediaStubs(messages) {
|
|
63
|
+
let tokens = 0;
|
|
64
|
+
for (const msg of messages) {
|
|
65
|
+
for (const part of msg.parts ?? []) {
|
|
66
|
+
if (part.type !== 'text')
|
|
67
|
+
tokens += mediaTokens(part.type);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return tokens;
|
|
71
|
+
}
|
|
72
|
+
async function countMessageTokens(msg, countText) {
|
|
73
|
+
let tokens = 0;
|
|
74
|
+
if (msg.content)
|
|
75
|
+
tokens += await countText(msg.content);
|
|
76
|
+
for (const part of msg.parts ?? []) {
|
|
77
|
+
tokens += part.type === 'text' ? await countText(part.text) : mediaTokens(part.type);
|
|
78
|
+
}
|
|
79
|
+
for (const tc of msg.tool_calls ?? []) {
|
|
80
|
+
tokens += await countText(tc.function.arguments);
|
|
81
|
+
}
|
|
82
|
+
return tokens;
|
|
83
|
+
}
|
|
84
|
+
/** BPE-count conversational history (text + media stubs). Lazy-loads o200k. */
|
|
85
|
+
export async function estimateHistoryTokens(messages) {
|
|
86
|
+
if (!needsTextEncode(messages))
|
|
87
|
+
return sumMediaStubs(messages);
|
|
88
|
+
let encode;
|
|
89
|
+
const countText = async (text) => {
|
|
90
|
+
if (!text)
|
|
91
|
+
return 0;
|
|
92
|
+
encode ??= await loadEncode();
|
|
93
|
+
return encode(text).length;
|
|
94
|
+
};
|
|
95
|
+
let tokens = 0;
|
|
96
|
+
for (const msg of messages) {
|
|
97
|
+
tokens += await countMessageTokens(msg, countText);
|
|
98
|
+
}
|
|
99
|
+
return tokens;
|
|
100
|
+
}
|
|
@@ -12,11 +12,87 @@ import { sanitizeTurnRequest } from '../../../guardrails/sanitize.js';
|
|
|
12
12
|
import { noopSink, writeTrace } from '../../../observability/trace.js';
|
|
13
13
|
import { buildRecord } from '../../../observability/trace-record.js';
|
|
14
14
|
import { pickSystemRole, resolveTurn } from '../../registry/resolve.js';
|
|
15
|
+
import { CONTINUE_INSTRUCTION } from '../../stop.js';
|
|
15
16
|
import { bindCanary } from '../boundary.js';
|
|
17
|
+
import { resolveCompactionTokens, shouldCompact, splitForCompaction } from '../compaction.js';
|
|
16
18
|
import { runAttemptsWithValidation } from './gates.js';
|
|
17
19
|
import { shouldSkipStreamEvent, systemFromProfile } from './stream.js';
|
|
18
20
|
import { calculateFallbackTokens } from './tokens.js';
|
|
19
21
|
import { invokeFromUi } from './tools.js';
|
|
22
|
+
function getCompactionSpec(profile, modelId) {
|
|
23
|
+
return profile.model.config[modelId]?.compaction;
|
|
24
|
+
}
|
|
25
|
+
async function runCompactionTurn(toCompact, spec, provider, signal) {
|
|
26
|
+
const compactText = toCompact
|
|
27
|
+
.map((m) => {
|
|
28
|
+
const content = m.content ?? m.parts?.map((p) => ('text' in p ? p.text : '')).join('') ?? '';
|
|
29
|
+
return `[${m.role}]: ${content}`;
|
|
30
|
+
})
|
|
31
|
+
.join('\n');
|
|
32
|
+
const events = [];
|
|
33
|
+
for await (const event of runTurn({
|
|
34
|
+
profile: spec.profile,
|
|
35
|
+
input: { text: compactText },
|
|
36
|
+
signal,
|
|
37
|
+
metadata: { _compacting: true },
|
|
38
|
+
}, provider)) {
|
|
39
|
+
events.push(event);
|
|
40
|
+
}
|
|
41
|
+
const structured = events.find((e) => e.type === 'structured')?.structured;
|
|
42
|
+
const text = structured
|
|
43
|
+
? JSON.stringify(structured)
|
|
44
|
+
: events
|
|
45
|
+
.filter((e) => e.type === 'text')
|
|
46
|
+
.map((e) => e.text ?? '')
|
|
47
|
+
.join('');
|
|
48
|
+
return {
|
|
49
|
+
role: 'assistant',
|
|
50
|
+
content: text,
|
|
51
|
+
metadata: { compactionSummary: true },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function lastTokensFromEvents(events) {
|
|
55
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
56
|
+
const input = events[i]?.tokens?.input;
|
|
57
|
+
if (input)
|
|
58
|
+
return input;
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
async function compactHistoryBeforeTurn(args) {
|
|
63
|
+
const decision = await resolveCompactionTokens({
|
|
64
|
+
spec: args.spec,
|
|
65
|
+
input: args.input,
|
|
66
|
+
});
|
|
67
|
+
if (!(decision && (await shouldCompact(decision, args.spec))))
|
|
68
|
+
return args.history;
|
|
69
|
+
const { toCompact, toRetain } = await splitForCompaction(args.history, args.spec);
|
|
70
|
+
if (toCompact.length === 0)
|
|
71
|
+
return args.history;
|
|
72
|
+
const summaryMessage = await runCompactionTurn(toCompact, args.spec, args.compactionProvider ?? args.provider, args.signal);
|
|
73
|
+
return [summaryMessage, ...toRetain];
|
|
74
|
+
}
|
|
75
|
+
async function attachAfterCompaction(event, args) {
|
|
76
|
+
if (args.history.length === 0)
|
|
77
|
+
return event;
|
|
78
|
+
const promptTokens = lastTokensFromEvents(args.seen);
|
|
79
|
+
const decision = await resolveCompactionTokens({
|
|
80
|
+
spec: args.spec,
|
|
81
|
+
input: args.input,
|
|
82
|
+
promptTokens,
|
|
83
|
+
});
|
|
84
|
+
if (!(decision && (await shouldCompact(decision, args.spec))))
|
|
85
|
+
return event;
|
|
86
|
+
const signal = {
|
|
87
|
+
needed: true,
|
|
88
|
+
meter: decision.meter,
|
|
89
|
+
tokens: decision.tokens,
|
|
90
|
+
history: args.history,
|
|
91
|
+
};
|
|
92
|
+
if (promptTokens > 0)
|
|
93
|
+
signal.promptTokens = promptTokens;
|
|
94
|
+
return { ...event, compaction: signal };
|
|
95
|
+
}
|
|
20
96
|
async function* emitTurn(args) {
|
|
21
97
|
const { safe, profile, generation, system, provider, gemini } = args;
|
|
22
98
|
if (safe.toolInvoke) {
|
|
@@ -34,75 +110,101 @@ async function* emitTurn(args) {
|
|
|
34
110
|
if (!state.sawTokensEvent) {
|
|
35
111
|
yield* calculateFallbackTokens(safe, system, state.allEmittedEvents);
|
|
36
112
|
}
|
|
37
|
-
yield {
|
|
113
|
+
yield {
|
|
114
|
+
type: 'done',
|
|
115
|
+
stop: state.lastStop ?? { kind: 'completed' },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function flushTurnTrace(sink, ctx) {
|
|
119
|
+
await writeTrace(sink, buildRecord({
|
|
120
|
+
req: ctx.req,
|
|
121
|
+
events: ctx.seen,
|
|
122
|
+
started: ctx.started,
|
|
123
|
+
model: ctx.model,
|
|
124
|
+
bucket: ctx.bucket,
|
|
125
|
+
thrown: ctx.thrown,
|
|
126
|
+
gemini: ctx.gemini,
|
|
127
|
+
canary: ctx.canary,
|
|
128
|
+
system: ctx.system,
|
|
129
|
+
generation: ctx.generation,
|
|
130
|
+
sanitizedReq: ctx.safe,
|
|
131
|
+
}));
|
|
38
132
|
}
|
|
39
133
|
/** Execute one host turn against a provider adapter. */
|
|
40
134
|
async function* runTurn(req, provider, sink = noopSink()) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let generation;
|
|
49
|
-
let safe;
|
|
135
|
+
const ctx = {
|
|
136
|
+
req,
|
|
137
|
+
seen: [],
|
|
138
|
+
started: Date.now(),
|
|
139
|
+
canary: '',
|
|
140
|
+
gemini: [],
|
|
141
|
+
};
|
|
50
142
|
try {
|
|
51
|
-
|
|
52
|
-
throwIfAborted(safe.signal);
|
|
53
|
-
const { profile, generation: gen } = resolveTurn(safe);
|
|
54
|
-
generation = gen;
|
|
55
|
-
const { model: resolvedModel, geminiBucket, canary: turnCanary } = gen;
|
|
56
|
-
model = resolvedModel;
|
|
57
|
-
bucket = geminiBucket;
|
|
58
|
-
canary = turnCanary;
|
|
59
|
-
const role = pickSystemRole(profile, safe.input?.role);
|
|
60
|
-
const profileSys = systemFromProfile(profile, role);
|
|
61
|
-
const combinedSys = [profileSys, safe.system].filter(Boolean).join('\n\n');
|
|
62
|
-
const bound = bindCanary(combinedSys, turnCanary);
|
|
63
|
-
system = bound;
|
|
64
|
-
for await (const event of emitTurn({
|
|
65
|
-
safe,
|
|
66
|
-
profile,
|
|
67
|
-
generation: gen,
|
|
68
|
-
system: bound,
|
|
69
|
-
provider,
|
|
70
|
-
gemini,
|
|
71
|
-
})) {
|
|
72
|
-
seen.push(event);
|
|
73
|
-
if (shouldSkipStreamEvent(event, profile)) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
yield event;
|
|
77
|
-
}
|
|
143
|
+
yield* runTurnBody(ctx, provider);
|
|
78
144
|
}
|
|
79
145
|
catch (err) {
|
|
80
|
-
await
|
|
81
|
-
req,
|
|
82
|
-
events: seen,
|
|
83
|
-
started,
|
|
84
|
-
model,
|
|
85
|
-
bucket,
|
|
86
|
-
thrown: err,
|
|
87
|
-
gemini,
|
|
88
|
-
canary,
|
|
89
|
-
system,
|
|
90
|
-
generation,
|
|
91
|
-
sanitizedReq: safe,
|
|
92
|
-
}));
|
|
146
|
+
await flushTurnTrace(sink, { ...ctx, thrown: err });
|
|
93
147
|
throw err;
|
|
94
148
|
}
|
|
95
|
-
await
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
149
|
+
await flushTurnTrace(sink, ctx);
|
|
150
|
+
}
|
|
151
|
+
async function* runTurnBody(ctx, provider) {
|
|
152
|
+
ctx.safe = sanitizeTurnRequest(ctx.req);
|
|
153
|
+
throwIfAborted(ctx.safe.signal);
|
|
154
|
+
const { profile, generation: gen } = resolveTurn(ctx.safe);
|
|
155
|
+
ctx.generation = gen;
|
|
156
|
+
ctx.model = gen.model;
|
|
157
|
+
ctx.bucket = gen.geminiBucket;
|
|
158
|
+
ctx.canary = gen.canary;
|
|
159
|
+
const isCompacting = ctx.req.metadata?._compacting === true;
|
|
160
|
+
const compactionSpec = isCompacting ? undefined : getCompactionSpec(profile, gen.model);
|
|
161
|
+
await maybeCompactBefore(ctx, gen, compactionSpec, provider);
|
|
162
|
+
const role = pickSystemRole(profile, ctx.safe.input?.role);
|
|
163
|
+
const continueSys = ctx.safe.continueFrom ? CONTINUE_INSTRUCTION : '';
|
|
164
|
+
const combinedSys = [systemFromProfile(profile, role), ctx.safe.system, continueSys]
|
|
165
|
+
.filter(Boolean)
|
|
166
|
+
.join('\n\n');
|
|
167
|
+
ctx.system = bindCanary(combinedSys, ctx.canary);
|
|
168
|
+
yield* streamTurnEvents(ctx, profile, gen, provider, compactionSpec, isCompacting);
|
|
169
|
+
}
|
|
170
|
+
async function maybeCompactBefore(ctx, gen, compactionSpec, provider) {
|
|
171
|
+
if (!(compactionSpec?.timing === 'before' && gen.history?.length && ctx.safe))
|
|
172
|
+
return;
|
|
173
|
+
gen.history = await compactHistoryBeforeTurn({
|
|
174
|
+
spec: compactionSpec,
|
|
175
|
+
history: gen.history,
|
|
176
|
+
input: ctx.safe.input,
|
|
177
|
+
provider,
|
|
178
|
+
compactionProvider: ctx.req.compactionProvider,
|
|
179
|
+
signal: ctx.safe.signal,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async function* streamTurnEvents(ctx, profile, gen, provider, compactionSpec, isCompacting) {
|
|
183
|
+
if (!ctx.safe || ctx.system === undefined)
|
|
184
|
+
return;
|
|
185
|
+
for await (const event of emitTurn({
|
|
186
|
+
safe: ctx.safe,
|
|
187
|
+
profile,
|
|
188
|
+
generation: gen,
|
|
189
|
+
system: ctx.system,
|
|
190
|
+
provider,
|
|
191
|
+
gemini: ctx.gemini,
|
|
192
|
+
})) {
|
|
193
|
+
const out = await maybeAttachAfter(event, ctx, gen, compactionSpec, isCompacting);
|
|
194
|
+
ctx.seen.push(out);
|
|
195
|
+
if (!shouldSkipStreamEvent(out, profile))
|
|
196
|
+
yield out;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function maybeAttachAfter(event, ctx, gen, compactionSpec, isCompacting) {
|
|
200
|
+
if (!(event.type === 'done' && compactionSpec?.timing === 'after' && !isCompacting && ctx.safe)) {
|
|
201
|
+
return event;
|
|
202
|
+
}
|
|
203
|
+
return await attachAfterCompaction(event, {
|
|
204
|
+
spec: compactionSpec,
|
|
205
|
+
history: gen.history ?? [],
|
|
206
|
+
input: ctx.safe.input,
|
|
207
|
+
seen: ctx.seen,
|
|
208
|
+
});
|
|
107
209
|
}
|
|
108
210
|
export { runTurn };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { ResolvedGeneration, TurnEvent, TurnHistoryMessage, TurnRequest } from '../../types.js';
|
|
1
|
+
import type { ResolvedGeneration, TurnEvent, TurnHistoryMessage, TurnRequest, TurnStop } from '../../types.js';
|
|
2
2
|
interface StepExecutionState {
|
|
3
3
|
currentHistory: TurnHistoryMessage[];
|
|
4
4
|
stepCount: number;
|
|
5
5
|
sawTokensEvent: boolean;
|
|
6
6
|
allEmittedEvents: TurnEvent[];
|
|
7
7
|
attemptEvents: TurnEvent[];
|
|
8
|
+
/** Last provider stop from a discarded provider `done` event. */
|
|
9
|
+
lastStop?: TurnStop;
|
|
8
10
|
}
|
|
9
11
|
interface AttemptFlowState {
|
|
10
12
|
currentAttempt: number;
|
|
@@ -28,6 +28,9 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
28
28
|
latestStructured = event.structured;
|
|
29
29
|
}
|
|
30
30
|
if (event.type === 'done') {
|
|
31
|
+
if (event.stop) {
|
|
32
|
+
state.lastStop = event.stop;
|
|
33
|
+
}
|
|
31
34
|
continue;
|
|
32
35
|
}
|
|
33
36
|
if (event.type === 'tool' && event.tool) {
|
package/esm/src/kernel/mod.d.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import "../../_dnt.polyfills.js";
|
|
11
|
+
export type { CompactionSplit, CompactionTokens } from './engine/compaction.js';
|
|
12
|
+
export { compactionMeter, compactionNeeded, estimateHistoryTokens, HISTORY_MEDIA_TOKENS, HISTORY_TEXT_ENCODING, resolveCompactionTokens, resolveHistoryTokens, shouldCompact, splitForCompaction, } from './engine/compaction.js';
|
|
11
13
|
export { runTurn } from './engine/runner.js';
|
|
12
14
|
export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, geminiKindForMime, getTool, listBuiltinIds, mimeAllowed, mimeEssence, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './registry/catalog.js';
|
|
13
15
|
export type { ProfileDefinition } from './registry/profiles.js';
|
|
@@ -15,4 +17,6 @@ export { clearProfiles, defineProfile, getProfile, hasProfile, listProfiles, reg
|
|
|
15
17
|
export { projectProfile, resolveTurn } from './registry/resolve.js';
|
|
16
18
|
export { getStructured, registerStructured } from './registry/schemas.js';
|
|
17
19
|
export { executeTool } from './registry/tools.js';
|
|
20
|
+
export type { ProfileResumeSpec, TurnContinueFrom, TurnStop, TurnStopKind } from './stop.js';
|
|
21
|
+
export { AUTO_CONTINUE_DELAY_MS, CONTINUE_INSTRUCTION, DEFAULT_AUTO_CONTINUE, GenerationStopError, isGenerationStopError, isResumeableStop, isUserCancelledStop, shouldAutoContinue, turnStopFromClientStreamEnd, turnStopFromInteractionStatus, turnStopFromOpenRouter, } from './stop.js';
|
|
18
22
|
export type * from './types.js';
|
package/esm/src/kernel/mod.js
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import "../../_dnt.polyfills.js";
|
|
11
|
+
export { compactionMeter, compactionNeeded, estimateHistoryTokens, HISTORY_MEDIA_TOKENS, HISTORY_TEXT_ENCODING, resolveCompactionTokens, resolveHistoryTokens, shouldCompact, splitForCompaction, } from './engine/compaction.js';
|
|
11
12
|
export { runTurn } from './engine/runner.js';
|
|
12
13
|
export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, geminiKindForMime, getTool, listBuiltinIds, mimeAllowed, mimeEssence, modelEntryByApiId, registerTools, requireModelSpec, resetTools, } from './registry/catalog.js';
|
|
13
14
|
export { clearProfiles, defineProfile, getProfile, hasProfile, listProfiles, registerProfile, registerProfiles, } from './registry/profiles.js';
|
|
14
15
|
export { projectProfile, resolveTurn } from './registry/resolve.js';
|
|
15
16
|
export { getStructured, registerStructured } from './registry/schemas.js';
|
|
16
17
|
export { executeTool } from './registry/tools.js';
|
|
18
|
+
export { AUTO_CONTINUE_DELAY_MS, CONTINUE_INSTRUCTION, DEFAULT_AUTO_CONTINUE, GenerationStopError, isGenerationStopError, isResumeableStop, isUserCancelledStop, shouldAutoContinue, turnStopFromClientStreamEnd, turnStopFromInteractionStatus, turnStopFromOpenRouter, } from './stop.js';
|