theorum 0.1.11 → 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 +6 -1
- package/docs/AGENT_PROFILE_CONTRACT.md +0 -189
- package/docs/CLI_SPEC.md +0 -183
|
@@ -79,6 +79,38 @@ function defineProfile(input) {
|
|
|
79
79
|
guardrails: buildDefaultGuardrails(input.guardrails),
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
function assertCompactionSpec(profileId, modelId, spec) {
|
|
83
|
+
const tag = `Profile ${profileId} model ${modelId} compaction`;
|
|
84
|
+
assertCompactionBudget(tag, spec);
|
|
85
|
+
assertCompactionRetain(tag, spec);
|
|
86
|
+
if (spec.meter != null && spec.meter !== 'history' && spec.meter !== 'input') {
|
|
87
|
+
throw new TheorumError(`${tag}: meter must be 'history' or 'input'`);
|
|
88
|
+
}
|
|
89
|
+
if (!profiles.has(spec.profile)) {
|
|
90
|
+
throw new TheorumError(`${tag}: compaction profile '${spec.profile}' must be registered before '${profileId}'`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function assertCompactionBudget(tag, spec) {
|
|
94
|
+
if (spec.maxTokens <= 0) {
|
|
95
|
+
throw new TheorumError(`${tag}: maxTokens must be > 0`);
|
|
96
|
+
}
|
|
97
|
+
if (spec.compactAt <= 0 || spec.compactAt >= 1) {
|
|
98
|
+
throw new TheorumError(`${tag}: compactAt must be in (0, 1)`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function assertCompactionRetain(tag, spec) {
|
|
102
|
+
if (spec.previousExchanges < 0) {
|
|
103
|
+
throw new TheorumError(`${tag}: previousExchanges must be >= 0`);
|
|
104
|
+
}
|
|
105
|
+
if (spec.previousExchanges > 0 && spec.previousExchanges < 1) {
|
|
106
|
+
if (spec.previousExchanges >= spec.compactAt) {
|
|
107
|
+
throw new TheorumError(`${tag}: previousExchanges as fraction (${spec.previousExchanges}) must be < compactAt (${spec.compactAt})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (spec.previousExchanges >= 1 && !Number.isInteger(spec.previousExchanges)) {
|
|
111
|
+
throw new TheorumError(`${tag}: previousExchanges >= 1 must be an integer`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
82
114
|
/** Register one host-owned profile in the process-local registry. */
|
|
83
115
|
function registerProfile(profileInput) {
|
|
84
116
|
const profile = defineProfile(profileInput);
|
|
@@ -89,6 +121,11 @@ function registerProfile(profileInput) {
|
|
|
89
121
|
throw new TheorumError(`Profile ${profile.id} must set maxFiles, maxBytes, and maxTurnBytes`);
|
|
90
122
|
}
|
|
91
123
|
}
|
|
124
|
+
for (const [modelId, spec] of Object.entries(profile.model.config)) {
|
|
125
|
+
if (spec.compaction) {
|
|
126
|
+
assertCompactionSpec(profile.id, modelId, spec.compaction);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
92
129
|
profiles.set(profile.id, profile);
|
|
93
130
|
}
|
|
94
131
|
/** Register several host-owned profiles in order. */
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized turn stop reasons and resume policy.
|
|
3
|
+
*
|
|
4
|
+
* Providers map Interactions `status` / OpenRouter `finish_reason` into `TurnStop`.
|
|
5
|
+
* Hosts classify client SSE drops via `turnStopFromClientStreamEnd`.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/** Why a turn ended (provider-neutral). */
|
|
10
|
+
export type TurnStopKind = 'completed' | 'length' | 'tool' | 'filtered' | 'provider_error' | 'cancelled' | 'stream_incomplete';
|
|
11
|
+
/** Normalized stop attached to terminal `done` events and host continue requests. */
|
|
12
|
+
export interface TurnStop {
|
|
13
|
+
kind: TurnStopKind;
|
|
14
|
+
/** Raw provider / native reason for diagnostics. */
|
|
15
|
+
native?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fixed continue instruction for resumeable stops.
|
|
19
|
+
* Hosts should not invent per-app continue prompts.
|
|
20
|
+
*/
|
|
21
|
+
export declare const CONTINUE_INSTRUCTION = "Continue and finish the incomplete output from the previous turn. Do not restart from scratch; preserve what was already generated and complete it.";
|
|
22
|
+
/**
|
|
23
|
+
* Default kinds for one silent auto-continue (hosts wait briefly, then resume once).
|
|
24
|
+
* User `cancelled` is never included.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_AUTO_CONTINUE: readonly TurnStopKind[];
|
|
27
|
+
/** Pause before the one-shot auto-continue so a flaky tunnel can settle. */
|
|
28
|
+
export declare const AUTO_CONTINUE_DELAY_MS = 1500;
|
|
29
|
+
/** Profile resume policy under `outputs.resume`. */
|
|
30
|
+
export interface ProfileResumeSpec {
|
|
31
|
+
/**
|
|
32
|
+
* Kinds eligible for a Continue / continueFrom turn.
|
|
33
|
+
* Defaults to length, stream_incomplete, provider_error.
|
|
34
|
+
*/
|
|
35
|
+
allowContinue?: TurnStopKind[];
|
|
36
|
+
/**
|
|
37
|
+
* Kinds the host may auto-continue once without a CTA.
|
|
38
|
+
* Kernel does not loop; hosts call continueFrom at most once.
|
|
39
|
+
*/
|
|
40
|
+
autoContinue?: TurnStopKind[];
|
|
41
|
+
}
|
|
42
|
+
/** Partial state passed when continuing a resumeable stop. */
|
|
43
|
+
export interface TurnContinueFrom {
|
|
44
|
+
stop: TurnStop;
|
|
45
|
+
partialText?: string;
|
|
46
|
+
/** Serialized artifact / code preview from the interrupted turn. */
|
|
47
|
+
partialArtifact?: string;
|
|
48
|
+
}
|
|
49
|
+
/** True when this stop may be continued (profile allow list or default). */
|
|
50
|
+
export declare function isResumeableStop(stop: TurnStop | undefined, allowContinue?: readonly TurnStopKind[]): boolean;
|
|
51
|
+
/** True when the host aborted (user Stop). */
|
|
52
|
+
export declare function isUserCancelledStop(stop: TurnStop | undefined): boolean;
|
|
53
|
+
/** True when profile policy allows one silent auto-continue for this stop. */
|
|
54
|
+
export declare function shouldAutoContinue(stop: TurnStop | undefined, autoContinue?: readonly TurnStopKind[] | undefined): boolean;
|
|
55
|
+
/** OpenRouter normalized `finish_reason` (+ optional `native_finish_reason`). */
|
|
56
|
+
export declare function turnStopFromOpenRouter(finishReason: string | null | undefined, nativeFinishReason?: string | null): TurnStop;
|
|
57
|
+
/** Gemini Interactions terminal `status`. */
|
|
58
|
+
export declare function turnStopFromInteractionStatus(status: string | null | undefined): TurnStop;
|
|
59
|
+
/**
|
|
60
|
+
* Client SSE ended without a clean terminal event.
|
|
61
|
+
* User Stop → cancelled; otherwise stream_incomplete (tunnel drop, etc.).
|
|
62
|
+
* Returns null when `sawTerminal` so hosts keep the provider stop.
|
|
63
|
+
*/
|
|
64
|
+
export declare function turnStopFromClientStreamEnd(opts: {
|
|
65
|
+
abortedByUser: boolean;
|
|
66
|
+
sawTerminal: boolean;
|
|
67
|
+
hadPartial?: boolean;
|
|
68
|
+
}): TurnStop | null;
|
|
69
|
+
/** Error hosts throw when classifying an incomplete / non-success stop. */
|
|
70
|
+
export declare class GenerationStopError extends Error {
|
|
71
|
+
readonly name = "GenerationStopError";
|
|
72
|
+
readonly stop: TurnStop;
|
|
73
|
+
constructor(stop: TurnStop, message?: string);
|
|
74
|
+
}
|
|
75
|
+
export declare function isGenerationStopError(err: unknown): err is GenerationStopError;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized turn stop reasons and resume policy.
|
|
3
|
+
*
|
|
4
|
+
* Providers map Interactions `status` / OpenRouter `finish_reason` into `TurnStop`.
|
|
5
|
+
* Hosts classify client SSE drops via `turnStopFromClientStreamEnd`.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Fixed continue instruction for resumeable stops.
|
|
11
|
+
* Hosts should not invent per-app continue prompts.
|
|
12
|
+
*/
|
|
13
|
+
export const CONTINUE_INSTRUCTION = 'Continue and finish the incomplete output from the previous turn. Do not restart from scratch; preserve what was already generated and complete it.';
|
|
14
|
+
/** Default kinds hosts may offer Continue for. */
|
|
15
|
+
const DEFAULT_ALLOW_CONTINUE = [
|
|
16
|
+
'length',
|
|
17
|
+
'stream_incomplete',
|
|
18
|
+
'provider_error',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Default kinds for one silent auto-continue (hosts wait briefly, then resume once).
|
|
22
|
+
* User `cancelled` is never included.
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_AUTO_CONTINUE = ['length', 'stream_incomplete'];
|
|
25
|
+
/** Pause before the one-shot auto-continue so a flaky tunnel can settle. */
|
|
26
|
+
export const AUTO_CONTINUE_DELAY_MS = 1_500;
|
|
27
|
+
const RESUMEABLE_DEFAULT = new Set(DEFAULT_ALLOW_CONTINUE);
|
|
28
|
+
/** True when this stop may be continued (profile allow list or default). */
|
|
29
|
+
export function isResumeableStop(stop, allowContinue) {
|
|
30
|
+
if (!stop)
|
|
31
|
+
return false;
|
|
32
|
+
const allow = allowContinue?.length ? new Set(allowContinue) : RESUMEABLE_DEFAULT;
|
|
33
|
+
return allow.has(stop.kind);
|
|
34
|
+
}
|
|
35
|
+
/** True when the host aborted (user Stop). */
|
|
36
|
+
export function isUserCancelledStop(stop) {
|
|
37
|
+
return stop?.kind === 'cancelled';
|
|
38
|
+
}
|
|
39
|
+
/** True when profile policy allows one silent auto-continue for this stop. */
|
|
40
|
+
export function shouldAutoContinue(stop, autoContinue = DEFAULT_AUTO_CONTINUE) {
|
|
41
|
+
if (!stop)
|
|
42
|
+
return false;
|
|
43
|
+
const list = autoContinue ?? DEFAULT_AUTO_CONTINUE;
|
|
44
|
+
if (list.length === 0)
|
|
45
|
+
return false;
|
|
46
|
+
return list.includes(stop.kind) && isResumeableStop(stop);
|
|
47
|
+
}
|
|
48
|
+
/** OpenRouter normalized `finish_reason` (+ optional `native_finish_reason`). */
|
|
49
|
+
export function turnStopFromOpenRouter(finishReason, nativeFinishReason) {
|
|
50
|
+
const native = nativeFinishReason?.trim() || finishReason?.trim() || undefined;
|
|
51
|
+
const effective = (nativeFinishReason || finishReason || '').toLowerCase();
|
|
52
|
+
if (!effective)
|
|
53
|
+
return { kind: 'stream_incomplete', native };
|
|
54
|
+
if (effective === 'network_error' || effective.includes('network')) {
|
|
55
|
+
return { kind: 'provider_error', native };
|
|
56
|
+
}
|
|
57
|
+
return openRouterFinishKind((finishReason || '').toLowerCase(), effective, native);
|
|
58
|
+
}
|
|
59
|
+
function openRouterFinishKind(finish, effective, native) {
|
|
60
|
+
if (finish === 'stop') {
|
|
61
|
+
return /error|fail/.test(effective)
|
|
62
|
+
? { kind: 'provider_error', native }
|
|
63
|
+
: { kind: 'completed', native };
|
|
64
|
+
}
|
|
65
|
+
if (finish === 'length')
|
|
66
|
+
return { kind: 'length', native };
|
|
67
|
+
if (finish === 'tool_calls' || finish === 'tool-calls')
|
|
68
|
+
return { kind: 'tool', native };
|
|
69
|
+
if (finish === 'content_filter' || finish === 'content-filter') {
|
|
70
|
+
return { kind: 'filtered', native };
|
|
71
|
+
}
|
|
72
|
+
return { kind: 'provider_error', native };
|
|
73
|
+
}
|
|
74
|
+
/** Gemini Interactions terminal `status`. */
|
|
75
|
+
export function turnStopFromInteractionStatus(status) {
|
|
76
|
+
const s = (status || '').toLowerCase();
|
|
77
|
+
const native = status || undefined;
|
|
78
|
+
switch (s) {
|
|
79
|
+
case 'completed':
|
|
80
|
+
return { kind: 'completed', native };
|
|
81
|
+
case 'incomplete':
|
|
82
|
+
case 'budget_exceeded':
|
|
83
|
+
return { kind: 'length', native };
|
|
84
|
+
case 'requires_action':
|
|
85
|
+
return { kind: 'tool', native };
|
|
86
|
+
case 'cancelled':
|
|
87
|
+
return { kind: 'cancelled', native };
|
|
88
|
+
case 'failed':
|
|
89
|
+
return { kind: 'provider_error', native };
|
|
90
|
+
case 'in_progress':
|
|
91
|
+
case 'queued':
|
|
92
|
+
return { kind: 'stream_incomplete', native };
|
|
93
|
+
default:
|
|
94
|
+
return { kind: 'stream_incomplete', native };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Client SSE ended without a clean terminal event.
|
|
99
|
+
* User Stop → cancelled; otherwise stream_incomplete (tunnel drop, etc.).
|
|
100
|
+
* Returns null when `sawTerminal` so hosts keep the provider stop.
|
|
101
|
+
*/
|
|
102
|
+
export function turnStopFromClientStreamEnd(opts) {
|
|
103
|
+
if (opts.sawTerminal)
|
|
104
|
+
return null;
|
|
105
|
+
if (opts.abortedByUser)
|
|
106
|
+
return { kind: 'cancelled' };
|
|
107
|
+
return { kind: 'stream_incomplete' };
|
|
108
|
+
}
|
|
109
|
+
/** Error hosts throw when classifying an incomplete / non-success stop. */
|
|
110
|
+
export class GenerationStopError extends Error {
|
|
111
|
+
name = 'GenerationStopError';
|
|
112
|
+
stop;
|
|
113
|
+
constructor(stop, message) {
|
|
114
|
+
super(message || `Generation stopped: ${stop.kind}`);
|
|
115
|
+
this.stop = stop;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function isGenerationStopError(err) {
|
|
119
|
+
return err instanceof GenerationStopError;
|
|
120
|
+
}
|
|
@@ -84,6 +84,82 @@ export interface ModelSpec {
|
|
|
84
84
|
* (and builtin routing). Host-owned — e.g. pin image models to `paid`.
|
|
85
85
|
*/
|
|
86
86
|
key?: GeminiBucket;
|
|
87
|
+
/** Optional compaction policy for this model's context window. */
|
|
88
|
+
compaction?: CompactionSpec;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* What the compaction threshold meters.
|
|
92
|
+
*
|
|
93
|
+
* - `'history'` (default) — conversational history only (`TurnInput.historyTokens`
|
|
94
|
+
* or a local estimate of `history`). Excludes system, tool schemas, and this
|
|
95
|
+
* turn's attachments.
|
|
96
|
+
* - `'input'` — full-prompt provider input tokens (`TurnInput.inputTokens` for
|
|
97
|
+
* `timing: 'before'`, this turn's `tokens.input` for `timing: 'after'`).
|
|
98
|
+
*/
|
|
99
|
+
export type CompactionMeter = 'history' | 'input';
|
|
100
|
+
/**
|
|
101
|
+
* Context supplied to a custom compaction trigger.
|
|
102
|
+
*
|
|
103
|
+
* Includes the resolved token count and the spec values so the trigger can
|
|
104
|
+
* incorporate the token-based threshold as a fallback alongside other signals
|
|
105
|
+
* (e.g. available system RAM).
|
|
106
|
+
*/
|
|
107
|
+
export interface CompactionTriggerContext {
|
|
108
|
+
/** Resolved token count for the configured meter. */
|
|
109
|
+
tokens: number;
|
|
110
|
+
/** `CompactionSpec.maxTokens` — the token ceiling for this profile. */
|
|
111
|
+
maxTokens: number;
|
|
112
|
+
/** `CompactionSpec.compactAt` — the fraction at which the default check fires. */
|
|
113
|
+
compactAt: number;
|
|
114
|
+
/** Which meter produced `tokens`. */
|
|
115
|
+
meter: CompactionMeter;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Compaction policy for a model.
|
|
119
|
+
*
|
|
120
|
+
* `previousExchanges` accepts three value ranges:
|
|
121
|
+
* - `≥ 1` (integer) — keep that many recent exchanges (user message + all
|
|
122
|
+
* messages until the next user message).
|
|
123
|
+
* - `(0, 1)` — fraction of `maxTokens`; the retained tail's estimated history
|
|
124
|
+
* tokens must fit within this budget.
|
|
125
|
+
* - `0` — compact everything; no tail is retained.
|
|
126
|
+
*/
|
|
127
|
+
export interface CompactionSpec {
|
|
128
|
+
/**
|
|
129
|
+
* Token budget compared by the trigger (`compactAt * maxTokens`).
|
|
130
|
+
* Meaning depends on `meter`: history budget vs full-prompt input budget.
|
|
131
|
+
*/
|
|
132
|
+
maxTokens: number;
|
|
133
|
+
/** Fraction of `maxTokens` at which compaction fires. Must be in (0, 1). */
|
|
134
|
+
compactAt: number;
|
|
135
|
+
/**
|
|
136
|
+
* How many recent exchanges to preserve verbatim.
|
|
137
|
+
* `≥ 1` = exchange count, `(0, 1)` = fraction of `maxTokens`, `0` = compact all.
|
|
138
|
+
*/
|
|
139
|
+
previousExchanges: number;
|
|
140
|
+
/** Profile id of the compaction agent. Must be registered before the owning profile. */
|
|
141
|
+
profile: ProfileId;
|
|
142
|
+
/**
|
|
143
|
+
* When compaction runs relative to the primary turn.
|
|
144
|
+
* - `'before'`: kernel compacts synchronously before the turn; user pays latency on this turn.
|
|
145
|
+
* - `'after'`: kernel signals in the `done` event; host runs compaction asynchronously.
|
|
146
|
+
*/
|
|
147
|
+
timing: 'before' | 'after';
|
|
148
|
+
/**
|
|
149
|
+
* Threshold meter. Defaults to `'history'`.
|
|
150
|
+
* Use `'input'` when the host prefers provider full-prompt usage (after
|
|
151
|
+
* subtracting a known baseline in `maxTokens` / `compactAt`).
|
|
152
|
+
*/
|
|
153
|
+
meter?: CompactionMeter;
|
|
154
|
+
/**
|
|
155
|
+
* Optional custom trigger. When present, replaces the default token-threshold
|
|
156
|
+
* check (`tokens > compactAt * maxTokens`). The trigger receives full context
|
|
157
|
+
* so it can incorporate the token-based logic as a fallback alongside other
|
|
158
|
+
* signals such as available system RAM.
|
|
159
|
+
*
|
|
160
|
+
* Both sync and async returns are accepted.
|
|
161
|
+
*/
|
|
162
|
+
trigger?: (ctx: CompactionTriggerContext) => boolean | Promise<boolean>;
|
|
87
163
|
}
|
|
88
164
|
/** Static metadata for harness, preset, and host-registered tools. */
|
|
89
165
|
export interface ToolCatalogEntry {
|
|
@@ -176,6 +252,8 @@ export interface ProfileStreamingSpec {
|
|
|
176
252
|
streamThoughts?: boolean;
|
|
177
253
|
gateMedia?: boolean;
|
|
178
254
|
}
|
|
255
|
+
export type { ProfileResumeSpec, TurnContinueFrom, TurnStop, TurnStopKind, } from './stop.js';
|
|
256
|
+
import type { ProfileResumeSpec, TurnContinueFrom, TurnStop } from './stop.js';
|
|
179
257
|
/** Context passed to a host-owned outbound disclosure guard. */
|
|
180
258
|
export interface EgressContext {
|
|
181
259
|
text: string;
|
|
@@ -214,7 +292,7 @@ export interface ProfileGuardrailsSpec {
|
|
|
214
292
|
/** Model, provider, thinking, and step bounds for a profile. */
|
|
215
293
|
export interface ProfileModelSpec {
|
|
216
294
|
protocol: 'geminiInteractions' | 'openAi';
|
|
217
|
-
provider: 'google' | 'openrouter';
|
|
295
|
+
provider: 'google' | 'openrouter' | 'local';
|
|
218
296
|
/** Ids this profile may select. Each id must exist in `config`. */
|
|
219
297
|
allow: ModelId[];
|
|
220
298
|
/** Host-owned wire config keyed by the same ids used in `allow` / `select`. */
|
|
@@ -249,6 +327,8 @@ export interface ProfileOutputsSpec {
|
|
|
249
327
|
speech?: ProfileSpeechSpec;
|
|
250
328
|
validation?: ProfileValidationSpec;
|
|
251
329
|
streaming?: ProfileStreamingSpec;
|
|
330
|
+
/** Resume / Continue policy for non-user stops. */
|
|
331
|
+
resume?: ProfileResumeSpec;
|
|
252
332
|
}
|
|
253
333
|
/** Complete host-owned agent contract consumed by the kernel. */
|
|
254
334
|
export interface Profile {
|
|
@@ -359,6 +439,17 @@ export interface TurnInput {
|
|
|
359
439
|
voice?: TurnBlob[];
|
|
360
440
|
history?: TurnHistoryMessage[];
|
|
361
441
|
repair?: TurnRepairRequest;
|
|
442
|
+
/**
|
|
443
|
+
* Optional host-supplied history token count for `meter: 'history'`.
|
|
444
|
+
* When set, overrides the local history estimate. Not full-prompt API tokens.
|
|
445
|
+
*/
|
|
446
|
+
historyTokens?: number;
|
|
447
|
+
/**
|
|
448
|
+
* Optional host-supplied full-prompt input token count for `meter: 'input'`
|
|
449
|
+
* with `timing: 'before'` (typically the previous turn's `tokens.input`).
|
|
450
|
+
* Ignored when `meter` is `'history'`.
|
|
451
|
+
*/
|
|
452
|
+
inputTokens?: number;
|
|
362
453
|
}
|
|
363
454
|
/** Host request after kernel ingress normalization. */
|
|
364
455
|
export type NormalizedTurnRequest = TurnRequest & {
|
|
@@ -392,11 +483,18 @@ export interface TurnRequest {
|
|
|
392
483
|
* in-flight provider HTTP where the adapter supports it.
|
|
393
484
|
*/
|
|
394
485
|
signal?: AbortSignal;
|
|
486
|
+
/**
|
|
487
|
+
* Continue a prior resumeable stop. Kernel appends CONTINUE_INSTRUCTION to
|
|
488
|
+
* the system prompt; hosts should also pass partial artifact via input/history.
|
|
489
|
+
*/
|
|
490
|
+
continueFrom?: TurnContinueFrom;
|
|
395
491
|
input?: TurnInput;
|
|
396
492
|
toolInvoke?: {
|
|
397
493
|
name: CustomToolId;
|
|
398
494
|
arguments: Record<string, unknown>;
|
|
399
495
|
};
|
|
496
|
+
/** Provider for the compaction profile when `timing: 'before'`. Falls back to the turn provider. */
|
|
497
|
+
compactionProvider?: ModelProvider;
|
|
400
498
|
}
|
|
401
499
|
/** Safe profile projection suitable for UI or host inspection. */
|
|
402
500
|
export interface ProjectedProfile {
|
|
@@ -485,6 +583,20 @@ export interface ProviderEvidenceEvent {
|
|
|
485
583
|
annotations?: unknown[];
|
|
486
584
|
sources?: GroundingSource[];
|
|
487
585
|
}
|
|
586
|
+
/** Compaction signal emitted in the `done` event when `timing: 'after'`. */
|
|
587
|
+
export interface CompactionSignal {
|
|
588
|
+
needed: boolean;
|
|
589
|
+
/** Which meter produced `tokens`. */
|
|
590
|
+
meter: CompactionMeter;
|
|
591
|
+
/** Token count used for the compaction decision. */
|
|
592
|
+
tokens: number;
|
|
593
|
+
/**
|
|
594
|
+
* Provider-reported full-prompt input tokens from this turn, when known.
|
|
595
|
+
* Always observability; also the decision value when `meter: 'input'`.
|
|
596
|
+
*/
|
|
597
|
+
promptTokens?: number;
|
|
598
|
+
history: TurnHistoryMessage[];
|
|
599
|
+
}
|
|
488
600
|
/** Public event yielded by providers and by `runTurn`. */
|
|
489
601
|
export interface TurnEvent {
|
|
490
602
|
type: TurnEventType;
|
|
@@ -508,6 +620,10 @@ export interface TurnEvent {
|
|
|
508
620
|
error?: string;
|
|
509
621
|
/** Raw diagnostic detail for traces/logs; never surface to end users. */
|
|
510
622
|
errorInternal?: string;
|
|
623
|
+
/** Compaction signal for `timing: 'after'` profiles. Present only on `done` events. */
|
|
624
|
+
compaction?: CompactionSignal;
|
|
625
|
+
/** Why the turn ended. Present on terminal `done` events when known. */
|
|
626
|
+
stop?: TurnStop;
|
|
511
627
|
}
|
|
512
628
|
/** Provider-neutral request object sent from the kernel to a model adapter. */
|
|
513
629
|
export interface ProviderCompleteRequest extends ProviderGenerationConfig {
|
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
* Routes from `profile.model.protocol` / `provider` (and whether the profile is
|
|
5
5
|
* a speech role). Adapters under this folder are internal implementation.
|
|
6
6
|
*
|
|
7
|
+
* The OpenRouter / Vercel AI SDK stack is loaded only when an `openAi` +
|
|
8
|
+
* `openrouter` chat provider actually runs `complete` — not when this module
|
|
9
|
+
* is imported, and not for Google or local paths.
|
|
10
|
+
*
|
|
7
11
|
* @module
|
|
8
12
|
*/
|
|
9
13
|
import type { ModelProvider, Profile } from '../kernel/types.js';
|
|
10
14
|
import type { GeminiTransport } from './keys.js';
|
|
15
|
+
import { type LocalProviderConfig } from './local.js';
|
|
11
16
|
import type { OpenRouterConfig } from './openrouter-payload.js';
|
|
12
17
|
/** Credentials supplied by the host when creating a provider. */
|
|
13
18
|
export interface CreateProviderOptions {
|
|
@@ -21,6 +26,8 @@ export interface CreateProviderOptions {
|
|
|
21
26
|
openRouter?: OpenRouterConfig & {
|
|
22
27
|
voice?: string;
|
|
23
28
|
};
|
|
29
|
+
/** Local OpenAI-compatible server (Ollama, llama.cpp, vLLM, LM Studio). */
|
|
30
|
+
local?: LocalProviderConfig;
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
33
|
* Create a `ModelProvider` for a profile.
|
|
@@ -4,16 +4,33 @@
|
|
|
4
4
|
* Routes from `profile.model.protocol` / `provider` (and whether the profile is
|
|
5
5
|
* a speech role). Adapters under this folder are internal implementation.
|
|
6
6
|
*
|
|
7
|
+
* The OpenRouter / Vercel AI SDK stack is loaded only when an `openAi` +
|
|
8
|
+
* `openrouter` chat provider actually runs `complete` — not when this module
|
|
9
|
+
* is imported, and not for Google or local paths.
|
|
10
|
+
*
|
|
7
11
|
* @module
|
|
8
12
|
*/
|
|
9
13
|
import { TheorumError } from '../guardrails/error.js';
|
|
10
|
-
import {
|
|
14
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
15
|
+
import { createLocalProvider } from './local.js';
|
|
11
16
|
import { createInteractionsProvider } from './provider.js';
|
|
12
17
|
import { createSpeechProvider } from './speech.js';
|
|
13
|
-
import { exposeForTests } from './expose-for-tests.js';
|
|
14
18
|
function isSpeechRole(profile) {
|
|
15
19
|
return profile.outputs.speech !== undefined;
|
|
16
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Defer loading `@openrouter/ai-sdk-provider` / `ai` until the first `complete`.
|
|
23
|
+
* Keeps Google and local hosts free of the Vercel SDK graph.
|
|
24
|
+
*/
|
|
25
|
+
function lazyOpenRouterChat(config) {
|
|
26
|
+
let pending;
|
|
27
|
+
return {
|
|
28
|
+
async *complete(req) {
|
|
29
|
+
pending ??= import('./openrouter.js').then((m) => m.createOpenRouterProvider(config));
|
|
30
|
+
yield* (await pending).complete(req);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
17
34
|
/**
|
|
18
35
|
* Create a `ModelProvider` for a profile.
|
|
19
36
|
* One call: protocol/provider (and speech role) pick the transport.
|
|
@@ -33,8 +50,11 @@ export function createProvider(profile, options = {}) {
|
|
|
33
50
|
if (isSpeechRole(profile)) {
|
|
34
51
|
return createSpeechProvider(options.openRouter);
|
|
35
52
|
}
|
|
36
|
-
return
|
|
53
|
+
return lazyOpenRouterChat(options.openRouter);
|
|
54
|
+
}
|
|
55
|
+
if (protocol === 'openAi' && provider === 'local') {
|
|
56
|
+
return createLocalProvider(options.local);
|
|
37
57
|
}
|
|
38
58
|
throw new TheorumError(`createProvider: unsupported protocol/provider pair '${protocol}'/'${provider}'`);
|
|
39
59
|
}
|
|
40
|
-
exposeForTests('create-provider', { isSpeechRole, createProvider });
|
|
60
|
+
exposeForTests('create-provider', { isSpeechRole, createProvider, lazyOpenRouterChat });
|
|
@@ -16,6 +16,10 @@ export function exposeForTests(bucket, api) {
|
|
|
16
16
|
if (!enabled)
|
|
17
17
|
return;
|
|
18
18
|
const g = dntShim.dntGlobalThis;
|
|
19
|
-
|
|
19
|
+
let root = g.__theorumTestInternals;
|
|
20
|
+
if (!root) {
|
|
21
|
+
root = {};
|
|
22
|
+
g.__theorumTestInternals = root;
|
|
23
|
+
}
|
|
20
24
|
root[bucket] = api;
|
|
21
25
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local provider adapter for OpenAI-compatible endpoints (Ollama, llama.cpp,
|
|
3
|
+
* vLLM, LM Studio, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Streams SSE from `/v1/chat/completions`, accumulates tool calls, and yields
|
|
6
|
+
* normalized `TurnEvent` objects. No external SDK dependency — raw fetch + SSE.
|
|
7
|
+
*
|
|
8
|
+
* Hosts pass `baseUrl` explicitly. THEORUM does not read `OLLAMA_HOST` or other
|
|
9
|
+
* environment variables (see docs/SECRETS.md).
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { ModelProvider } from '../kernel/types.js';
|
|
14
|
+
/** Default OpenAI-compat base when the host omits `baseUrl` (Ollama's default port). */
|
|
15
|
+
export declare const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:11434";
|
|
16
|
+
/** Host-supplied config for the local provider. */
|
|
17
|
+
export interface LocalProviderConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Base URL of the OpenAI-compat server (no trailing slash).
|
|
20
|
+
* Defaults to `http://127.0.0.1:11434`. Hosts that honor `OLLAMA_HOST` should
|
|
21
|
+
* resolve it themselves and pass the result here.
|
|
22
|
+
*/
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
/** Custom fetch implementation for testing or proxying. */
|
|
25
|
+
fetch?: typeof globalThis.fetch;
|
|
26
|
+
}
|
|
27
|
+
/** Create a `ModelProvider` for a local OpenAI-compatible server (Ollama, llama.cpp, vLLM, LM Studio). */
|
|
28
|
+
declare function createLocalProvider(config?: LocalProviderConfig): ModelProvider;
|
|
29
|
+
export { createLocalProvider };
|