@theoremjs/agents 0.3.4-next.387.g8bea7ff → 0.3.4-next.389.gecc7333
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.
|
@@ -14,6 +14,7 @@ import { AgentCallFailed, agentToolOrigin } from './agent.js';
|
|
|
14
14
|
import { coordinateTool } from './coordinate.js';
|
|
15
15
|
import { dispatchToolBody, failedActivity, failedActivityOf, failureEvent, messageOf, networkBlocked, networkBlockedEvent, requestChecks, sourceEvents, startToolExecution, toolCallArguments, toolEvent, } from './events.js';
|
|
16
16
|
import { HandlerEvents } from './handler-events.js';
|
|
17
|
+
import { fetchToolMedia } from './media-fetch.js';
|
|
17
18
|
import { formatToolFailureForModel, formatToolResult, frameToolFailure } from './model-text.js';
|
|
18
19
|
import { operationUnavailable, operationWireValue } from './operation.js';
|
|
19
20
|
import { checkPermission, gateDetails, isGateResumeDenied, isGateResumeGranted, isResumeContinuation, } from './permission.js';
|
|
@@ -665,6 +666,18 @@ function authorizeSavedDisclosure(args, tool, outcome) {
|
|
|
665
666
|
if (guard.event?.action === 'block' || guard.event?.action === 'redact')
|
|
666
667
|
throw blocked();
|
|
667
668
|
}
|
|
669
|
+
/** A successful remote call's result with the pictures its tool's `media` names, fetched and added beside any the server sent. */
|
|
670
|
+
async function* withNamedMedia(tool, outcome, ctx, base) {
|
|
671
|
+
if (outcome.kind !== 'ok' || !tool.media)
|
|
672
|
+
return outcome;
|
|
673
|
+
const fetched = await fetchToolMedia(tool.media, outcome.outputRaw, ctx);
|
|
674
|
+
if (fetched.warning)
|
|
675
|
+
yield toolEvent(base, { phase: 'warning', warning: fetched.warning });
|
|
676
|
+
if (fetched.parts.length === 0)
|
|
677
|
+
return outcome;
|
|
678
|
+
const parts = [...(outcome.modelResult.parts ?? []), ...fetched.parts];
|
|
679
|
+
return { ...outcome, modelResult: { ...outcome.modelResult, parts } };
|
|
680
|
+
}
|
|
668
681
|
/** Settles an outcome the kernel reached without a handler's own result: a remote body, or a sign-in. */
|
|
669
682
|
async function* settleBodyOutcome(args) {
|
|
670
683
|
const { outcome, tool, base, callId, safeInput, stages, guard, labels } = args;
|
|
@@ -1097,9 +1110,10 @@ async function* settleByType(tools, tool, safeInput, fullCtx, base, snapshot, st
|
|
|
1097
1110
|
});
|
|
1098
1111
|
}
|
|
1099
1112
|
const guard = resultGuard(tools, tool, fullCtx, snapshot, stages?.span);
|
|
1100
|
-
const
|
|
1113
|
+
const body = tool.type === 'http'
|
|
1101
1114
|
? yield* executeHttpTool(tool, safeInput, fullCtx, base, stages, beginBody, new HandlerEvents(base, guard.handlerEvents(stages?.drafts === true)))
|
|
1102
1115
|
: yield* executeMcpTool(tool, safeInput, fullCtx, base, stages, tools.mcpSessions, beginBody);
|
|
1116
|
+
const remoteOutcome = yield* withNamedMedia(tool, body, fullCtx, base);
|
|
1103
1117
|
return yield* settleBodyOutcome({
|
|
1104
1118
|
outcome: remoteOutcome,
|
|
1105
1119
|
tool,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InteractionPart, ToolWarning } from '../turn-events.js';
|
|
2
|
+
import type { ToolContext } from './types.js';
|
|
3
|
+
/** The most pictures one call hands the model. */
|
|
4
|
+
export declare const MAX_TOOL_MEDIA_FILES = 4;
|
|
5
|
+
/**
|
|
6
|
+
* The pictures a tool's `media(output)` names, fetched through the profile's network policy (every
|
|
7
|
+
* hop, redirects included) and handed to the model as image parts. A picture that is blocked, slow,
|
|
8
|
+
* too big or not an image is left out and reported in one `media_unfetched` warning: the call
|
|
9
|
+
* itself stays whole.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fetchToolMedia(media: (output: never) => string[], output: unknown, ctx: ToolContext): Promise<{
|
|
12
|
+
parts: InteractionPart[];
|
|
13
|
+
warning?: ToolWarning;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { fetchGuarded } from '../../guardrails/network.js';
|
|
2
|
+
import { bytesToBase64 } from '../util/base64.js';
|
|
3
|
+
import { toolNetworkPolicy } from './events.js';
|
|
4
|
+
/** The most pictures one call hands the model. */
|
|
5
|
+
export const MAX_TOOL_MEDIA_FILES = 4;
|
|
6
|
+
/** The largest picture fetched; a larger one is left out, not cut. */
|
|
7
|
+
const MAX_TOOL_MEDIA_BYTES = 5 * 1024 * 1024;
|
|
8
|
+
/** How long each picture may take, so a slow host cannot hold the call. */
|
|
9
|
+
const MEDIA_FETCH_TIMEOUT_MS = 15_000;
|
|
10
|
+
const IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
11
|
+
/** The body, read up to `maxBytes`; `undefined` when it runs past. */
|
|
12
|
+
async function readCappedBytes(response, maxBytes) {
|
|
13
|
+
const declared = Number(response.headers.get('content-length'));
|
|
14
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
15
|
+
await response.body?.cancel().catch(() => { });
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (!response.body)
|
|
19
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
20
|
+
const reader = response.body.getReader();
|
|
21
|
+
const chunks = [];
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (;;) {
|
|
24
|
+
const { done, value } = await reader.read();
|
|
25
|
+
if (done)
|
|
26
|
+
break;
|
|
27
|
+
total += value.byteLength;
|
|
28
|
+
if (total > maxBytes) {
|
|
29
|
+
await reader.cancel().catch(() => { });
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
chunks.push(value);
|
|
33
|
+
}
|
|
34
|
+
const bytes = new Uint8Array(total);
|
|
35
|
+
let at = 0;
|
|
36
|
+
for (const chunk of chunks) {
|
|
37
|
+
bytes.set(chunk, at);
|
|
38
|
+
at += chunk.byteLength;
|
|
39
|
+
}
|
|
40
|
+
return bytes;
|
|
41
|
+
}
|
|
42
|
+
async function fetchOne(url, ctx) {
|
|
43
|
+
const timeout = AbortSignal.timeout(MEDIA_FETCH_TIMEOUT_MS);
|
|
44
|
+
const signal = ctx.signal ? AbortSignal.any([ctx.signal, timeout]) : timeout;
|
|
45
|
+
const response = await fetchGuarded(url, { method: 'GET', headers: { Accept: 'image/*' }, signal }, {
|
|
46
|
+
policy: toolNetworkPolicy(ctx),
|
|
47
|
+
followRedirects: true,
|
|
48
|
+
resolveHost: ctx.resolveHost,
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
throw new Error(`HTTP ${response.status}`); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
52
|
+
const mimeType = (response.headers.get('content-type') ?? '').split(';')[0]?.trim().toLowerCase();
|
|
53
|
+
if (!mimeType || !IMAGE_TYPES.has(mimeType)) {
|
|
54
|
+
await response.body?.cancel().catch(() => { });
|
|
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)
|
|
56
|
+
}
|
|
57
|
+
const bytes = await readCappedBytes(response, MAX_TOOL_MEDIA_BYTES);
|
|
58
|
+
if (!bytes)
|
|
59
|
+
throw new Error(`larger than ${MAX_TOOL_MEDIA_BYTES} bytes`); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
60
|
+
return { type: 'image', mimeType, data: bytesToBase64(bytes) };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The pictures a tool's `media(output)` names, fetched through the profile's network policy (every
|
|
64
|
+
* hop, redirects included) and handed to the model as image parts. A picture that is blocked, slow,
|
|
65
|
+
* too big or not an image is left out and reported in one `media_unfetched` warning: the call
|
|
66
|
+
* itself stays whole.
|
|
67
|
+
*/
|
|
68
|
+
export async function fetchToolMedia(media, output, ctx) {
|
|
69
|
+
let urls;
|
|
70
|
+
try {
|
|
71
|
+
urls = [...new Set(media(output))].slice(0, MAX_TOOL_MEDIA_FILES);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
// labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
75
|
+
return { parts: [], warning: unfetched([`the media hook threw: ${String(err)}`]) };
|
|
76
|
+
}
|
|
77
|
+
const settled = await Promise.allSettled(urls.map((url) => fetchOne(url, ctx)));
|
|
78
|
+
const parts = [];
|
|
79
|
+
const reasons = [];
|
|
80
|
+
settled.forEach((result, index) => {
|
|
81
|
+
if (result.status === 'fulfilled')
|
|
82
|
+
parts.push(result.value);
|
|
83
|
+
else
|
|
84
|
+
reasons.push(`${new URL(urls[index] ?? 'https://invalid.invalid').hostname}: ${String(result.reason instanceof Error ? result.reason.message : result.reason)}`);
|
|
85
|
+
});
|
|
86
|
+
return { parts, ...(reasons.length ? { warning: unfetched(reasons) } : {}) };
|
|
87
|
+
}
|
|
88
|
+
function unfetched(reasons) {
|
|
89
|
+
return {
|
|
90
|
+
code: 'media_unfetched',
|
|
91
|
+
severity: 'warning',
|
|
92
|
+
message: `Some pictures the tool named were left out. ${reasons.join('; ')}`, // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -160,6 +160,15 @@ export interface ToolOutputHooks<TOut = unknown> {
|
|
|
160
160
|
/** A source that fails `sourceSchema`, or a throw, is a `sources_invalid` warning and is not cited. */
|
|
161
161
|
sources?: (output: TOut) => Source[];
|
|
162
162
|
}
|
|
163
|
+
/** For a tool whose result names pictures by address; function tools return the bytes themselves as `parts`. */
|
|
164
|
+
export interface ToolMediaHooks<TOut = unknown> {
|
|
165
|
+
/**
|
|
166
|
+
* The https addresses of pictures in `output` the model should see, at most `MAX_TOOL_MEDIA_FILES`
|
|
167
|
+
* of them. The kernel fetches each one through the profile's network policy and hands the model
|
|
168
|
+
* the image as a part; one it cannot fetch is a `media_unfetched` warning, never a failed call.
|
|
169
|
+
*/
|
|
170
|
+
media?: (output: TOut) => string[];
|
|
171
|
+
}
|
|
163
172
|
export interface FunctionToolDef<TIn = unknown, TOut = unknown> extends ToolBase, ToolHostHooks<TIn>, ToolOutputHooks<TOut> {
|
|
164
173
|
type: 'function';
|
|
165
174
|
input: z.ZodType<TIn>;
|
|
@@ -201,7 +210,7 @@ export interface ToolAuthConfig {
|
|
|
201
210
|
clientId?: string;
|
|
202
211
|
redirectUri?: string;
|
|
203
212
|
}
|
|
204
|
-
export interface HttpToolDef<TIn = unknown, TOut = unknown> extends ToolBase, ToolHostHooks<TIn>, ToolOutputHooks<TOut> {
|
|
213
|
+
export interface HttpToolDef<TIn = unknown, TOut = unknown> extends ToolBase, ToolHostHooks<TIn>, ToolOutputHooks<TOut>, ToolMediaHooks<TOut> {
|
|
205
214
|
type: 'http';
|
|
206
215
|
input: z.ZodType<TIn>;
|
|
207
216
|
output: z.ZodType<TOut>;
|
|
@@ -228,7 +237,7 @@ export interface HttpToolLimit {
|
|
|
228
237
|
requests: number;
|
|
229
238
|
windowMs: number;
|
|
230
239
|
}
|
|
231
|
-
export interface McpToolDef<TIn = unknown, TOut = unknown> extends ToolBase, ToolHostHooks<TIn>, ToolOutputHooks<TOut> {
|
|
240
|
+
export interface McpToolDef<TIn = unknown, TOut = unknown> extends ToolBase, ToolHostHooks<TIn>, ToolOutputHooks<TOut>, ToolMediaHooks<TOut> {
|
|
232
241
|
type: 'mcp';
|
|
233
242
|
input: z.ZodType<TIn>;
|
|
234
243
|
output: z.ZodType<TOut>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theoremjs/agents",
|
|
3
|
-
"version": "0.3.4-next.
|
|
3
|
+
"version": "0.3.4-next.389.gecc7333",
|
|
4
4
|
"description": "A flat TypeScript agent kernel for typed profiles, deterministic turn execution, registered tools, provider adapters, guardrails, and host-injected traces.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|