@theoremjs/agents 0.3.4-next.484.g20d8b9d → 0.3.4-next.522.g9e684cd
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 +61 -51
- package/esm/mod.d.ts +2 -2
- package/esm/mod.js +1 -1
- package/esm/src/guardrails/credential-scan.d.ts +1 -1
- package/esm/src/guardrails/credential-scan.js +41 -18
- package/esm/src/guardrails/destinations.js +6 -2
- package/esm/src/guardrails/detect-at.d.ts +5 -5
- package/esm/src/guardrails/detect-at.js +51 -41
- package/esm/src/guardrails/detect-reply.d.ts +1 -1
- package/esm/src/guardrails/detect-reply.js +4 -2
- package/esm/src/guardrails/egress-automata.js +203 -202
- package/esm/src/guardrails/egress-compiler.js +1 -4
- package/esm/src/guardrails/egress-stream.js +10 -3
- package/esm/src/guardrails/egress-urls.js +4 -2
- package/esm/src/guardrails/error.d.ts +1 -1
- package/esm/src/guardrails/error.js +19 -2
- package/esm/src/guardrails/host-patterns.d.ts +1 -4
- package/esm/src/guardrails/host-patterns.js +1 -10
- package/esm/src/guardrails/injection-patterns.d.ts +3 -1
- package/esm/src/guardrails/injection-patterns.js +87 -4
- package/esm/src/guardrails/labels.d.ts +2 -2
- package/esm/src/guardrails/labels.js +7 -5
- package/esm/src/guardrails/mod.d.ts +2 -2
- package/esm/src/guardrails/mod.js +1 -1
- package/esm/src/guardrails/normalize.js +4 -2
- package/esm/src/guardrails/prompt-echo.js +4 -2
- package/esm/src/guardrails/sanitize.d.ts +1 -1
- package/esm/src/guardrails/sanitize.js +4 -3
- package/esm/src/guardrails/sensitive.d.ts +2 -2
- package/esm/src/guardrails/sensitive.js +1 -1
- package/esm/src/host/client-turn.js +8 -1
- package/esm/src/interface/profile-interface.js +13 -0
- package/esm/src/kernel/answer-tool.d.ts +14 -0
- package/esm/src/kernel/answer-tool.js +26 -0
- package/esm/src/kernel/engine/decision-references.d.ts +14 -0
- package/esm/src/kernel/engine/decision-references.js +55 -0
- package/esm/src/kernel/engine/decision.d.ts +3 -3
- package/esm/src/kernel/engine/decision.js +38 -22
- package/esm/src/kernel/engine/embedding.js +0 -2
- package/esm/src/kernel/engine/runner/gates.js +40 -5
- package/esm/src/kernel/engine/runner/mod.js +13 -5
- package/esm/src/kernel/engine/runner/state.d.ts +9 -1
- package/esm/src/kernel/engine/runner/state.js +10 -1
- package/esm/src/kernel/engine/runner/steps.js +50 -13
- package/esm/src/kernel/engine/runner/stream.d.ts +14 -8
- package/esm/src/kernel/engine/runner/stream.js +25 -13
- package/esm/src/kernel/engine/turn-trace.js +2 -2
- package/esm/src/kernel/mod.d.ts +1 -1
- package/esm/src/kernel/mod.js +1 -1
- package/esm/src/kernel/profile-presence.js +12 -0
- package/esm/src/kernel/profile-scope.js +14 -14
- package/esm/src/kernel/protocols/binding.d.ts +3 -1
- package/esm/src/kernel/protocols/binding.js +6 -0
- package/esm/src/kernel/protocols/limits.d.ts +10 -11
- package/esm/src/kernel/protocols/limits.js +9 -23
- package/esm/src/kernel/protocols/runtime.d.ts +3 -2
- package/esm/src/kernel/protocols/runtime.js +3 -4
- package/esm/src/kernel/provider-budget.d.ts +2 -2
- package/esm/src/kernel/provider-budget.js +22 -12
- package/esm/src/kernel/provider-runtime.js +8 -5
- package/esm/src/kernel/registry/profiles.js +3 -3
- package/esm/src/kernel/registry/resolve.js +4 -0
- package/esm/src/kernel/registry/schemas.js +1 -1
- package/esm/src/kernel/schema.js +28 -2
- package/esm/src/kernel/tools/execute.js +3 -1
- package/esm/src/kernel/tools/image-fetch.d.ts +2 -4
- package/esm/src/kernel/tools/image-fetch.js +19 -20
- package/esm/src/kernel/tools/mcp-sessions.d.ts +7 -2
- package/esm/src/kernel/tools/mcp-sessions.js +21 -12
- package/esm/src/kernel/tools/registry.js +21 -0
- package/esm/src/kernel/tools/remote.d.ts +0 -2
- package/esm/src/kernel/tools/remote.js +28 -23
- package/esm/src/kernel/tools/types.d.ts +30 -4
- package/esm/src/kernel/turn-events.d.ts +2 -0
- package/esm/src/kernel/turn-events.js +1 -0
- package/esm/src/kernel/types.d.ts +4 -2
- package/esm/src/observability/spans.js +7 -8
- package/esm/src/observability/trace-catalog.js +1 -0
- package/esm/src/providers/embedding-limits.js +4 -4
- package/esm/src/providers/gemini/{embedding-protocol.d.ts → embeddings-protocol.d.ts} +3 -3
- package/esm/src/providers/gemini/{embedding-protocol.js → embeddings-protocol.js} +3 -3
- package/esm/src/providers/gemini/live-protocol.js +4 -1
- package/esm/src/providers/gemini/provider.js +2 -2
- package/esm/src/providers/local/embeddings-protocol.d.ts +7 -0
- package/esm/src/providers/local/{embedding-protocol.js → embeddings-protocol.js} +2 -2
- package/esm/src/providers/local/provider.js +2 -2
- package/esm/src/providers/mod.d.ts +3 -3
- package/esm/src/providers/mod.js +3 -3
- package/esm/src/providers/openrouter/decisions-protocol.js +11 -8
- package/esm/src/providers/openrouter/embeddings-protocol.d.ts +6 -0
- package/esm/src/providers/openrouter/{embedding-protocol.js → embeddings-protocol.js} +2 -2
- package/esm/src/providers/openrouter/provider.js +2 -2
- package/package.json +1 -1
- package/esm/src/providers/local/embedding-protocol.d.ts +0 -7
- package/esm/src/providers/openrouter/embedding-protocol.d.ts +0 -6
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { fetchGuarded } from '../../guardrails/network.js';
|
|
2
2
|
import { bytesToBase64 } from '../util/base64.js';
|
|
3
3
|
import { toolNetworkPolicy } from './events.js';
|
|
4
|
-
/** The most pictures one call hands the model. */
|
|
5
|
-
export const MAX_TOOL_IMAGES = 4;
|
|
6
|
-
/** The largest picture fetched; a larger one is left out, not cut. */
|
|
7
|
-
const MAX_TOOL_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
8
|
-
/** The most one call hands the model in all, so pictures cannot crowd a request past a provider's limit. */
|
|
9
|
-
const MAX_TOOL_IMAGES_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
10
|
-
/** How long each picture may take, so a slow host cannot hold the call. */
|
|
11
|
-
const IMAGE_FETCH_TIMEOUT_MS = 15_000;
|
|
12
4
|
const IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
13
|
-
/** The body, read up to `maxBytes
|
|
14
|
-
async function readCappedBytes(response, maxBytes) {
|
|
5
|
+
/** The body, read up to `maxBytes` when set; `undefined` when it runs past. */
|
|
6
|
+
async function readCappedBytes(response, maxBytes = Number.POSITIVE_INFINITY) {
|
|
15
7
|
const declared = Number(response.headers.get('content-length'));
|
|
16
8
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
17
9
|
await response.body?.cancel().catch(() => { });
|
|
@@ -41,9 +33,15 @@ async function readCappedBytes(response, maxBytes) {
|
|
|
41
33
|
}
|
|
42
34
|
return bytes;
|
|
43
35
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
/** The call's signal, joined by the picture's own timeout when the tool sets one. */
|
|
37
|
+
function fetchSignal(ctx, timeoutMs) {
|
|
38
|
+
if (timeoutMs === undefined)
|
|
39
|
+
return ctx.signal;
|
|
40
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
41
|
+
return ctx.signal ? AbortSignal.any([ctx.signal, timeout]) : timeout;
|
|
42
|
+
}
|
|
43
|
+
async function fetchOne(url, ctx, limits) {
|
|
44
|
+
const signal = fetchSignal(ctx, limits.timeoutMs);
|
|
47
45
|
const response = await fetchGuarded(url, { method: 'GET', headers: { Accept: 'image/*' }, signal }, {
|
|
48
46
|
policy: toolNetworkPolicy(ctx),
|
|
49
47
|
followRedirects: true,
|
|
@@ -56,9 +54,9 @@ async function fetchOne(url, ctx) {
|
|
|
56
54
|
await response.body?.cancel().catch(() => { });
|
|
57
55
|
throw new Error(`not a png, jpeg, gif or webp image (${mimeType || 'no type'})`); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
58
56
|
}
|
|
59
|
-
const bytes = await readCappedBytes(response,
|
|
57
|
+
const bytes = await readCappedBytes(response, limits.bytes);
|
|
60
58
|
if (!bytes)
|
|
61
|
-
throw new Error(`larger than ${
|
|
59
|
+
throw new Error(`larger than ${limits.bytes} bytes`); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
62
60
|
return { type: 'image', mimeType, data: bytesToBase64(bytes) };
|
|
63
61
|
}
|
|
64
62
|
/** `url` when it is an absolute https address; otherwise the reason it is not. */
|
|
@@ -80,7 +78,7 @@ function httpsAddress(url) {
|
|
|
80
78
|
* that is not an https address, blocked, slow, too big or not an image is left out and reported in
|
|
81
79
|
* one `images_unfetched` warning: the call itself stays whole. `urls` are the ones fetched.
|
|
82
80
|
*/
|
|
83
|
-
export async function fetchToolImages(images, output, ctx) {
|
|
81
|
+
export async function fetchToolImages(images, output, ctx, limits = {}) {
|
|
84
82
|
const reasons = [];
|
|
85
83
|
const named = [];
|
|
86
84
|
try {
|
|
@@ -100,8 +98,9 @@ export async function fetchToolImages(images, output, ctx) {
|
|
|
100
98
|
warning: unfetched([`the images hook threw: ${String(err)}`]), // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
101
99
|
};
|
|
102
100
|
}
|
|
103
|
-
const wanted = named.slice(0,
|
|
104
|
-
const settled = await Promise.allSettled(wanted.map((url) => fetchOne(url, ctx)));
|
|
101
|
+
const wanted = named.slice(0, limits.count);
|
|
102
|
+
const settled = await Promise.allSettled(wanted.map((url) => fetchOne(url, ctx, limits)));
|
|
103
|
+
const totalBytes = limits.totalBytes ?? Number.POSITIVE_INFINITY;
|
|
105
104
|
const parts = [];
|
|
106
105
|
const urls = [];
|
|
107
106
|
let total = 0;
|
|
@@ -113,8 +112,8 @@ export async function fetchToolImages(images, output, ctx) {
|
|
|
113
112
|
return;
|
|
114
113
|
}
|
|
115
114
|
const size = 'data' in result.value ? result.value.data.length : 0;
|
|
116
|
-
if (total + size >
|
|
117
|
-
reasons.push(`${new URL(url).hostname}: past the ${
|
|
115
|
+
if (total + size > totalBytes) {
|
|
116
|
+
reasons.push(`${new URL(url).hostname}: past the ${totalBytes} byte total`); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
118
117
|
return;
|
|
119
118
|
}
|
|
120
119
|
total += size;
|
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
* Sessions for MCP servers that refuse a call without one. A session is a cache, not state: it
|
|
3
3
|
* lives in memory on one tool registry, is never persisted, and losing it costs one reopen.
|
|
4
4
|
*/
|
|
5
|
+
import type { McpSessionLimits } from './types.js';
|
|
5
6
|
export type McpSession = {
|
|
6
7
|
id: string;
|
|
7
8
|
protocolVersion: string;
|
|
8
9
|
};
|
|
10
|
+
/** The tool a session is used for, and that tool's own `session` limits. */
|
|
11
|
+
export interface McpSessionUse extends McpSessionLimits {
|
|
12
|
+
tool: string;
|
|
13
|
+
}
|
|
9
14
|
export interface McpSessionCache {
|
|
10
|
-
get(key: string): McpSession | undefined;
|
|
15
|
+
get(key: string, use: McpSessionUse): McpSession | undefined;
|
|
11
16
|
/** Parallel callers for one key share a single `open`. */
|
|
12
|
-
open(key: string, open: () => Promise<McpSession>): Promise<McpSession>;
|
|
17
|
+
open(key: string, use: McpSessionUse, open: () => Promise<McpSession>): Promise<McpSession>;
|
|
13
18
|
/** Only drops the session if it's still `id`, so a late expiry can't evict a newer one. */
|
|
14
19
|
drop(key: string, id: string): void;
|
|
15
20
|
clear(): void;
|
|
@@ -2,38 +2,47 @@
|
|
|
2
2
|
* Sessions for MCP servers that refuse a call without one. A session is a cache, not state: it
|
|
3
3
|
* lives in memory on one tool registry, is never persisted, and losing it costs one reopen.
|
|
4
4
|
*/
|
|
5
|
-
const MAX_SESSIONS = 256;
|
|
6
|
-
const IDLE_MS = 30 * 60 * 1000;
|
|
7
5
|
// why: The spec allows visible ASCII only; the cap keeps a server from bloating every later request.
|
|
8
6
|
const SESSION_ID = /^[\x21-\x7E]{1,256}$/;
|
|
9
7
|
export function createMcpSessionCache(now = Date.now) {
|
|
10
8
|
const sessions = new Map();
|
|
11
9
|
const pending = new Map();
|
|
10
|
+
/** Drops the tool's least recently used sessions past its `max`. */
|
|
11
|
+
const trim = ({ tool, max }) => {
|
|
12
|
+
if (max === undefined)
|
|
13
|
+
return;
|
|
14
|
+
let held = [...sessions.values()].filter((entry) => entry.tool === tool).length;
|
|
15
|
+
for (const [key, entry] of sessions) {
|
|
16
|
+
if (held <= max)
|
|
17
|
+
break;
|
|
18
|
+
if (entry.tool !== tool)
|
|
19
|
+
continue;
|
|
20
|
+
sessions.delete(key);
|
|
21
|
+
held--;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
12
24
|
return {
|
|
13
|
-
get(key) {
|
|
25
|
+
get(key, use) {
|
|
14
26
|
const entry = sessions.get(key);
|
|
15
27
|
if (!entry)
|
|
16
28
|
return undefined;
|
|
17
29
|
sessions.delete(key);
|
|
18
|
-
if (now() - entry.usedAt >
|
|
30
|
+
if (use.idleMs !== undefined && now() - entry.usedAt > use.idleMs)
|
|
19
31
|
return undefined;
|
|
20
32
|
// why: Re-inserting keeps the map in least-recently-used order for eviction.
|
|
21
|
-
sessions.set(key, { ...entry, usedAt: now() });
|
|
33
|
+
sessions.set(key, { ...entry, usedAt: now(), tool: use.tool });
|
|
34
|
+
trim(use);
|
|
22
35
|
return { id: entry.id, protocolVersion: entry.protocolVersion };
|
|
23
36
|
},
|
|
24
|
-
open(key, open) {
|
|
37
|
+
open(key, use, open) {
|
|
25
38
|
const inFlight = pending.get(key);
|
|
26
39
|
if (inFlight)
|
|
27
40
|
return inFlight;
|
|
28
41
|
const opening = open()
|
|
29
42
|
.then((session) => {
|
|
30
43
|
sessions.delete(key);
|
|
31
|
-
sessions.set(key, { ...session, usedAt: now() });
|
|
32
|
-
|
|
33
|
-
if (sessions.size <= MAX_SESSIONS)
|
|
34
|
-
break;
|
|
35
|
-
sessions.delete(oldest);
|
|
36
|
-
}
|
|
44
|
+
sessions.set(key, { ...session, usedAt: now(), tool: use.tool });
|
|
45
|
+
trim(use);
|
|
37
46
|
return session;
|
|
38
47
|
})
|
|
39
48
|
.finally(() => pending.delete(key));
|
|
@@ -63,17 +63,38 @@ function assertTimeout(name, timeoutMs) {
|
|
|
63
63
|
return;
|
|
64
64
|
throw new TheoremError('config', `Tool "${name}" timeoutMs needs a finite number of ms above 0`);
|
|
65
65
|
}
|
|
66
|
+
/** A count or size limit is a whole number above 0. */
|
|
67
|
+
function assertWhole(name, field, value) {
|
|
68
|
+
if (value === undefined || (Number.isInteger(value) && value > 0))
|
|
69
|
+
return;
|
|
70
|
+
throw new TheoremError('config', `Tool "${name}" ${field} needs a whole number above 0`);
|
|
71
|
+
}
|
|
72
|
+
/** What a remote tool may set on how much it reads: its body, and the pictures it names. */
|
|
73
|
+
function assertReadLimits(def) {
|
|
74
|
+
assertWhole(def.name, 'maxResponseBytes', def.maxResponseBytes);
|
|
75
|
+
const images = def.imageLimits;
|
|
76
|
+
if (!images)
|
|
77
|
+
return;
|
|
78
|
+
assertWhole(def.name, 'imageLimits.count', images.count);
|
|
79
|
+
assertWhole(def.name, 'imageLimits.bytes', images.bytes);
|
|
80
|
+
assertWhole(def.name, 'imageLimits.totalBytes', images.totalBytes);
|
|
81
|
+
assertTimeout(def.name, images.timeoutMs);
|
|
82
|
+
}
|
|
66
83
|
function normalizeHttp(def) {
|
|
67
84
|
assertFixedEndpointOrigin(def.endpoint);
|
|
68
85
|
assertAuthService(def.name, def.auth);
|
|
69
86
|
assertQueryParam(def.name, def.auth, def.mapping?.queryParams);
|
|
70
87
|
assertLimit(def.name, def.limit);
|
|
71
88
|
assertTimeout(def.name, def.timeoutMs);
|
|
89
|
+
assertReadLimits(def);
|
|
72
90
|
return { ...def, type: 'http', ...schemasFromZod(def) };
|
|
73
91
|
}
|
|
74
92
|
function normalizeMcp(def) {
|
|
75
93
|
assertAuthService(def.name, def.auth);
|
|
76
94
|
assertQueryParam(def.name, def.auth);
|
|
95
|
+
assertReadLimits(def);
|
|
96
|
+
assertWhole(def.name, 'session.idleMs', def.session?.idleMs);
|
|
97
|
+
assertWhole(def.name, 'session.max', def.session?.max);
|
|
77
98
|
return { ...def, type: 'mcp', ...schemasFromZod(def) };
|
|
78
99
|
}
|
|
79
100
|
function normalizeFunction(def) {
|
|
@@ -96,8 +96,6 @@ export declare function refusedCredentialOutcome(tool: {
|
|
|
96
96
|
name: string;
|
|
97
97
|
auth?: ToolAuthConfig;
|
|
98
98
|
}, refusal: CredentialRefusal | undefined, carried: boolean, ctx: ToolContext, base: ToolCallBase): Generator<TurnEvent, ToolBodyOutcome | undefined>;
|
|
99
|
-
/** The most of a tool's response body the kernel reads; past it the call fails instead of exhausting the host's memory. */
|
|
100
|
-
export declare const MAX_TOOL_RESPONSE_BYTES: number;
|
|
101
99
|
/** Every string in `value` with the credential replaced. */
|
|
102
100
|
export declare function omitSecret(value: unknown, secret: string): unknown;
|
|
103
101
|
/** A failure's message and details with the credential replaced. */
|
|
@@ -391,12 +391,10 @@ function* thrownOutcome(err, checks) {
|
|
|
391
391
|
return failureOutcome({ code: 'network_error', kind: 'network', message: messageOf(err) }, false);
|
|
392
392
|
}
|
|
393
393
|
const OMIT_CREDENTIAL = '[omitted - credential]';
|
|
394
|
-
/** The most of a tool's response body the kernel reads; past it the call fails instead of exhausting the host's memory. */
|
|
395
|
-
export const MAX_TOOL_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
396
394
|
/** The most of an error or unparseable body quoted back in a failure message. */
|
|
397
395
|
const MAX_QUOTED_BODY_CHARS = 2000;
|
|
398
|
-
/** A body's text as it arrives, up to `maxBytes
|
|
399
|
-
async function* cappedPieces(response, maxBytes) {
|
|
396
|
+
/** A body's text as it arrives, up to `maxBytes` when set; returns `false` when it runs past, with the rest left unread. */
|
|
397
|
+
async function* cappedPieces(response, maxBytes = Number.POSITIVE_INFINITY) {
|
|
400
398
|
const declared = Number(response.headers.get('content-length'));
|
|
401
399
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
402
400
|
await response.body?.cancel().catch(() => { });
|
|
@@ -427,8 +425,8 @@ async function* cappedPieces(response, maxBytes) {
|
|
|
427
425
|
yield rest;
|
|
428
426
|
return true;
|
|
429
427
|
}
|
|
430
|
-
/** A body, read up to `maxBytes
|
|
431
|
-
async function readCappedText(response, maxBytes
|
|
428
|
+
/** A body, read up to `maxBytes` when set; `undefined` when it runs past, with the rest left unread. */
|
|
429
|
+
async function readCappedText(response, maxBytes) {
|
|
432
430
|
const pieces = cappedPieces(response, maxBytes);
|
|
433
431
|
let text = '';
|
|
434
432
|
for (;;) {
|
|
@@ -443,8 +441,8 @@ async function readCappedText(response, maxBytes = MAX_TOOL_RESPONSE_BYTES) {
|
|
|
443
441
|
* shows: the last `secret.length - 1` characters wait for the next piece, so one split across
|
|
444
442
|
* pieces is whole when it is replaced. Drafts that ran before the body ran past its cap stay sent.
|
|
445
443
|
*/
|
|
446
|
-
async function* readCappedTextShowing(response, shown, secret) {
|
|
447
|
-
const pieces = cappedPieces(response,
|
|
444
|
+
async function* readCappedTextShowing(response, shown, secret, maxBytes) {
|
|
445
|
+
const pieces = cappedPieces(response, maxBytes);
|
|
448
446
|
const hold = secret ? secret.length - 1 : 0;
|
|
449
447
|
let text = '';
|
|
450
448
|
let pending = '';
|
|
@@ -471,12 +469,12 @@ async function* readCappedTextShowing(response, shown, secret) {
|
|
|
471
469
|
function quotedBody(text) {
|
|
472
470
|
return text.length > MAX_QUOTED_BODY_CHARS ? `${text.slice(0, MAX_QUOTED_BODY_CHARS)}…` : text;
|
|
473
471
|
}
|
|
474
|
-
/** The failure for a body past
|
|
475
|
-
function tooLargeFailure(host) {
|
|
472
|
+
/** The failure for a body past the tool's `maxResponseBytes`. */
|
|
473
|
+
function tooLargeFailure(host, maxBytes) {
|
|
476
474
|
return {
|
|
477
475
|
code: 'response_too_large',
|
|
478
476
|
kind: 'bad_response',
|
|
479
|
-
message: `Response from ${host} is larger than ${
|
|
477
|
+
message: `Response from ${host} is larger than ${maxBytes} bytes`, // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
480
478
|
};
|
|
481
479
|
}
|
|
482
480
|
/** The text with the credential replaced. */
|
|
@@ -568,10 +566,10 @@ function isDraftable(response) {
|
|
|
568
566
|
return type === '' || type.startsWith('text/') || /[/+]json\b/.test(type);
|
|
569
567
|
}
|
|
570
568
|
/** An HTTP tool's body; a successful text body shows as drafts while it arrives, any other reads whole. */
|
|
571
|
-
async function* readHttpBody(response, shown, secret) {
|
|
569
|
+
async function* readHttpBody(response, shown, secret, maxBytes) {
|
|
572
570
|
if (!shown || !isDraftable(response))
|
|
573
|
-
return await readCappedText(response);
|
|
574
|
-
return yield* readCappedTextShowing(response, shown, secret);
|
|
571
|
+
return await readCappedText(response, maxBytes);
|
|
572
|
+
return yield* readCappedTextShowing(response, shown, secret, maxBytes);
|
|
575
573
|
}
|
|
576
574
|
async function* sendHttpRequest(tool, targetUrl, body, { authHeaders, secret }, ctx, base, span, shown) {
|
|
577
575
|
const checks = requestChecks(span);
|
|
@@ -597,9 +595,10 @@ async function* sendHttpRequest(tool, targetUrl, body, { authHeaders, secret },
|
|
|
597
595
|
resolveHost: ctx.resolveHost,
|
|
598
596
|
onCheck: checks.onCheck,
|
|
599
597
|
}));
|
|
600
|
-
const text = yield* readHttpBody(response, shown, secret);
|
|
601
|
-
if (text === undefined)
|
|
602
|
-
return failureOutcome(tooLargeFailure(targetUrl.hostname), false);
|
|
598
|
+
const text = yield* readHttpBody(response, shown, secret, tool.maxResponseBytes);
|
|
599
|
+
if (text === undefined) {
|
|
600
|
+
return failureOutcome(tooLargeFailure(targetUrl.hostname, tool.maxResponseBytes), false);
|
|
601
|
+
}
|
|
603
602
|
if (!response.ok) {
|
|
604
603
|
const refused = yield* refusedCredentialOutcome(tool, credentialRefusal(response), secret !== undefined, ctx, base);
|
|
605
604
|
if (refused)
|
|
@@ -747,9 +746,10 @@ async function fetchMcpProtocolAttempt(transport, rpcId, mcpToolName, input, pro
|
|
|
747
746
|
resolveHost: transport.resolveHost,
|
|
748
747
|
onCheck: transport.onCheck,
|
|
749
748
|
});
|
|
750
|
-
const text = await readCappedText(response);
|
|
749
|
+
const text = await readCappedText(response, transport.maxResponseBytes);
|
|
751
750
|
if (text === undefined) {
|
|
752
|
-
|
|
751
|
+
const host = new URL(transport.url).hostname;
|
|
752
|
+
return { kind: 'failure', failure: tooLargeFailure(host, transport.maxResponseBytes) };
|
|
753
753
|
}
|
|
754
754
|
if (!response.ok) {
|
|
755
755
|
const sessionSent = SESSION_HEADER in transport.originBoundHeaders;
|
|
@@ -854,9 +854,10 @@ async function openMcpSession(transport) {
|
|
|
854
854
|
clientInfo: { name: 'theorem', version: '1' },
|
|
855
855
|
},
|
|
856
856
|
}, { 'Mcp-Method': 'initialize' });
|
|
857
|
-
const text = await readCappedText(response);
|
|
857
|
+
const text = await readCappedText(response, transport.maxResponseBytes);
|
|
858
858
|
if (text === undefined) {
|
|
859
|
-
|
|
859
|
+
const host = new URL(transport.url).hostname;
|
|
860
|
+
throw new McpSessionFailure(tooLargeFailure(host, transport.maxResponseBytes));
|
|
860
861
|
}
|
|
861
862
|
if (!response.ok) {
|
|
862
863
|
throw sessionFailure(`mcp_session_http_${response.status}`, `MCP server refused to open a session: HTTP ${response.status}: ${quotedBody(text)}`);
|
|
@@ -886,7 +887,7 @@ async function negotiateInSession(transport, rpcId, mcpToolName, input, sessions
|
|
|
886
887
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
887
888
|
if (!session) {
|
|
888
889
|
try {
|
|
889
|
-
session = await sessions.cache.open(sessions.key, () => openMcpSession(transport));
|
|
890
|
+
session = await sessions.cache.open(sessions.key, sessions.use, () => openMcpSession(transport));
|
|
890
891
|
}
|
|
891
892
|
catch (err) {
|
|
892
893
|
if (err instanceof McpSessionFailure)
|
|
@@ -1090,13 +1091,16 @@ async function* sendMcpRequest(tool, url, input, { authHeaders, secret }, ctx, b
|
|
|
1090
1091
|
resolveHost: ctx.resolveHost,
|
|
1091
1092
|
signal: ctx.signal,
|
|
1092
1093
|
onCheck: checks.onCheck,
|
|
1094
|
+
...(tool.maxResponseBytes === undefined ? {} : { maxResponseBytes: tool.maxResponseBytes }),
|
|
1093
1095
|
};
|
|
1094
1096
|
const key = await mcpSessionKey(url.href, transport.originBoundHeaders);
|
|
1095
|
-
const
|
|
1097
|
+
const use = { tool: tool.name, ...tool.session };
|
|
1098
|
+
const cached = sessions.get(key, use);
|
|
1096
1099
|
const first = cached
|
|
1097
1100
|
? await negotiateInSession(transport, base.callId, tool.mcpToolName, input, {
|
|
1098
1101
|
cache: sessions,
|
|
1099
1102
|
key,
|
|
1103
|
+
use,
|
|
1100
1104
|
cached,
|
|
1101
1105
|
})
|
|
1102
1106
|
: await negotiateMcpRpc(transport, base.callId, tool.mcpToolName, input);
|
|
@@ -1104,6 +1108,7 @@ async function* sendMcpRequest(tool, url, input, { authHeaders, secret }, ctx, b
|
|
|
1104
1108
|
? await negotiateInSession(transport, base.callId, tool.mcpToolName, input, {
|
|
1105
1109
|
cache: sessions,
|
|
1106
1110
|
key,
|
|
1111
|
+
use,
|
|
1107
1112
|
})
|
|
1108
1113
|
: first;
|
|
1109
1114
|
const refused = yield* refusedCredentialOutcome(tool, negotiated.refusal, secret !== undefined, ctx, base);
|
|
@@ -163,12 +163,25 @@ export interface ToolOutputHooks<TOut = unknown> {
|
|
|
163
163
|
/** For a tool whose result names pictures by address; function tools return the bytes themselves as `parts`. */
|
|
164
164
|
export interface ToolImageHooks<TOut = unknown> {
|
|
165
165
|
/**
|
|
166
|
-
* The https addresses of pictures in `output` the model should see
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
166
|
+
* The https addresses of pictures in `output` the model should see. After the result passes the
|
|
167
|
+
* output guard, the kernel fetches each one through the profile's network policy, without the
|
|
168
|
+
* tool's credentials, and hands the model the image as a part; one it cannot fetch is an
|
|
169
|
+
* `images_unfetched` warning, never a failed call.
|
|
170
170
|
*/
|
|
171
171
|
images?: (output: TOut) => string[];
|
|
172
|
+
/** How many of those pictures the kernel fetches, how large, and how slowly. Left out, there is no limit. */
|
|
173
|
+
imageLimits?: ToolImageLimits;
|
|
174
|
+
}
|
|
175
|
+
/** Each limit left out is no limit. A picture past one is left out, not cut. */
|
|
176
|
+
export interface ToolImageLimits {
|
|
177
|
+
/** The most pictures one call hands the model; the first ones named are kept. */
|
|
178
|
+
count?: number;
|
|
179
|
+
/** The largest picture fetched. */
|
|
180
|
+
bytes?: number;
|
|
181
|
+
/** The most one call hands the model in all. */
|
|
182
|
+
totalBytes?: number;
|
|
183
|
+
/** How long each picture may take. */
|
|
184
|
+
timeoutMs?: number;
|
|
172
185
|
}
|
|
173
186
|
export interface FunctionToolDef<TIn = unknown, TOut = unknown> extends ToolBase, ToolHostHooks<TIn>, ToolOutputHooks<TOut> {
|
|
174
187
|
type: 'function';
|
|
@@ -226,6 +239,8 @@ export interface HttpToolDef<TIn = unknown, TOut = unknown> extends ToolBase, To
|
|
|
226
239
|
};
|
|
227
240
|
/** The service's own limit for one key; the kernel keeps this tool's requests within it. */
|
|
228
241
|
limit?: HttpToolLimit;
|
|
242
|
+
/** The most of a response body the kernel reads; past it the call fails `response_too_large`. Left out, there is no limit. */
|
|
243
|
+
maxResponseBytes?: number;
|
|
229
244
|
/**
|
|
230
245
|
* How long the service may take to answer, its body read included. Past it the call
|
|
231
246
|
* fails `timeout` and frees its place in the limit. Left out, a call waits as long as
|
|
@@ -246,6 +261,17 @@ export interface McpToolDef<TIn = unknown, TOut = unknown> extends ToolBase, Too
|
|
|
246
261
|
mcpToolName: string;
|
|
247
262
|
headers?: Record<string, string>;
|
|
248
263
|
auth?: ToolAuthConfig;
|
|
264
|
+
/** The most of a response body the kernel reads; past it the call fails `response_too_large`. Left out, there is no limit. */
|
|
265
|
+
maxResponseBytes?: number;
|
|
266
|
+
/** How long and how many of this tool's server sessions the kernel keeps. Left out, there is no limit. */
|
|
267
|
+
session?: McpSessionLimits;
|
|
268
|
+
}
|
|
269
|
+
/** The sessions an MCP tool keeps: each field left out is no limit. */
|
|
270
|
+
export interface McpSessionLimits {
|
|
271
|
+
/** How long a session may sit unused before the next call opens a new one, in milliseconds. */
|
|
272
|
+
idleMs?: number;
|
|
273
|
+
/** The most sessions the tool keeps open; past it the least recently used is dropped. */
|
|
274
|
+
max?: number;
|
|
249
275
|
}
|
|
250
276
|
/**
|
|
251
277
|
* What an agent tool call carries: the text, and whatever else the called
|
|
@@ -520,6 +520,8 @@ export interface CompactionFailure {
|
|
|
520
520
|
stop?: TurnStopKind;
|
|
521
521
|
/** The error it reported, when it reported one. */
|
|
522
522
|
error?: ErrorKind;
|
|
523
|
+
/** Why it failed, for the builder only: `forClient` strips it for a visitor. */
|
|
524
|
+
errorInternal?: string;
|
|
523
525
|
/** It completed without a summary. */
|
|
524
526
|
empty?: true;
|
|
525
527
|
/** Nothing in `toCompact` was left for it to read, so it did not run. */
|
|
@@ -333,6 +333,7 @@ true;
|
|
|
333
333
|
const compactionFailure = z.object({
|
|
334
334
|
stop: z.enum(TURN_STOP_KINDS).optional(),
|
|
335
335
|
error: errorKindSchema.optional(),
|
|
336
|
+
errorInternal: z.string().optional(),
|
|
336
337
|
empty: z.literal(true).optional(),
|
|
337
338
|
unreadable: z.literal(true).optional(),
|
|
338
339
|
});
|
|
@@ -458,8 +458,10 @@ export interface DecisionRequest {
|
|
|
458
458
|
state: Exclude<DecisionJson, null>;
|
|
459
459
|
questions: Record<string, DecisionQuestion>;
|
|
460
460
|
/**
|
|
461
|
-
* Images by reference. Write a reference
|
|
462
|
-
* question's instructions, or in its criteria
|
|
461
|
+
* Images by reference. Write a reference in a string where its image belongs, in `state`, in a
|
|
462
|
+
* question's instructions, or in its criteria, as the whole string or inside it; the model reads
|
|
463
|
+
* the image in its place, once for each use. Keys are never read. Only the images some string
|
|
464
|
+
* references are sent, so pick references that cannot occur in your text by accident.
|
|
463
465
|
*/
|
|
464
466
|
assets?: Record<string, DecisionAsset>;
|
|
465
467
|
signal?: AbortSignal;
|
|
@@ -47,15 +47,14 @@ function mergeSpans(spans) {
|
|
|
47
47
|
return out;
|
|
48
48
|
}
|
|
49
49
|
function applySpans(text, spans) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
out = out.slice(0, span.start) + tokenFor(span.kind) + out.slice(span.end);
|
|
56
|
-
}
|
|
50
|
+
const pieces = [];
|
|
51
|
+
let at = 0;
|
|
52
|
+
for (const span of mergeSpans(spans)) {
|
|
53
|
+
pieces.push(text.slice(at, span.start), tokenFor(span.kind));
|
|
54
|
+
at = span.end;
|
|
57
55
|
}
|
|
58
|
-
|
|
56
|
+
pieces.push(text.slice(at));
|
|
57
|
+
return pieces.join('');
|
|
59
58
|
}
|
|
60
59
|
function spansFromPatterns(text, patterns, kind) {
|
|
61
60
|
const spans = [];
|
|
@@ -718,6 +718,7 @@ const TRACE_EVENTS = {
|
|
|
718
718
|
dropped_media: attr('messages', 'Media left out', 'number', 'Media the compactor does not take, left out of what it read.'),
|
|
719
719
|
failure_stop: attr('agent', 'Compactor stop', 'text', 'How the failed compactor stopped.', STOP_KINDS),
|
|
720
720
|
failure_error: attr('agent', 'Compactor error', 'text', 'The error the compactor failed with.', ERROR_KIND_OPTIONS),
|
|
721
|
+
failure_reason: attr('agent', 'Compactor reason', 'content', 'Why the compactor failed, in its own words.'),
|
|
721
722
|
failure_empty: attr('agent', 'Empty summary', 'boolean', 'The compactor replied with nothing.'),
|
|
722
723
|
failure_unreadable: attr('agent', 'Nothing to read', 'boolean', 'Everything to compact was media the compactor does not take, so it did not run.'),
|
|
723
724
|
summary: attr('messages', 'Summary', 'content', 'The summary that replaced them.'),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
const SHIPPED = [
|
|
1
|
+
import { geminiEmbeddings } from './gemini/embeddings-protocol.js';
|
|
2
|
+
import { ollamaEmbeddings } from './local/embeddings-protocol.js';
|
|
3
|
+
import { openRouterEmbeddings } from './openrouter/embeddings-protocol.js';
|
|
4
|
+
const SHIPPED = [geminiEmbeddings, openRouterEmbeddings, ollamaEmbeddings];
|
|
5
5
|
/**
|
|
6
6
|
* What a binding's embedding model takes, read from its protocol: one Theorem ships, or one of
|
|
7
7
|
* `protocols`. Undefined when neither has it, and the kernel then holds requests to the profile alone.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Protocol } from '../../kernel/protocols/protocol.js';
|
|
2
|
-
/** What a `
|
|
3
|
-
export interface
|
|
2
|
+
/** What a `geminiEmbeddings` binding sets in `options`. */
|
|
3
|
+
export interface GeminiEmbeddingsOptions {
|
|
4
4
|
/** For `gemini-embedding-001`; Gemini Embedding 2 refuses it and reads the request's `task` from the text. */
|
|
5
5
|
taskType?: 'RETRIEVAL_QUERY' | 'RETRIEVAL_DOCUMENT' | 'SEMANTIC_SIMILARITY' | 'CLASSIFICATION' | 'CLUSTERING' | 'QUESTION_ANSWERING' | 'FACT_VERIFICATION' | 'CODE_RETRIEVAL_QUERY';
|
|
6
6
|
outputDimensionality?: 768 | 1536 | 3072;
|
|
7
7
|
}
|
|
8
|
-
export declare const
|
|
8
|
+
export declare const geminiEmbeddings: Protocol<GeminiEmbeddingsOptions>;
|
|
@@ -76,8 +76,8 @@ function requestBody(input, parsed, shape) {
|
|
|
76
76
|
body.outputDimensionality = parsed.outputDimensionality;
|
|
77
77
|
return body;
|
|
78
78
|
}
|
|
79
|
-
export const
|
|
80
|
-
id: '
|
|
79
|
+
export const geminiEmbeddings = defineProtocol({
|
|
80
|
+
id: 'geminiEmbeddings',
|
|
81
81
|
name: 'Gemini Embeddings',
|
|
82
82
|
description: "Google's embedding API for text and supported media.",
|
|
83
83
|
endpoints: { embedding: '/models/{apiId}:embedContent' },
|
|
@@ -88,7 +88,7 @@ export const geminiEmbedding = defineProtocol({
|
|
|
88
88
|
create(context) {
|
|
89
89
|
return {
|
|
90
90
|
async embed(request) {
|
|
91
|
-
const parsed = modelOptions(
|
|
91
|
+
const parsed = modelOptions(geminiEmbeddings, context.apiId, request.options);
|
|
92
92
|
const taskField = TASK_FIELD_MODELS.test(context.apiId);
|
|
93
93
|
const shape = { taskField, ...(request.task ? { task: request.task } : {}) };
|
|
94
94
|
const inputs = Array.isArray(request.input) ? request.input : [request.input];
|
|
@@ -20,8 +20,11 @@ function socketTrouble(frame, during) {
|
|
|
20
20
|
/** Waits for `setupComplete`; a refusal here comes before any event, so the kernel can try the other key. */
|
|
21
21
|
async function setupComplete(frames, context) {
|
|
22
22
|
let timer;
|
|
23
|
+
const limit = context.limits.connectTimeoutMs;
|
|
23
24
|
const late = new Promise((_, reject) => {
|
|
24
|
-
|
|
25
|
+
if (limit === undefined)
|
|
26
|
+
return;
|
|
27
|
+
timer = setTimeout(() => reject(new TheoremError('timeout', `Gemini Live setup timed out after ${limit}ms`)), limit);
|
|
25
28
|
});
|
|
26
29
|
try {
|
|
27
30
|
for (;;) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { labelText } from '../../guardrails/labels.js';
|
|
3
3
|
import { defineProvider, endpointUrlSchema, header, query, } from '../../kernel/protocols/provider.js';
|
|
4
|
-
import {
|
|
4
|
+
import { geminiEmbeddings } from './embeddings-protocol.js';
|
|
5
5
|
import { geminiInteractions } from './interactions-protocol.js';
|
|
6
6
|
import { geminiLive } from './live-protocol.js';
|
|
7
7
|
import { geminiPrices } from './prices.js';
|
|
@@ -31,7 +31,7 @@ export const geminiProvider = defineProvider({
|
|
|
31
31
|
const liveOrigin = `${host.protocol === 'http:' ? 'ws:' : 'wss:'}//${host.host}`;
|
|
32
32
|
return [
|
|
33
33
|
{ protocol: geminiInteractions, baseUrl: `${origin}/v1beta` },
|
|
34
|
-
{ protocol:
|
|
34
|
+
{ protocol: geminiEmbeddings, baseUrl: `${origin}/v1beta` },
|
|
35
35
|
// probed 2026-09-23: Live's WebSocket takes the key only in the URL's query.
|
|
36
36
|
{ protocol: geminiLive, baseUrl: `${liveOrigin}/ws`, authorize: query('key') },
|
|
37
37
|
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Protocol } from '../../kernel/protocols/mod.js';
|
|
2
|
+
/** What an `ollamaEmbeddings` binding sets in `options`. */
|
|
3
|
+
export interface OllamaEmbeddingsOptions {
|
|
4
|
+
truncate?: boolean;
|
|
5
|
+
dimensions?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const ollamaEmbeddings: Protocol<OllamaEmbeddingsOptions>;
|
|
@@ -23,8 +23,8 @@ function strings(input) {
|
|
|
23
23
|
return item.parts[0].text;
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
export const
|
|
27
|
-
id: '
|
|
26
|
+
export const ollamaEmbeddings = defineProtocol({
|
|
27
|
+
id: 'ollamaEmbeddings',
|
|
28
28
|
name: 'Ollama Embeddings',
|
|
29
29
|
description: 'Ollama’s local embedding endpoint.',
|
|
30
30
|
endpoints: { embedding: '/api/embed' },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { bearer, defineProvider, endpointUrlSchema, } from '../../kernel/protocols/provider.js';
|
|
3
3
|
import { openAi } from '../open-ai/protocol.js';
|
|
4
|
-
import {
|
|
4
|
+
import { ollamaEmbeddings } from './embeddings-protocol.js';
|
|
5
5
|
/** A server on your machine or network that speaks OpenAI's chat API: Ollama, llama.cpp, vLLM, LM Studio. */
|
|
6
6
|
export const localProvider = defineProvider({
|
|
7
7
|
id: 'local',
|
|
@@ -16,7 +16,7 @@ export const localProvider = defineProvider({
|
|
|
16
16
|
serves: (at) => [
|
|
17
17
|
{ protocol: openAi, baseUrl: at.baseUrl },
|
|
18
18
|
{
|
|
19
|
-
protocol:
|
|
19
|
+
protocol: ollamaEmbeddings,
|
|
20
20
|
baseUrl: at.baseUrl,
|
|
21
21
|
url: ({ path }) => {
|
|
22
22
|
const base = new URL(at.baseUrl);
|
|
@@ -17,15 +17,15 @@ export { bearer, createProviderRegistry, defineProvider, endpointUrlSchema, head
|
|
|
17
17
|
export type { OpenWebSocket, ProviderHostOptions } from '../kernel/protocols/runtime.js';
|
|
18
18
|
export type { CredentialContext, CredentialResolver, KeyCredential, KeyVault, } from '../kernel/registry/vault.js';
|
|
19
19
|
export { embeddingModelLimits } from './embedding-limits.js';
|
|
20
|
-
export {
|
|
20
|
+
export { geminiEmbeddings } from './gemini/embeddings-protocol.js';
|
|
21
21
|
export { geminiInteractions } from './gemini/interactions-protocol.js';
|
|
22
22
|
export { geminiLive } from './gemini/live-protocol.js';
|
|
23
23
|
export { geminiProvider } from './gemini/provider.js';
|
|
24
|
-
export {
|
|
24
|
+
export { ollamaEmbeddings } from './local/embeddings-protocol.js';
|
|
25
25
|
export { localProvider } from './local/provider.js';
|
|
26
26
|
export { openAi } from './open-ai/protocol.js';
|
|
27
27
|
export { openRouterDecisions } from './openrouter/decisions-protocol.js';
|
|
28
|
-
export {
|
|
28
|
+
export { openRouterEmbeddings } from './openrouter/embeddings-protocol.js';
|
|
29
29
|
export { openRouterImages } from './openrouter/images-protocol.js';
|
|
30
30
|
export { openRouterOpenAi } from './openrouter/protocol.js';
|
|
31
31
|
export { openRouterProvider } from './openrouter/provider.js';
|