theorum 1.0.0 → 1.1.3
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 +81 -25
- package/esm/mod.d.ts +48 -29
- package/esm/mod.js +37 -24
- package/esm/src/cli/commands/bench.js +13 -16
- package/esm/src/cli/commands/fuzz-canary.js +25 -29
- package/esm/src/cli/commands/guardrails-eval.d.ts +14 -0
- package/esm/src/cli/commands/guardrails-eval.js +15 -0
- package/esm/src/cli/commands/profile.js +12 -3
- package/esm/src/cli/commands/run.js +8 -9
- package/esm/src/cli/commands/test.js +8 -8
- package/esm/src/cli/event-log.d.ts +1 -1
- package/esm/src/cli/index.js +12 -0
- package/esm/src/cli/matrix/synthesizer.d.ts +6 -6
- package/esm/src/cli/matrix/synthesizer.js +23 -22
- package/esm/src/guardrails/canary-gate.d.ts +2 -1
- package/esm/src/guardrails/canary-gate.js +2 -1
- package/esm/src/guardrails/canary.d.ts +9 -1
- package/esm/src/guardrails/canary.js +21 -10
- package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +1 -0
- package/esm/src/guardrails/corpus/fuzz-inbound.js +12 -15
- package/esm/src/guardrails/corpus/inbound-payloads.js +1 -0
- package/esm/src/guardrails/corpus/live-attacks.js +0 -1
- package/esm/src/guardrails/corpus/mod.d.ts +1 -0
- package/esm/src/guardrails/corpus/secrets.d.ts +1 -0
- package/esm/src/guardrails/corpus/secrets.js +1 -0
- package/esm/src/guardrails/corpus/strings.d.ts +1 -0
- package/esm/src/guardrails/corpus/strings.js +1 -0
- package/esm/src/guardrails/corpus/types.d.ts +1 -0
- package/esm/src/guardrails/egress.d.ts +26 -3
- package/esm/src/guardrails/egress.js +69 -18
- package/esm/src/guardrails/error.d.ts +14 -23
- package/esm/src/guardrails/error.js +86 -77
- package/esm/src/guardrails/eval/corpus.d.ts +108 -0
- package/esm/src/guardrails/eval/corpus.js +978 -0
- package/esm/src/guardrails/eval/mod.d.ts +51 -0
- package/esm/src/guardrails/eval/mod.js +133 -0
- package/esm/src/guardrails/eval/score.d.ts +66 -0
- package/esm/src/guardrails/eval/score.js +114 -0
- package/esm/src/guardrails/events.d.ts +25 -0
- package/esm/src/guardrails/events.js +56 -0
- package/esm/src/guardrails/hits.d.ts +24 -0
- package/esm/src/guardrails/hits.js +45 -0
- package/esm/src/guardrails/lexicon.d.ts +39 -0
- package/esm/src/guardrails/lexicon.js +200 -0
- package/esm/src/guardrails/live-outbound-gate.d.ts +15 -13
- package/esm/src/guardrails/live-outbound-gate.js +131 -89
- package/esm/src/guardrails/mod.d.ts +19 -4
- package/esm/src/guardrails/mod.js +12 -3
- package/esm/src/guardrails/network.d.ts +19 -0
- package/esm/src/guardrails/network.js +234 -0
- package/esm/src/guardrails/policy.d.ts +35 -0
- package/esm/src/guardrails/policy.js +50 -0
- package/esm/src/guardrails/progressive-yield.d.ts +51 -0
- package/esm/src/guardrails/progressive-yield.js +98 -0
- package/esm/src/guardrails/quota.d.ts +17 -3
- package/esm/src/guardrails/quota.js +18 -4
- package/esm/src/guardrails/sanitize.d.ts +36 -6
- package/esm/src/guardrails/sanitize.js +152 -60
- package/esm/src/guardrails/serialize.d.ts +35 -0
- package/esm/src/guardrails/serialize.js +58 -0
- package/esm/src/guardrails/testing.d.ts +7 -0
- package/esm/src/guardrails/testing.js +4 -0
- package/esm/src/guardrails/theorum-error.d.ts +12 -0
- package/esm/src/guardrails/theorum-error.js +15 -0
- package/esm/src/guardrails/tool-directives.d.ts +48 -0
- package/esm/src/guardrails/tool-directives.js +124 -0
- package/esm/src/guardrails/tool-result.d.ts +93 -0
- package/esm/src/guardrails/tool-result.js +276 -0
- package/esm/src/guardrails/types.d.ts +291 -0
- package/esm/src/guardrails/types.js +72 -0
- package/esm/src/host/client-turn.js +4 -0
- package/esm/src/host/mint-trace.d.ts +1 -1
- package/esm/src/kernel/auth/crypto.d.ts +42 -0
- package/esm/src/kernel/auth/crypto.js +106 -0
- package/esm/src/kernel/auth/mod.d.ts +11 -0
- package/esm/src/kernel/auth/mod.js +11 -0
- package/esm/src/kernel/auth/oauth.d.ts +47 -0
- package/esm/src/kernel/auth/oauth.js +278 -0
- package/esm/src/kernel/auth/types.d.ts +133 -0
- package/esm/src/kernel/auth/types.js +13 -0
- package/esm/src/kernel/engine/delta.js +43 -2
- package/esm/src/kernel/engine/live-inbound.d.ts +15 -3
- package/esm/src/kernel/engine/live-inbound.js +22 -8
- package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
- package/esm/src/kernel/engine/live-ingress.js +47 -0
- package/esm/src/kernel/engine/repair.js +13 -12
- package/esm/src/kernel/engine/runner/gates.js +127 -40
- package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
- package/esm/src/kernel/engine/runner/mod.js +182 -43
- package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
- package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
- package/esm/src/kernel/engine/runner/stages.js +89 -0
- package/esm/src/kernel/engine/runner/state.d.ts +24 -0
- package/esm/src/kernel/engine/runner/steps.js +128 -61
- package/esm/src/kernel/engine/runner/stream.d.ts +9 -2
- package/esm/src/kernel/engine/runner/stream.js +114 -55
- package/esm/src/kernel/engine/session/mod.d.ts +3 -1
- package/esm/src/kernel/engine/session/mod.js +386 -34
- package/esm/src/kernel/interaction-parts.d.ts +14 -0
- package/esm/src/kernel/interaction-parts.js +23 -0
- package/esm/src/kernel/mod.d.ts +19 -7
- package/esm/src/kernel/mod.js +10 -5
- package/esm/src/kernel/profile-graph.d.ts +159 -0
- package/esm/src/kernel/profile-graph.js +156 -0
- package/esm/src/kernel/registry/attachments.d.ts +10 -10
- package/esm/src/kernel/registry/attachments.js +29 -26
- package/esm/src/kernel/registry/catalog.d.ts +25 -11
- package/esm/src/kernel/registry/catalog.js +59 -24
- package/esm/src/kernel/registry/ingress.js +34 -22
- package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
- package/esm/src/kernel/registry/profile-outputs.js +8 -0
- package/esm/src/kernel/registry/profiles.d.ts +34 -14
- package/esm/src/kernel/registry/profiles.js +306 -59
- package/esm/src/kernel/registry/provider-request.js +2 -0
- package/esm/src/kernel/registry/resolve.d.ts +7 -6
- package/esm/src/kernel/registry/resolve.js +102 -117
- package/esm/src/kernel/registry/schemas.js +1 -1
- package/esm/src/kernel/registry/sole-model.d.ts +8 -0
- package/esm/src/kernel/registry/sole-model.js +10 -0
- package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
- package/esm/src/kernel/registry/system-prompt.js +40 -0
- package/esm/src/kernel/registry/system-role.d.ts +8 -0
- package/esm/src/kernel/registry/system-role.js +14 -0
- package/esm/src/kernel/registry/vault.d.ts +2 -2
- package/esm/src/kernel/registry/vault.js +4 -4
- package/esm/src/kernel/schema.d.ts +97 -11
- package/esm/src/kernel/schema.js +256 -111
- package/esm/src/kernel/stages.d.ts +175 -0
- package/esm/src/kernel/stages.js +476 -0
- package/esm/src/kernel/stop.d.ts +65 -11
- package/esm/src/kernel/stop.js +46 -11
- package/esm/src/kernel/tools/events.d.ts +41 -0
- package/esm/src/kernel/tools/events.js +71 -0
- package/esm/src/kernel/tools/execute.d.ts +63 -24
- package/esm/src/kernel/tools/execute.js +475 -224
- package/esm/src/kernel/tools/harness.js +16 -14
- package/esm/src/kernel/tools/invoke.js +26 -8
- package/esm/src/kernel/tools/mod.d.ts +3 -1
- package/esm/src/kernel/tools/mod.js +2 -1
- package/esm/src/kernel/tools/permission.d.ts +15 -0
- package/esm/src/kernel/tools/permission.js +47 -0
- package/esm/src/kernel/tools/project.js +5 -3
- package/esm/src/kernel/tools/registry.js +20 -10
- package/esm/src/kernel/tools/remote.d.ts +94 -0
- package/esm/src/kernel/tools/remote.js +577 -0
- package/esm/src/kernel/tools/resolve.d.ts +17 -7
- package/esm/src/kernel/tools/resolve.js +38 -17
- package/esm/src/kernel/tools/schema.d.ts +2 -0
- package/esm/src/kernel/tools/schema.js +23 -6
- package/esm/src/kernel/tools/stage-run.d.ts +105 -0
- package/esm/src/kernel/tools/stage-run.js +155 -0
- package/esm/src/kernel/tools/types.d.ts +198 -20
- package/esm/src/kernel/types.d.ts +253 -114
- package/esm/src/kernel/util/find-last.d.ts +2 -0
- package/esm/src/kernel/util/find-last.js +10 -0
- package/esm/src/observability/destinations.d.ts +31 -0
- package/esm/src/observability/destinations.js +67 -0
- package/esm/src/observability/mod.d.ts +10 -3
- package/esm/src/observability/mod.js +6 -2
- package/esm/src/observability/policy.d.ts +27 -0
- package/esm/src/observability/policy.js +80 -0
- package/esm/src/observability/resolve-policy.d.ts +16 -0
- package/esm/src/observability/resolve-policy.js +64 -0
- package/esm/src/observability/trace-attach.d.ts +3 -1
- package/esm/src/observability/trace-attach.js +17 -10
- package/esm/src/observability/trace-record.d.ts +12 -4
- package/esm/src/observability/trace-record.js +63 -19
- package/esm/src/observability/trace-sink.d.ts +19 -0
- package/esm/src/observability/trace-sink.js +10 -0
- package/esm/src/observability/trace.d.ts +14 -11
- package/esm/src/observability/trace.js +23 -14
- package/esm/src/observability/types.d.ts +113 -0
- package/esm/src/observability/types.js +11 -0
- package/esm/src/providers/create-provider.d.ts +7 -4
- package/esm/src/providers/create-provider.js +21 -7
- package/esm/src/providers/google/interactions/framing.d.ts +8 -0
- package/esm/src/providers/google/interactions/framing.js +74 -13
- package/esm/src/providers/google/live/framing.d.ts +5 -2
- package/esm/src/providers/google/live/framing.js +49 -12
- package/esm/src/providers/google/live/openapi-schema.js +3 -2
- package/esm/src/providers/google/live/stream.d.ts +9 -0
- package/esm/src/providers/google/live/stream.js +16 -3
- package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
- package/esm/src/providers/openrouter/cache-control.js +23 -0
- package/esm/src/providers/openrouter/chat.d.ts +21 -1
- package/esm/src/providers/openrouter/chat.js +51 -6
- package/esm/src/providers/openrouter/image.js +8 -19
- package/esm/src/providers/openrouter/openai/chat-payload.js +23 -1
- package/esm/src/providers/openrouter/openai/compat.d.ts +7 -1
- package/esm/src/providers/openrouter/openai/compat.js +23 -4
- package/esm/src/providers/openrouter/openai/image-payload.js +8 -2
- package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +1 -1
- package/esm/src/providers/openrouter/openai/sdk-messages.js +32 -5
- package/package.json +1 -1
- package/esm/src/interface/blocks.d.ts +0 -20
- package/esm/src/interface/blocks.js +0 -180
- package/esm/src/interface/from-profile.d.ts +0 -14
- package/esm/src/interface/from-profile.js +0 -104
- package/esm/src/interface/inputs.d.ts +0 -8
- package/esm/src/interface/inputs.js +0 -127
- package/esm/src/interface/mod.d.ts +0 -10
- package/esm/src/interface/mod.js +0 -9
- package/esm/src/interface/types.d.ts +0 -153
- package/esm/src/interface/types.js +0 -13
|
@@ -2,50 +2,225 @@
|
|
|
2
2
|
* Runtime profile registry for host-owned THEORUM profiles.
|
|
3
3
|
*
|
|
4
4
|
* THEORUM ships profile types — not application profiles and not invented defaults.
|
|
5
|
-
* Hosts must pass required fields explicitly (`type`, `
|
|
5
|
+
* Hosts must pass required fields explicitly (`type`, `models`, …).
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { TheorumError } from '../../guardrails/error.js';
|
|
10
|
-
import {
|
|
10
|
+
import { HOST_GUARDRAIL_FIELDS, } from '../../guardrails/types.js';
|
|
11
|
+
import { resolveObservabilityPolicy } from '../../observability/resolve-policy.js';
|
|
12
|
+
import { assertLiveIngressConfigured } from '../engine/live-ingress.js';
|
|
13
|
+
import { CACHE_MODES, CACHE_TTLS, isValidPair, isValidProfileProtocol, protocolsForProfileType, } from '../schema.js';
|
|
14
|
+
import { isContinueStopKind } from '../stop.js';
|
|
11
15
|
import { getTool } from '../tools/registry.js';
|
|
16
|
+
import { soleModelId } from './sole-model.js';
|
|
12
17
|
const profiles = new Map();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
/** Fields a `host` profile must not carry — rejected when supplied. */
|
|
19
|
+
const HOST_ABSENT_FIELDS = [
|
|
20
|
+
'models',
|
|
21
|
+
'defaultModel',
|
|
22
|
+
'allowModelSelect',
|
|
23
|
+
'identity',
|
|
24
|
+
'inputs',
|
|
25
|
+
'outputs',
|
|
26
|
+
'turnBehaviour',
|
|
27
|
+
'key',
|
|
28
|
+
'maxSteps',
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Guardrails that only a model turn can run, so a host profile must not declare
|
|
32
|
+
* them: `egress` gates user-visible model text in the turn runner, `canary` is
|
|
33
|
+
* minted into a system prompt, and `quota` counts turns. None is reachable from
|
|
34
|
+
* `invokeTool`, so accepting them would register config that guards nothing.
|
|
35
|
+
*/
|
|
36
|
+
const HOST_ABSENT_GUARDRAILS = ['quota', 'canary', 'egress'];
|
|
37
|
+
const HOST_GUARDRAIL_REASON = {
|
|
38
|
+
quota: 'quota counts model turns', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
39
|
+
canary: 'canary is minted into a system prompt', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
40
|
+
egress: 'egress gates user-visible model text in the turn runner', // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
41
|
+
};
|
|
42
|
+
function assertHostGuardrails(profileId, guardrails) {
|
|
43
|
+
if (!guardrails)
|
|
44
|
+
return;
|
|
45
|
+
const extra = guardrails;
|
|
46
|
+
for (const key of HOST_ABSENT_GUARDRAILS) {
|
|
47
|
+
if (extra[key] !== undefined) {
|
|
48
|
+
throw new TheorumError(`Profile ${profileId}: type 'host' must not set guardrails.${key} — a host profile runs no model and ${HOST_GUARDRAIL_REASON[key]}. Host profiles accept ${HOST_GUARDRAIL_FIELDS.join(', ')}.`);
|
|
17
49
|
}
|
|
18
50
|
}
|
|
19
51
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
52
|
+
function defineHostProfile(input) {
|
|
53
|
+
const extra = input;
|
|
54
|
+
for (const key of HOST_ABSENT_FIELDS) {
|
|
55
|
+
if (extra[key] !== undefined) {
|
|
56
|
+
throw new TheorumError(`Profile ${input.id}: type 'host' must not set ${key}`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
57
|
+
}
|
|
23
58
|
}
|
|
24
|
-
|
|
25
|
-
|
|
59
|
+
assertHostGuardrails(input.id, input.guardrails);
|
|
60
|
+
assertHostTools(input.id, input.tools);
|
|
61
|
+
assertObservability(input.id, input.observability);
|
|
62
|
+
return {
|
|
63
|
+
type: 'host',
|
|
64
|
+
id: input.id,
|
|
65
|
+
tools: { allow: input.tools.allow },
|
|
66
|
+
guardrails: input.guardrails,
|
|
67
|
+
observability: input.observability,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function assertHostTools(profileId, tools) {
|
|
71
|
+
if (!Array.isArray(tools?.allow)) {
|
|
72
|
+
throw new TheorumError(`Profile ${profileId}: type 'host' must set tools.allow`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
26
73
|
}
|
|
27
|
-
|
|
28
|
-
|
|
74
|
+
const extra = tools;
|
|
75
|
+
if (extra.t1Policy !== undefined || extra.t2Loader !== undefined) {
|
|
76
|
+
throw new TheorumError(`Profile ${profileId}: tools.t1Policy / tools.t2Loader are not supported on type 'host' — every allowed tool is executable`);
|
|
29
77
|
}
|
|
30
78
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
79
|
+
function assertModelsNonEmpty(profileId, models) {
|
|
80
|
+
if (Object.keys(models).length === 0) {
|
|
81
|
+
throw new TheorumError(`Profile ${profileId} must declare at least one model`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function assertDefaultModel(profileId, input) {
|
|
85
|
+
const ids = Object.keys(input.models);
|
|
86
|
+
const inferred = input.defaultModel ?? soleModelId(input.models);
|
|
87
|
+
if (!inferred) {
|
|
88
|
+
throw new TheorumError(`Profile ${profileId} must set defaultModel when more than one model is declared`);
|
|
89
|
+
}
|
|
90
|
+
if (!input.models[inferred]) {
|
|
91
|
+
throw new TheorumError(`Profile ${profileId} defaultModel '${inferred}' is not declared`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
92
|
+
}
|
|
93
|
+
if (input.allowModelSelect && ids.length < 2) {
|
|
94
|
+
throw new TheorumError(`Profile ${profileId} allowModelSelect requires at least two models`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function assertModelBinding(profileId, modelId, binding) {
|
|
98
|
+
if (!binding.protocol) {
|
|
99
|
+
throw new TheorumError(`Profile ${profileId} model '${modelId}' must set protocol`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
100
|
+
}
|
|
101
|
+
if (!binding.provider) {
|
|
102
|
+
throw new TheorumError(`Profile ${profileId} model '${modelId}' must set provider`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
103
|
+
}
|
|
104
|
+
if (!binding.apiId) {
|
|
105
|
+
throw new TheorumError(`Profile ${profileId} model '${modelId}' must set apiId`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
106
|
+
}
|
|
107
|
+
if (!isValidPair(binding.protocol, binding.provider)) {
|
|
108
|
+
throw new TheorumError(`Profile ${profileId} model '${modelId}': protocol '${binding.protocol}' is not valid for provider '${binding.provider}'`);
|
|
109
|
+
}
|
|
110
|
+
assertModelEfforts(profileId, modelId, binding);
|
|
111
|
+
if (binding.cache) {
|
|
112
|
+
assertCacheSpec(profileId, modelId, binding);
|
|
113
|
+
}
|
|
114
|
+
assertInteractionsPersistence(profileId, modelId, binding);
|
|
115
|
+
}
|
|
116
|
+
function assertModelEfforts(profileId, modelId, binding) {
|
|
117
|
+
const efforts = binding.efforts;
|
|
118
|
+
if (!efforts || Object.keys(efforts).length === 0) {
|
|
119
|
+
if (binding.defaultEffort || binding.allowEffortSelect) {
|
|
120
|
+
throw new TheorumError(`Profile ${profileId} model '${modelId}': defaultEffort and allowEffortSelect require efforts`);
|
|
121
|
+
}
|
|
34
122
|
return;
|
|
35
|
-
|
|
36
|
-
|
|
123
|
+
}
|
|
124
|
+
const keys = Object.keys(efforts);
|
|
125
|
+
const defaultAlias = binding.defaultEffort ?? (keys.length === 1 ? keys[0] : undefined);
|
|
126
|
+
if (!defaultAlias) {
|
|
127
|
+
throw new TheorumError(`Profile ${profileId} model '${modelId}' must set defaultEffort when more than one effort is declared`);
|
|
128
|
+
}
|
|
129
|
+
if (!efforts[defaultAlias]) {
|
|
130
|
+
throw new TheorumError(`Profile ${profileId} model '${modelId}' defaultEffort '${defaultAlias}' is not declared`);
|
|
131
|
+
}
|
|
132
|
+
if (binding.allowEffortSelect && keys.length < 2) {
|
|
133
|
+
throw new TheorumError(`Profile ${profileId} model '${modelId}' allowEffortSelect requires at least two efforts`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function assertTypeProtocols(profile) {
|
|
137
|
+
for (const [modelId, binding] of Object.entries(profile.models)) {
|
|
138
|
+
if (!isValidProfileProtocol(profile.type, binding.protocol)) {
|
|
139
|
+
const valid = protocolsForProfileType(profile.type).join(', ');
|
|
140
|
+
throw new TheorumError(`Profile ${profile.id} model '${modelId}': type '${profile.type}' cannot use protocol '${binding.protocol}'. Supported: ${valid}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function profileModelFields(input) {
|
|
145
|
+
return {
|
|
146
|
+
models: input.models,
|
|
147
|
+
defaultModel: input.defaultModel ?? soleModelId(input.models),
|
|
148
|
+
allowModelSelect: input.allowModelSelect,
|
|
149
|
+
maxSteps: input.maxSteps,
|
|
150
|
+
key: input.key,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function assertContinueKindList(profileId, path, kinds) {
|
|
154
|
+
if (!kinds?.length)
|
|
155
|
+
return;
|
|
156
|
+
for (const kind of kinds) {
|
|
157
|
+
if (!isContinueStopKind(kind)) {
|
|
158
|
+
throw new TheorumError(`Profile ${profileId}: turnBehaviour.resumption.${path} may only include ContinueStopKind ` + // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
159
|
+
`(length | stream_incomplete | provider_error); got '${kind}'`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function assertResumption(profileId, resumption) {
|
|
164
|
+
if (!resumption)
|
|
165
|
+
return;
|
|
166
|
+
assertContinueKindList(profileId, 'allowContinue', resumption.allowContinue);
|
|
167
|
+
assertContinueKindList(profileId, 'autoContinue', resumption.autoContinue);
|
|
168
|
+
}
|
|
169
|
+
function assertTurnBehaviour(profileId, input) {
|
|
170
|
+
if (input.type === 'host')
|
|
171
|
+
return;
|
|
172
|
+
if (input.type === 'live') {
|
|
173
|
+
const tb = input.turnBehaviour;
|
|
174
|
+
if (tb?.resumption !== undefined) {
|
|
175
|
+
throw new TheorumError(`Profile ${profileId}: type 'live' uses live.sessionResumption, not turnBehaviour.resumption`);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (input.type !== 'text' && input.turnBehaviour?.allowSteering !== undefined) {
|
|
180
|
+
throw new TheorumError(`Profile ${profileId}: turnBehaviour.allowSteering is only valid on type 'text' or 'live'`);
|
|
181
|
+
}
|
|
182
|
+
assertResumption(profileId, input.turnBehaviour?.resumption);
|
|
183
|
+
}
|
|
184
|
+
function assertObservability(profileId, spec) {
|
|
185
|
+
if (!spec) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const policy = resolveObservabilityPolicy(spec);
|
|
190
|
+
if (policy.retainForDays <= 0 || !Number.isFinite(policy.retainForDays)) {
|
|
191
|
+
throw new TheorumError(`Profile ${profileId}: observability.retainForDays must be a positive number`);
|
|
192
|
+
}
|
|
193
|
+
if (policy.rotateAfterMiB <= 0 || !Number.isFinite(policy.rotateAfterMiB)) {
|
|
194
|
+
throw new TheorumError(`Profile ${profileId}: observability.rotateAfterMiB must be a positive number`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
if (err instanceof TheorumError && err.message.startsWith('Profile ')) {
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
202
|
+
throw new TheorumError(`Profile ${profileId}: ${message}`);
|
|
203
|
+
}
|
|
37
204
|
}
|
|
38
|
-
/** Define a typed profile. Required fields must be set explicitly; optional fields stay optional. */
|
|
39
205
|
function defineProfile(input) {
|
|
40
|
-
|
|
41
|
-
|
|
206
|
+
if (input.type === 'host') {
|
|
207
|
+
return defineHostProfile(input);
|
|
208
|
+
}
|
|
209
|
+
assertModelsNonEmpty(input.id, input.models);
|
|
210
|
+
assertDefaultModel(input.id, input);
|
|
211
|
+
assertTurnBehaviour(input.id, input);
|
|
212
|
+
assertObservability(input.id, input.observability);
|
|
213
|
+
for (const [modelId, binding] of Object.entries(input.models)) {
|
|
214
|
+
assertModelBinding(input.id, modelId, binding);
|
|
215
|
+
}
|
|
42
216
|
const identity = {
|
|
43
217
|
handle: input.identity.handle,
|
|
44
218
|
system: input.identity.system,
|
|
45
219
|
systemByRole: input.identity.systemByRole,
|
|
46
220
|
};
|
|
47
|
-
const outputs = input.outputs;
|
|
48
221
|
const guardrails = input.guardrails;
|
|
222
|
+
const observability = input.observability;
|
|
223
|
+
const modelFields = profileModelFields(input);
|
|
49
224
|
let profile;
|
|
50
225
|
switch (input.type) {
|
|
51
226
|
case 'text':
|
|
@@ -53,12 +228,13 @@ function defineProfile(input) {
|
|
|
53
228
|
type: 'text',
|
|
54
229
|
id: input.id,
|
|
55
230
|
identity,
|
|
56
|
-
|
|
231
|
+
...modelFields,
|
|
57
232
|
tools: input.tools,
|
|
58
233
|
inputs: input.inputs,
|
|
59
|
-
outputs,
|
|
60
|
-
|
|
234
|
+
outputs: input.outputs,
|
|
235
|
+
turnBehaviour: input.turnBehaviour,
|
|
61
236
|
guardrails,
|
|
237
|
+
observability,
|
|
62
238
|
};
|
|
63
239
|
break;
|
|
64
240
|
case 'image':
|
|
@@ -66,13 +242,14 @@ function defineProfile(input) {
|
|
|
66
242
|
type: 'image',
|
|
67
243
|
id: input.id,
|
|
68
244
|
identity,
|
|
69
|
-
|
|
245
|
+
...modelFields,
|
|
70
246
|
image: input.image,
|
|
71
247
|
tools: input.tools,
|
|
72
248
|
inputs: input.inputs,
|
|
73
|
-
outputs,
|
|
74
|
-
|
|
249
|
+
outputs: input.outputs,
|
|
250
|
+
turnBehaviour: input.turnBehaviour,
|
|
75
251
|
guardrails,
|
|
252
|
+
observability,
|
|
76
253
|
};
|
|
77
254
|
break;
|
|
78
255
|
case 'speech':
|
|
@@ -80,56 +257,96 @@ function defineProfile(input) {
|
|
|
80
257
|
type: 'speech',
|
|
81
258
|
id: input.id,
|
|
82
259
|
identity,
|
|
83
|
-
|
|
260
|
+
...modelFields,
|
|
84
261
|
speech: input.speech,
|
|
85
|
-
outputs,
|
|
86
|
-
|
|
262
|
+
outputs: input.outputs,
|
|
263
|
+
turnBehaviour: input.turnBehaviour,
|
|
87
264
|
guardrails,
|
|
265
|
+
observability,
|
|
88
266
|
};
|
|
89
267
|
break;
|
|
90
|
-
case 'live':
|
|
268
|
+
case 'live': {
|
|
269
|
+
const liveInput = input;
|
|
270
|
+
if (liveInput.inputs !== undefined) {
|
|
271
|
+
throw new TheorumError(`Profile ${input.id}: type 'live' must not set inputs`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
272
|
+
}
|
|
273
|
+
if (liveInput.outputs !== undefined) {
|
|
274
|
+
throw new TheorumError(`Profile ${input.id}: type 'live' must not set outputs`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
275
|
+
}
|
|
91
276
|
profile = {
|
|
92
277
|
type: 'live',
|
|
93
278
|
id: input.id,
|
|
94
279
|
identity,
|
|
95
|
-
|
|
280
|
+
...modelFields,
|
|
96
281
|
live: input.live,
|
|
97
|
-
tools: input.tools,
|
|
98
|
-
|
|
99
|
-
outputs,
|
|
282
|
+
tools: assertLiveTools(input.id, input.tools),
|
|
283
|
+
turnBehaviour: liveInput.turnBehaviour,
|
|
100
284
|
guardrails,
|
|
285
|
+
observability,
|
|
101
286
|
};
|
|
287
|
+
assertLiveIngressConfigured(profile);
|
|
102
288
|
break;
|
|
289
|
+
}
|
|
103
290
|
default: {
|
|
104
291
|
const _exhaustive = input;
|
|
105
|
-
throw new TheorumError(`Unknown profile type '${String(_exhaustive)}'`);
|
|
292
|
+
throw new TheorumError(`Unknown profile type '${String(_exhaustive)}'`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
106
293
|
}
|
|
107
294
|
}
|
|
108
|
-
|
|
295
|
+
assertTypeProtocols(profile);
|
|
109
296
|
return profile;
|
|
110
297
|
}
|
|
111
298
|
function assertCompactionSpec(profileId, modelId, spec) {
|
|
112
|
-
const tag = `Profile ${profileId} model ${modelId} compaction`;
|
|
299
|
+
const tag = `Profile ${profileId} model ${modelId} compaction`; // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
113
300
|
assertCompactionBudget(tag, spec);
|
|
114
301
|
assertCompactionRetain(tag, spec);
|
|
115
302
|
if (spec.meter != null && spec.meter !== 'history' && spec.meter !== 'input') {
|
|
116
|
-
throw new TheorumError(`${tag}: meter must be 'history' or 'input'`);
|
|
303
|
+
throw new TheorumError(`${tag}: meter must be 'history' or 'input'`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
117
304
|
}
|
|
118
305
|
if (!profiles.has(spec.profile)) {
|
|
119
306
|
throw new TheorumError(`${tag}: compaction profile '${spec.profile}' must be registered before '${profileId}'`);
|
|
120
307
|
}
|
|
121
308
|
}
|
|
309
|
+
function assertCacheSpec(profileId, modelId, binding) {
|
|
310
|
+
const spec = binding.cache;
|
|
311
|
+
if (!spec) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const tag = `Profile ${profileId} model '${modelId}'`;
|
|
315
|
+
if (binding.provider !== 'openrouter' || binding.protocol !== 'openAi') {
|
|
316
|
+
throw new TheorumError(`${tag}: cache is only valid when protocol is 'openAi' and provider is 'openrouter'`);
|
|
317
|
+
}
|
|
318
|
+
if (!CACHE_MODES.includes(spec.mode)) {
|
|
319
|
+
throw new TheorumError(`${tag}: cache.mode must be one of ${CACHE_MODES.join(' | ')}`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
320
|
+
}
|
|
321
|
+
if (spec.ttl != null && !CACHE_TTLS.includes(spec.ttl)) {
|
|
322
|
+
throw new TheorumError(`${tag}: cache.ttl must be one of ${CACHE_TTLS.join(' | ')}`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function assertInteractionsPersistence(profileId, modelId, binding) {
|
|
326
|
+
if (binding.store === undefined && binding.persistViaInteractionId === undefined) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (binding.protocol === 'geminiInteractions' && binding.provider === 'google') {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const which = binding.store !== undefined && binding.persistViaInteractionId !== undefined
|
|
333
|
+
? 'store and persistViaInteractionId' // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
334
|
+
: binding.store !== undefined
|
|
335
|
+
? 'store'
|
|
336
|
+
: 'persistViaInteractionId';
|
|
337
|
+
throw new TheorumError(`Profile ${profileId} model '${modelId}': ${which} is only valid when protocol is 'geminiInteractions' and provider is 'google'`);
|
|
338
|
+
}
|
|
122
339
|
function assertCompactionBudget(tag, spec) {
|
|
123
340
|
if (spec.maxTokens <= 0) {
|
|
124
|
-
throw new TheorumError(`${tag}: maxTokens must be > 0`);
|
|
341
|
+
throw new TheorumError(`${tag}: maxTokens must be > 0`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
125
342
|
}
|
|
126
343
|
if (spec.compactAt <= 0 || spec.compactAt >= 1) {
|
|
127
|
-
throw new TheorumError(`${tag}: compactAt must be in (0, 1)`);
|
|
344
|
+
throw new TheorumError(`${tag}: compactAt must be in (0, 1)`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
128
345
|
}
|
|
129
346
|
}
|
|
130
347
|
function assertCompactionRetain(tag, spec) {
|
|
131
348
|
if (spec.previousExchanges < 0) {
|
|
132
|
-
throw new TheorumError(`${tag}: previousExchanges must be >= 0`);
|
|
349
|
+
throw new TheorumError(`${tag}: previousExchanges must be >= 0`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
133
350
|
}
|
|
134
351
|
if (spec.previousExchanges > 0 && spec.previousExchanges < 1) {
|
|
135
352
|
if (spec.previousExchanges >= spec.compactAt) {
|
|
@@ -137,7 +354,7 @@ function assertCompactionRetain(tag, spec) {
|
|
|
137
354
|
}
|
|
138
355
|
}
|
|
139
356
|
if (spec.previousExchanges >= 1 && !Number.isInteger(spec.previousExchanges)) {
|
|
140
|
-
throw new TheorumError(`${tag}: previousExchanges >= 1 must be an integer`);
|
|
357
|
+
throw new TheorumError(`${tag}: previousExchanges >= 1 must be an integer`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
141
358
|
}
|
|
142
359
|
}
|
|
143
360
|
function profileToolsAllow(profile) {
|
|
@@ -150,14 +367,34 @@ function assertCustomToolsOnly(profile) {
|
|
|
150
367
|
for (const id of profileToolsAllow(profile)) {
|
|
151
368
|
const tool = getTool(id);
|
|
152
369
|
if (tool?.type === 'builtin') {
|
|
153
|
-
throw new TheorumError(
|
|
370
|
+
throw new TheorumError(profile.type === 'host'
|
|
371
|
+
? `Profile ${profile.id} lists builtin '${id}' in tools.allow — type 'host' never runs a model` // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
372
|
+
: `Profile ${profile.id} lists builtin '${id}' in tools.allow — declare it on models.*.builtInTools instead`);
|
|
154
373
|
}
|
|
155
374
|
}
|
|
156
375
|
}
|
|
376
|
+
function assertLiveTools(profileId, tools) {
|
|
377
|
+
const extra = tools;
|
|
378
|
+
if (extra.t1Policy !== undefined) {
|
|
379
|
+
throw new TheorumError(`Profile ${profileId}: tools.t1Policy is not supported on type 'live' — wire T0 tools in tools.allow for session setup`);
|
|
380
|
+
}
|
|
381
|
+
if (extra.t2Loader !== undefined) {
|
|
382
|
+
throw new TheorumError(`Profile ${profileId}: tools.t2Loader is not supported on type 'live' — Gemini Live function declarations are fixed at session setup`);
|
|
383
|
+
}
|
|
384
|
+
return { allow: tools.allow };
|
|
385
|
+
}
|
|
157
386
|
function assertProfileToolLoader(profile) {
|
|
158
387
|
if (profile.type === 'speech') {
|
|
159
388
|
return;
|
|
160
389
|
}
|
|
390
|
+
if (profile.type === 'live') {
|
|
391
|
+
assertLiveTools(profile.id, profile.tools);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (profile.type === 'host') {
|
|
395
|
+
assertHostTools(profile.id, profile.tools);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
161
398
|
const loaderId = profile.tools.t2Loader;
|
|
162
399
|
if (!loaderId) {
|
|
163
400
|
return;
|
|
@@ -170,13 +407,19 @@ function assertProfileToolLoader(profile) {
|
|
|
170
407
|
throw new TheorumError(`Profile ${profile.id} tools.t2Loader '${loaderId}' must be a registered type: 'function' tool`);
|
|
171
408
|
}
|
|
172
409
|
}
|
|
410
|
+
/** Every builtin id declared across a profile's model bindings. */
|
|
411
|
+
function* modelBuiltinIds(profile) {
|
|
412
|
+
for (const [modelId, binding] of Object.entries(profile.models)) {
|
|
413
|
+
for (const id of binding.builtInTools ?? []) {
|
|
414
|
+
yield { modelId, id };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
173
418
|
function assertModelBuiltInTools(profile) {
|
|
174
|
-
for (const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
throw new TheorumError(`Profile ${profile.id} model '${modelId}' lists '${id}' in builtInTools — not a registered builtin`);
|
|
179
|
-
}
|
|
419
|
+
for (const { modelId, id } of modelBuiltinIds(profile)) {
|
|
420
|
+
const tool = getTool(id);
|
|
421
|
+
if (tool?.type !== 'builtin') {
|
|
422
|
+
throw new TheorumError(`Profile ${profile.id} model '${modelId}' lists '${id}' in builtInTools — not a registered builtin`);
|
|
180
423
|
}
|
|
181
424
|
}
|
|
182
425
|
}
|
|
@@ -184,14 +427,14 @@ function assertCompactionOnlyOnText(profile) {
|
|
|
184
427
|
if (profile.type === 'text') {
|
|
185
428
|
return;
|
|
186
429
|
}
|
|
187
|
-
for (const [modelId,
|
|
188
|
-
if (
|
|
430
|
+
for (const [modelId, binding] of Object.entries(profile.models)) {
|
|
431
|
+
if (binding.compaction) {
|
|
189
432
|
throw new TheorumError(`Profile ${profile.id} model '${modelId}': compaction is only valid on type 'text'`);
|
|
190
433
|
}
|
|
191
434
|
}
|
|
192
435
|
}
|
|
193
436
|
function assertMediaLimits(profile) {
|
|
194
|
-
if (profile.type === 'speech') {
|
|
437
|
+
if (profile.type === 'speech' || profile.type === 'live') {
|
|
195
438
|
return;
|
|
196
439
|
}
|
|
197
440
|
const inputs = profile.inputs;
|
|
@@ -201,22 +444,26 @@ function assertMediaLimits(profile) {
|
|
|
201
444
|
const { attachments, voice, maxFiles, maxBytes, maxTurnBytes } = inputs;
|
|
202
445
|
if (attachments || voice) {
|
|
203
446
|
if (!(maxFiles && maxBytes && maxTurnBytes)) {
|
|
204
|
-
throw new TheorumError(`Profile ${profile.id} must set maxFiles, maxBytes, and maxTurnBytes`);
|
|
447
|
+
throw new TheorumError(`Profile ${profile.id} must set maxFiles, maxBytes, and maxTurnBytes`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
205
448
|
}
|
|
206
449
|
}
|
|
207
450
|
}
|
|
208
451
|
/** Register one host-owned profile in the process-local registry. */
|
|
209
452
|
function registerProfile(profileInput) {
|
|
210
453
|
const profile = defineProfile(profileInput);
|
|
211
|
-
assertModelSpecs(profile.id, profile.model.allow, profile.model.config);
|
|
212
454
|
assertCustomToolsOnly(profile);
|
|
213
455
|
assertProfileToolLoader(profile);
|
|
456
|
+
if (profile.type === 'host') {
|
|
457
|
+
// No models, ingress, media limits, or compaction to validate — the tool ceiling is the whole contract.
|
|
458
|
+
profiles.set(profile.id, profile);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
214
461
|
assertModelBuiltInTools(profile);
|
|
215
462
|
assertCompactionOnlyOnText(profile);
|
|
216
463
|
assertMediaLimits(profile);
|
|
217
|
-
for (const [modelId,
|
|
218
|
-
if (
|
|
219
|
-
assertCompactionSpec(profile.id, modelId,
|
|
464
|
+
for (const [modelId, binding] of Object.entries(profile.models)) {
|
|
465
|
+
if (binding.compaction) {
|
|
466
|
+
assertCompactionSpec(profile.id, modelId, binding.compaction);
|
|
220
467
|
}
|
|
221
468
|
}
|
|
222
469
|
profiles.set(profile.id, profile);
|
|
@@ -13,6 +13,8 @@ function providerCompleteRequest(generation, system) {
|
|
|
13
13
|
temperature: generation.temperature,
|
|
14
14
|
builtins: generation.builtins,
|
|
15
15
|
googleMapsLocation: isInteractions ? generation.googleMapsLocation : undefined,
|
|
16
|
+
cache: generation.cache,
|
|
17
|
+
sessionId: generation.sessionId,
|
|
16
18
|
system,
|
|
17
19
|
input: generation.input,
|
|
18
20
|
history: generation.history,
|
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
import type { ModelId, Profile, ProjectedProfile, ResolvedGeneration, TurnRequest } from '../types.js';
|
|
7
|
-
|
|
6
|
+
import type { ModelId, ModelProfile, Profile, ProjectedProfile, ResolvedGeneration, TurnRequest } from '../types.js';
|
|
7
|
+
/** Narrow to a model-binding profile; `host` never runs a model. */
|
|
8
|
+
declare function requireModelProfile(profile: Profile, door: string): ModelProfile;
|
|
9
|
+
declare function pickModel(profile: ModelProfile, requested?: string): ModelId;
|
|
8
10
|
/** Resolve a host `TurnRequest` into provider-ready generation state. */
|
|
9
11
|
declare function resolveTurn(req: TurnRequest): {
|
|
10
|
-
profile:
|
|
12
|
+
profile: ModelProfile;
|
|
11
13
|
generation: ResolvedGeneration;
|
|
12
14
|
};
|
|
13
15
|
/** Project a profile object into a safe host/UI inspection object. */
|
|
14
|
-
declare function projectProfileObject(
|
|
16
|
+
declare function projectProfileObject(input: Profile): ProjectedProfile;
|
|
15
17
|
/** Project a registered profile into a safe host/UI inspection object. */
|
|
16
18
|
declare function projectProfile(id: Profile['id']): ProjectedProfile;
|
|
17
|
-
|
|
18
|
-
export { pickModel, pickSystemRole, projectProfile, projectProfileObject, resolveTurn };
|
|
19
|
+
export { pickModel, projectProfile, projectProfileObject, requireModelProfile, resolveTurn };
|