toolnexus 0.16.0 → 0.18.0
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 +46 -0
- package/dist/agents/agent.d.ts +11 -1
- package/dist/agents/agent.js +0 -9
- package/dist/agents/compaction.d.ts +0 -25
- package/dist/agents/compaction.js +41 -2
- package/dist/agents/loop.d.ts +3 -2
- package/dist/agents/loop.js +10 -14
- package/dist/agents/runtime.d.ts +4 -3
- package/dist/agents/runtime.js +8 -17
- package/dist/builtin.js +24 -3
- package/dist/classifier.d.ts +334 -0
- package/dist/classifier.js +630 -0
- package/dist/client.d.ts +40 -7
- package/dist/client.js +39 -33
- package/dist/content.d.ts +158 -0
- package/dist/content.js +379 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp.d.ts +9 -0
- package/dist/mcp.js +59 -4
- package/dist/mcpserve.js +27 -1
- package/dist/retry.d.ts +30 -0
- package/dist/retry.js +55 -0
- package/dist/translate.js +30 -2
- package/dist/types.d.ts +8 -0
- package/dist/wire.d.ts +40 -0
- package/dist/wire.js +104 -0
- package/package.json +5 -3
- package/src/agents/agent.ts +2 -1
- package/src/agents/compaction.ts +39 -1
- package/src/agents/loop.ts +17 -4
- package/src/agents/runtime.ts +13 -5
- package/src/builtin.ts +23 -3
- package/src/classifier.ts +832 -0
- package/src/client.ts +84 -44
- package/src/content.ts +448 -0
- package/src/index.ts +3 -0
- package/src/mcp.ts +55 -4
- package/src/mcpserve.ts +26 -3
- package/src/retry.ts +57 -0
- package/src/translate.ts +25 -1
- package/src/types.ts +9 -0
- package/src/wire.ts +131 -0
package/dist/client.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Toolkit } from "./toolkit.js";
|
|
7
7
|
import type { ToolResult, Request, Answer } from "./types.js";
|
|
8
|
+
import type { PromptInput, UnsupportedPartMode } from "./content.js";
|
|
8
9
|
import type { TranslateRequest, TranslateResult } from "./translate.js";
|
|
9
10
|
export type ClientStyle = "openai" | "anthropic";
|
|
10
11
|
export interface ClientOptions {
|
|
@@ -16,10 +17,19 @@ export interface ClientOptions {
|
|
|
16
17
|
systemPrompt?: string;
|
|
17
18
|
maxTurns?: number;
|
|
18
19
|
hooks?: Hooks;
|
|
19
|
-
/** Retries on transient LLM errors (429
|
|
20
|
+
/** Retries on transient LLM errors (`429`/`500`/`502`/`503`/`504`/`529` + network). Default 2.
|
|
21
|
+
* Widen the status set with `retryableStatuses`. */
|
|
20
22
|
retries?: number;
|
|
21
23
|
/** Base backoff in ms (exponential + jitter). Default 500. */
|
|
22
24
|
retryBaseMs?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Extra HTTP statuses to treat as retryable, ADDED to the default set
|
|
27
|
+
* (`429`/`500`/`502`/`503`/`504`/`529`). It can only widen: a host cannot remove `429` and
|
|
28
|
+
* lose `Retry-After` handling with it. This sets the DEFAULT classification; `onError` still
|
|
29
|
+
* runs per attempt and has the final say, so `onError` returning `"fail"` overrides a status
|
|
30
|
+
* listed here. Example: a Cloudflare-fronted origin that answers `520`–`527`.
|
|
31
|
+
*/
|
|
32
|
+
retryableStatuses?: readonly number[];
|
|
23
33
|
/** Whole-run deadline in ms; aborts the run (and its in-flight request) when exceeded. */
|
|
24
34
|
timeoutMs?: number;
|
|
25
35
|
/** Conversation provider for `ask(prompt, { id })`. Default: in-memory (process lifetime).
|
|
@@ -50,6 +60,13 @@ export interface ClientOptions {
|
|
|
50
60
|
* today. A "retry" is always bounded by `retries`; the classifier cannot loop unbounded.
|
|
51
61
|
* There is no "suspend" tier — suspension (§10) stays a user-action pause, not an error path. */
|
|
52
62
|
onError?: (info: ErrorInfo) => ErrorTier;
|
|
63
|
+
/** §1B. Reject a content part carrying more than this many **decoded** bytes (never the
|
|
64
|
+
* +33% base64 string). Unset ⇒ no limit, as today. */
|
|
65
|
+
maxPartBytes?: number;
|
|
66
|
+
/** §8A. Overrides the provenance rule uniformly: `"error"` fails on any part the style
|
|
67
|
+
* cannot represent (attached or tool-derived), `"text"` degrades every one of them to a
|
|
68
|
+
* text placeholder. Unset ⇒ attached parts error, derived parts degrade. */
|
|
69
|
+
onUnsupportedPart?: UnsupportedPartMode;
|
|
53
70
|
}
|
|
54
71
|
/** §8 Resilience. What to do with a failed LLM call. */
|
|
55
72
|
export type ErrorTier = "retry" | "fail";
|
|
@@ -61,7 +78,8 @@ export interface ErrorInfo {
|
|
|
61
78
|
status?: number;
|
|
62
79
|
/** Zero-based attempt index (0 = first try). */
|
|
63
80
|
attempt: number;
|
|
64
|
-
/** Whether `status`/the error is in the default retryable set
|
|
81
|
+
/** Whether `status`/the error is in the default retryable set
|
|
82
|
+
* (429/500/502/503/504/529/network, plus anything `retryableStatuses` added). */
|
|
65
83
|
retryable: boolean;
|
|
66
84
|
}
|
|
67
85
|
/**
|
|
@@ -91,6 +109,18 @@ export type MetricEvent = {
|
|
|
91
109
|
totalTokens: number;
|
|
92
110
|
ms: number;
|
|
93
111
|
error?: string;
|
|
112
|
+
} | {
|
|
113
|
+
event: "classifier.evaluate";
|
|
114
|
+
model: string;
|
|
115
|
+
status: "ok" | "error";
|
|
116
|
+
ms: number;
|
|
117
|
+
promptTokens: number;
|
|
118
|
+
completionTokens: number;
|
|
119
|
+
error?: string;
|
|
120
|
+
} | {
|
|
121
|
+
event: "classifier.warning";
|
|
122
|
+
question: string;
|
|
123
|
+
warning: string;
|
|
94
124
|
};
|
|
95
125
|
/**
|
|
96
126
|
* Where `ask()` conversations are remembered — two methods. Ship the in-memory
|
|
@@ -243,6 +273,8 @@ export declare class Client {
|
|
|
243
273
|
private openaiBody;
|
|
244
274
|
/** §8 Gap 5. Anthropic-style body; omits tools when the tool list is empty. */
|
|
245
275
|
private anthropicBody;
|
|
276
|
+
/** The §8A/§1B shaping options every wire build shares. */
|
|
277
|
+
private wireOpts;
|
|
246
278
|
/** Prometheus text exposition of cumulative metrics (§8). Always valid, empty-but-valid before activity. */
|
|
247
279
|
metrics(): string;
|
|
248
280
|
/** Feed a semantic metric event to the built-in registry and the optional onMetric sink. */
|
|
@@ -252,7 +284,7 @@ export declare class Client {
|
|
|
252
284
|
private endRun;
|
|
253
285
|
/** Emit a `run` error metric event (once, on a thrown run). */
|
|
254
286
|
private emitRunError;
|
|
255
|
-
run(prompt:
|
|
287
|
+
run(prompt: PromptInput, ctx: {
|
|
256
288
|
toolkit: Toolkit;
|
|
257
289
|
signal?: AbortSignal;
|
|
258
290
|
history?: any[];
|
|
@@ -285,7 +317,7 @@ export declare class Client {
|
|
|
285
317
|
* the answer — so the next `ask` with the same `id` continues it. Without an
|
|
286
318
|
* `id` it is a stateless one-shot (identical to `run`).
|
|
287
319
|
*/
|
|
288
|
-
ask(prompt:
|
|
320
|
+
ask(prompt: PromptInput, ctx: {
|
|
289
321
|
toolkit: Toolkit;
|
|
290
322
|
id?: string;
|
|
291
323
|
on_text?: (delta: string) => void;
|
|
@@ -301,7 +333,7 @@ export declare class Client {
|
|
|
301
333
|
* With an `id`, it is stateful (like `ask`): the thread's transcript is loaded as history before
|
|
302
334
|
* streaming, and saved back to the ConversationStore on the terminal `done` event. No `id` ⇒ stateless.
|
|
303
335
|
*/
|
|
304
|
-
stream(prompt:
|
|
336
|
+
stream(prompt: PromptInput, ctx: {
|
|
305
337
|
toolkit: Toolkit;
|
|
306
338
|
id?: string;
|
|
307
339
|
signal?: AbortSignal;
|
|
@@ -309,7 +341,8 @@ export declare class Client {
|
|
|
309
341
|
private system;
|
|
310
342
|
/** Build a run-scoped abort signal from the optional run-level timeout + an external signal. */
|
|
311
343
|
private makeSignal;
|
|
312
|
-
/** fetch with retry + exponential backoff on
|
|
344
|
+
/** fetch with retry + exponential backoff on the default retryable set (429/500/502/503/504/529)
|
|
345
|
+
* plus network errors and anything `retryableStatuses` added, honoring Retry-After; aborts via signal.
|
|
313
346
|
* The retry-vs-fail decision is the host's `onError` (default: retryable-within-budget ⇒ retry). */
|
|
314
347
|
private llmFetch;
|
|
315
348
|
/** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
|
|
@@ -387,7 +420,7 @@ export declare class Conversation {
|
|
|
387
420
|
messages: any[];
|
|
388
421
|
constructor(client: Client, toolkit: Toolkit, signal?: AbortSignal | undefined);
|
|
389
422
|
/** Send the next user turn; prior history is retained automatically. */
|
|
390
|
-
send(prompt:
|
|
423
|
+
send(prompt: PromptInput): Promise<RunResult>;
|
|
391
424
|
/** Reset the conversation memory. */
|
|
392
425
|
reset(): void;
|
|
393
426
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { pendingOf } from "./types.js";
|
|
2
|
+
import { isRetryableStatus, retryAfterMs } from "./retry.js";
|
|
3
|
+
import { toOpenAIWire, toAnthropicWire, checkPromptParts } from "./wire.js";
|
|
2
4
|
import { finishReasonFor, argsString, openAIMessagesToAnthropic, openAIToolsToAnthropic, openAIToolChoiceToAnthropic, hasSystemMessage, } from "./translate.js";
|
|
3
5
|
/** Default conversation provider — keeps transcripts in memory for the client's lifetime. */
|
|
4
6
|
export class InMemoryConversationStore {
|
|
@@ -11,29 +13,6 @@ export class InMemoryConversationStore {
|
|
|
11
13
|
this.map.set(id, [...messages]);
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
|
-
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
|
|
15
|
-
/** ~68 years; the widest whole-second count all seven ports represent exactly. */
|
|
16
|
-
const RETRY_AFTER_MAX_SECONDS = 2147483647;
|
|
17
|
-
/**
|
|
18
|
-
* Honour `Retry-After` only in its delay-seconds form: a run of ASCII digits
|
|
19
|
-
* (RFC 9110 §10.2.3) in 0…2147483647, returned in milliseconds.
|
|
20
|
-
*
|
|
21
|
-
* `Number()` is far too permissive for this — it accepts `"0.5"`, `"-5"`, `"1e3"`
|
|
22
|
-
* and `" "` (as 0), which is how this port came to wait for fractional seconds
|
|
23
|
-
* that five of the other six rejected outright. Fractional, signed, HTTP-date and
|
|
24
|
-
* out-of-range values are not delays we can honour, so the caller falls back to
|
|
25
|
-
* backoff rather than guessing. `0` is a real answer, so it returns `0`, not
|
|
26
|
-
* `null`, and callers must use `??` rather than `||`.
|
|
27
|
-
*/
|
|
28
|
-
function retryAfterMs(raw) {
|
|
29
|
-
if (raw == null)
|
|
30
|
-
return null;
|
|
31
|
-
const s = raw.trim();
|
|
32
|
-
if (!/^[0-9]+$/.test(s))
|
|
33
|
-
return null;
|
|
34
|
-
const secs = Number(s);
|
|
35
|
-
return secs <= RETRY_AFTER_MAX_SECONDS ? secs * 1000 : null;
|
|
36
|
-
}
|
|
37
16
|
function delay(ms, signal) {
|
|
38
17
|
return new Promise((resolve, reject) => {
|
|
39
18
|
const t = setTimeout(resolve, ms);
|
|
@@ -207,7 +186,7 @@ export class Client {
|
|
|
207
186
|
/** §8 Gap 5. OpenAI-style body; omits tools/tool_choice when the tool list is empty. Key order
|
|
208
187
|
* matches the pre-change body so a no-options call is byte-identical. */
|
|
209
188
|
openaiBody(messages, tools, stream) {
|
|
210
|
-
const b = { model: this.opts.model, messages };
|
|
189
|
+
const b = { model: this.opts.model, messages: toOpenAIWire(messages, this.wireOpts()) };
|
|
211
190
|
if (tools.length) {
|
|
212
191
|
b.tools = tools;
|
|
213
192
|
b.tool_choice = "auto";
|
|
@@ -220,13 +199,17 @@ export class Client {
|
|
|
220
199
|
}
|
|
221
200
|
/** §8 Gap 5. Anthropic-style body; omits tools when the tool list is empty. */
|
|
222
201
|
anthropicBody(system, messages, tools, stream) {
|
|
223
|
-
const b = { model: this.opts.model, max_tokens: 4096, system, messages };
|
|
202
|
+
const b = { model: this.opts.model, max_tokens: 4096, system, messages: toAnthropicWire(messages, this.wireOpts()) };
|
|
224
203
|
if (tools.length)
|
|
225
204
|
b.tools = tools;
|
|
226
205
|
if (stream)
|
|
227
206
|
b.stream = true;
|
|
228
207
|
return this.finalizeBody(b);
|
|
229
208
|
}
|
|
209
|
+
/** The §8A/§1B shaping options every wire build shares. */
|
|
210
|
+
wireOpts() {
|
|
211
|
+
return { onUnsupportedPart: this.opts.onUnsupportedPart, maxPartBytes: this.opts.maxPartBytes };
|
|
212
|
+
}
|
|
230
213
|
/** Prometheus text exposition of cumulative metrics (§8). Always valid, empty-but-valid before activity. */
|
|
231
214
|
metrics() {
|
|
232
215
|
return this.registry.render();
|
|
@@ -247,6 +230,7 @@ export class Client {
|
|
|
247
230
|
this.emit({ event: "run", model: this.opts.model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart, error: e instanceof Error ? e.message : String(e) });
|
|
248
231
|
}
|
|
249
232
|
async run(prompt, ctx) {
|
|
233
|
+
checkPromptParts(prompt, this.wireOpts());
|
|
250
234
|
return this.opts.style === "anthropic"
|
|
251
235
|
? this.runAnthropic(prompt, ctx.toolkit, ctx.signal, ctx.history)
|
|
252
236
|
: this.runOpenAI(prompt, ctx.toolkit, ctx.signal, ctx.history);
|
|
@@ -433,6 +417,7 @@ export class Client {
|
|
|
433
417
|
* streaming, and saved back to the ConversationStore on the terminal `done` event. No `id` ⇒ stateless.
|
|
434
418
|
*/
|
|
435
419
|
async *stream(prompt, ctx) {
|
|
420
|
+
checkPromptParts(prompt, this.wireOpts());
|
|
436
421
|
const history = ctx.id ? (await this.store.get(ctx.id)) ?? [] : undefined;
|
|
437
422
|
const gen = this.opts.style === "anthropic"
|
|
438
423
|
? this.streamAnthropic(prompt, ctx.toolkit, ctx.signal, history)
|
|
@@ -461,7 +446,8 @@ export class Client {
|
|
|
461
446
|
}
|
|
462
447
|
return ctrl.signal;
|
|
463
448
|
}
|
|
464
|
-
/** fetch with retry + exponential backoff on
|
|
449
|
+
/** fetch with retry + exponential backoff on the default retryable set (429/500/502/503/504/529)
|
|
450
|
+
* plus network errors and anything `retryableStatuses` added, honoring Retry-After; aborts via signal.
|
|
465
451
|
* The retry-vs-fail decision is the host's `onError` (default: retryable-within-budget ⇒ retry). */
|
|
466
452
|
async llmFetch(url, init, signal) {
|
|
467
453
|
const retries = this.opts.retries ?? 2;
|
|
@@ -475,7 +461,7 @@ export class Client {
|
|
|
475
461
|
const res = await (this.opts.fetch ?? fetch)(url, { ...init, signal });
|
|
476
462
|
if (res.ok)
|
|
477
463
|
return res;
|
|
478
|
-
const retryable =
|
|
464
|
+
const retryable = isRetryableStatus(res.status, this.opts.retryableStatuses);
|
|
479
465
|
const tier = classify({ status: res.status, attempt, retryable });
|
|
480
466
|
if (tier === "fail" || attempt === retries)
|
|
481
467
|
return res; // caller surfaces the non-ok status
|
|
@@ -602,7 +588,7 @@ export class Client {
|
|
|
602
588
|
// on resume). Mirrors the streaming path.
|
|
603
589
|
for (const s of settled) {
|
|
604
590
|
toolCalls.push({ name: s.call.function.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata });
|
|
605
|
-
messages.push(
|
|
591
|
+
messages.push(toolMessage(s.call.id, s.call.function.name, s.result));
|
|
606
592
|
if (s.halted)
|
|
607
593
|
return this.pendingRun(runStart, s.halted, messages, toolCalls, turns, usage);
|
|
608
594
|
}
|
|
@@ -710,7 +696,7 @@ export class Client {
|
|
|
710
696
|
let halted;
|
|
711
697
|
for (const s of settled) {
|
|
712
698
|
toolCalls.push({ name: s.use.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata });
|
|
713
|
-
content.push(
|
|
699
|
+
content.push(toolResultBlock(s.use.id, s.result));
|
|
714
700
|
if (s.halted) {
|
|
715
701
|
halted = s.halted;
|
|
716
702
|
break;
|
|
@@ -820,13 +806,13 @@ export class Client {
|
|
|
820
806
|
result = r.result;
|
|
821
807
|
if (r.halted) {
|
|
822
808
|
toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
823
|
-
messages.push(
|
|
809
|
+
messages.push(toolMessage(c.id, c.name, result));
|
|
824
810
|
yield { type: "done", result: this.pendingRun(runStart, r.halted, messages, toolCalls, turns, usage) };
|
|
825
811
|
return;
|
|
826
812
|
}
|
|
827
813
|
}
|
|
828
814
|
toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
829
|
-
messages.push(
|
|
815
|
+
messages.push(toolMessage(c.id, c.name, result));
|
|
830
816
|
yield { type: "tool_result", id: c.id, name: c.name, output: result.output, isError: result.isError };
|
|
831
817
|
}
|
|
832
818
|
}
|
|
@@ -927,14 +913,14 @@ export class Client {
|
|
|
927
913
|
result = r.result;
|
|
928
914
|
if (r.halted) {
|
|
929
915
|
toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
930
|
-
results.push(
|
|
916
|
+
results.push(toolResultBlock(u.id, result));
|
|
931
917
|
messages.push({ role: "user", content: results });
|
|
932
918
|
yield { type: "done", result: this.pendingRun(runStart, r.halted, messages, toolCalls, turns, usage) };
|
|
933
919
|
return;
|
|
934
920
|
}
|
|
935
921
|
}
|
|
936
922
|
toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
937
|
-
results.push(
|
|
923
|
+
results.push(toolResultBlock(u.id, result));
|
|
938
924
|
yield { type: "tool_result", id: u.id, name: u.name, output: result.output, isError: result.isError };
|
|
939
925
|
}
|
|
940
926
|
messages.push({ role: "user", content: results });
|
|
@@ -1086,6 +1072,26 @@ function safeJson(s) {
|
|
|
1086
1072
|
return {};
|
|
1087
1073
|
}
|
|
1088
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* A canonical `tool` turn. With no parts this is byte-identical to the pre-0.17 message;
|
|
1077
|
+
* with parts it also carries them (and the tool `name` the §8A relocation header needs) —
|
|
1078
|
+
* both consumed and stripped by the wire build, so neither reaches OpenAI.
|
|
1079
|
+
*/
|
|
1080
|
+
function toolMessage(id, name, result) {
|
|
1081
|
+
const m = { role: "tool", tool_call_id: id, content: result.output };
|
|
1082
|
+
if (result.parts?.length) {
|
|
1083
|
+
m.name = name;
|
|
1084
|
+
m.parts = result.parts;
|
|
1085
|
+
}
|
|
1086
|
+
return m;
|
|
1087
|
+
}
|
|
1088
|
+
/** A canonical Anthropic `tool_result` block; parts ride along and are emitted natively. */
|
|
1089
|
+
function toolResultBlock(id, result) {
|
|
1090
|
+
const b = { type: "tool_result", tool_use_id: id, content: result.output, is_error: result.isError };
|
|
1091
|
+
if (result.parts?.length)
|
|
1092
|
+
b.parts = result.parts;
|
|
1093
|
+
return b;
|
|
1094
|
+
}
|
|
1089
1095
|
function lastText(messages) {
|
|
1090
1096
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1091
1097
|
if (messages[i].role === "assistant" && typeof messages[i].content === "string")
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export type ContentPart = {
|
|
2
|
+
type: "text";
|
|
3
|
+
text: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: "image";
|
|
6
|
+
mimeType: string;
|
|
7
|
+
data?: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: "file";
|
|
11
|
+
mimeType: string;
|
|
12
|
+
data?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "audio";
|
|
17
|
+
mimeType: string;
|
|
18
|
+
data?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
};
|
|
21
|
+
/** A prompt is a string or an ordered list of parts — ordering is semantic to a model. */
|
|
22
|
+
export type PromptInput = string | ContentPart[];
|
|
23
|
+
/** The two provider styles that have a request path (`toGemini` is declarations only). */
|
|
24
|
+
export type PartStyle = "openai" | "anthropic";
|
|
25
|
+
/** How a part got here. Drives the unsupported-part rule (§8A): the caller asked for an
|
|
26
|
+
* attached part, so silently changing it is the betrayal; nobody asked for a part an MCP
|
|
27
|
+
* server volunteered, so failing the run over it is a regression. */
|
|
28
|
+
export type PartProvenance = "attached" | "derived";
|
|
29
|
+
/** What to do with a part the style cannot represent; overrides provenance uniformly. */
|
|
30
|
+
export type UnsupportedPartMode = "error" | "text";
|
|
31
|
+
/** Why a part could not be constructed or sent (§1B / §8A). `code` is the machine-readable half. */
|
|
32
|
+
export declare class ContentPartError extends Error {
|
|
33
|
+
readonly code: "source-conflict" | "source-missing" | "unknown-extension" | "too-large" | "unsupported";
|
|
34
|
+
constructor(message: string, code: "source-conflict" | "source-missing" | "unknown-extension" | "too-large" | "unsupported");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The media extension table (§6 `read`) — fixed, shared with the edge constructors,
|
|
38
|
+
* identical in every port. No magic-byte sniffing and no platform mime database:
|
|
39
|
+
* `/etc/mime.types` varies per machine and would break cross-port parity.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MEDIA_TYPES: Record<string, {
|
|
42
|
+
mimeType: string;
|
|
43
|
+
type: "image" | "file" | "audio";
|
|
44
|
+
}>;
|
|
45
|
+
/** Lower-cased extension without the dot, or "" when there is none. */
|
|
46
|
+
export declare function extensionOf(p: string): string;
|
|
47
|
+
/** The table entry for a path/URL, or undefined when the extension is not media. */
|
|
48
|
+
export declare function mediaTypeFor(p: string): {
|
|
49
|
+
mimeType: string;
|
|
50
|
+
type: "image" | "file" | "audio";
|
|
51
|
+
} | undefined;
|
|
52
|
+
/** Part kind for a mime type: `image/*` ⇒ image, `audio/*` ⇒ audio, everything else ⇒ file. */
|
|
53
|
+
export declare function partTypeForMime(mimeType: string): "image" | "file" | "audio";
|
|
54
|
+
/** Decoded byte length of standard, padded base64 — without decoding it (RFC 4648 §4:
|
|
55
|
+
* `floor(len/4)*3 - padCount`). `<bytes>` in every rendered string is a plain integer, never
|
|
56
|
+
* a fraction, so this floors the group count rather than assuming `len` is a multiple of 4. */
|
|
57
|
+
export declare function base64Bytes(b64: string): number;
|
|
58
|
+
/** Decoded byte length of a part's payload; a url-only part carries no bytes here. */
|
|
59
|
+
export declare function partBytes(part: ContentPart): number;
|
|
60
|
+
/** §1B token estimate for a part, from its decoded byte count: `max(85, floor(bytes/750))`.
|
|
61
|
+
* Measured against real providers: image cost is not proportional to bytes, so this is an
|
|
62
|
+
* explicit estimate, not a count — but it is normative and lives with the part logic so a
|
|
63
|
+
* budgeting helper (e.g. `agents.estimateTokens`) cannot silently diverge from it. */
|
|
64
|
+
export declare const PART_BYTES_PER_TOKEN = 750;
|
|
65
|
+
export declare const MIN_PART_TOKENS = 85;
|
|
66
|
+
export declare function partTokens(bytes: number): number;
|
|
67
|
+
/**
|
|
68
|
+
* How a part appears in a log line or a §9 event: `{type, mimeType, bytes}` — never `data`.
|
|
69
|
+
* Same rule as never-log-headers: part payloads are user content.
|
|
70
|
+
*/
|
|
71
|
+
export declare function describePart(part: ContentPart): Record<string, unknown>;
|
|
72
|
+
/** A one-line, payload-free rendering of a part — for `output` text and placeholders. */
|
|
73
|
+
export declare function summarizePart(part: ContentPart): string;
|
|
74
|
+
/** Validate the §1B invariant: exactly one of `data` / `url`. */
|
|
75
|
+
export declare function validatePart(part: ContentPart, maxPartBytes?: number): ContentPart;
|
|
76
|
+
/** True when a value looks like a `ContentPart` (as opposed to a provider-native block). */
|
|
77
|
+
export declare function isContentPart(v: unknown): v is ContentPart;
|
|
78
|
+
/** A text part. */
|
|
79
|
+
export declare function text(value: string): ContentPart;
|
|
80
|
+
/** Options for {@link attach}. */
|
|
81
|
+
export interface AttachOptions {
|
|
82
|
+
/** Overrides the extension table; **required** for raw bytes and for an unlisted extension. */
|
|
83
|
+
mimeType?: string;
|
|
84
|
+
/** Filename carried on a `file` part (OpenAI's `file.filename`). Defaults to the basename. */
|
|
85
|
+
name?: string;
|
|
86
|
+
/** Reject a payload over this many **decoded** bytes (never the +33% base64 string). */
|
|
87
|
+
maxPartBytes?: number;
|
|
88
|
+
}
|
|
89
|
+
/** Everything the edge constructor accepts (§1B): a path / `data:` / `https:` string, native
|
|
90
|
+
* bytes in any of their JS spellings, or the host's own file object. Whatever comes in, only
|
|
91
|
+
* `mimeType` + base64 `data` (or a `url`) lands in the part — accept broadly, store narrowly. */
|
|
92
|
+
export type AttachSource = string | Uint8Array | ArrayBuffer | ArrayBufferView | Blob;
|
|
93
|
+
/**
|
|
94
|
+
* The edge constructor: hand it whatever you actually have and get a path-free part.
|
|
95
|
+
*
|
|
96
|
+
* | given | becomes |
|
|
97
|
+
* |---|---|
|
|
98
|
+
* | a filesystem path | bytes read now, base64d now; mime from the fixed extension table |
|
|
99
|
+
* | native bytes (`Uint8Array`/`Buffer`/`ArrayBuffer`/any `ArrayBufferView`) | base64d now; `mimeType` required |
|
|
100
|
+
* | a `Blob` / `File` | read to bytes **now**; mime from `blob.type`, else the extension table via `File.name` |
|
|
101
|
+
* | `data:<mime>;base64,<b64>` | parsed into `{mimeType, data}` — never stored as a url |
|
|
102
|
+
* | an `http(s):` URL | kept as `url` |
|
|
103
|
+
*/
|
|
104
|
+
export declare function attach(source: AttachSource, opts?: AttachOptions): Promise<ContentPart>;
|
|
105
|
+
/**
|
|
106
|
+
* Build a part from a `Blob` or `File`, consuming it **eagerly** — a part holding a half-read
|
|
107
|
+
* stream would not survive the transcript boundary any better than a path does (§1B).
|
|
108
|
+
*
|
|
109
|
+
* Mime comes from `blob.type` when it is non-empty, else from the fixed extension table applied
|
|
110
|
+
* to `File.name`; an explicit `opts.mimeType` beats both. A `File`'s name is carried onto a
|
|
111
|
+
* `file` part unless `opts.name` overrides it.
|
|
112
|
+
*/
|
|
113
|
+
export declare function fromBlob(blob: Blob, opts?: AttachOptions): Promise<ContentPart>;
|
|
114
|
+
/** Build a part from in-memory bytes. `mimeType` is required — bytes carry no extension. */
|
|
115
|
+
export declare function fromBytes(bytes: Uint8Array | ArrayBuffer | ArrayBufferView, mimeType: string, opts?: AttachOptions): ContentPart;
|
|
116
|
+
/** Read a file now and base64 it now, so the part never carries the path (§1B). */
|
|
117
|
+
export declare function fromPath(p: string, opts?: AttachOptions): Promise<ContentPart>;
|
|
118
|
+
/** Parse `data:<mime>;base64,<b64>` into `{mimeType, data}`, so two spellings of the same
|
|
119
|
+
* bytes cannot diverge downstream. */
|
|
120
|
+
export declare function fromDataUrl(url: string, opts?: AttachOptions): ContentPart;
|
|
121
|
+
/** Keep an `http(s):` URL as a url part; mime comes from the table or an explicit override. */
|
|
122
|
+
export declare function fromUrl(url: string, opts?: AttachOptions): ContentPart;
|
|
123
|
+
/**
|
|
124
|
+
* Encode one part into its provider block, or `undefined` when the style defines no shape
|
|
125
|
+
* for it. Refusals are explicit, never a fall-through: `openai × file+url` (Chat Completions
|
|
126
|
+
* has no URL form for a file) and `anthropic × audio` (the provider defines no audio block).
|
|
127
|
+
*/
|
|
128
|
+
export declare function encodePart(part: ContentPart, style: PartStyle): Record<string, any> | undefined;
|
|
129
|
+
/** Context for {@link encodeParts}: which wire, who asked for the part, and the override. */
|
|
130
|
+
export interface EncodeOptions {
|
|
131
|
+
style: PartStyle;
|
|
132
|
+
provenance: PartProvenance;
|
|
133
|
+
onUnsupportedPart?: UnsupportedPartMode;
|
|
134
|
+
maxPartBytes?: number;
|
|
135
|
+
}
|
|
136
|
+
/** Test seam: forget which unsupported (style, type) pairs have already been warned about. */
|
|
137
|
+
export declare function resetUnsupportedWarnings(): void;
|
|
138
|
+
/** The text a degraded (non-failing) unsupported part leaves behind — never silence. */
|
|
139
|
+
export declare function unsupportedPlaceholder(part: ContentPart): string;
|
|
140
|
+
/**
|
|
141
|
+
* Encode a list of parts for one style, applying the §8A rules:
|
|
142
|
+
* an **attached** part the style cannot represent raises before any HTTP call; a **derived**
|
|
143
|
+
* part degrades to a text placeholder and warns once. `onUnsupportedPart` overrides both.
|
|
144
|
+
* Every emitted block is asserted against the style's allowlist.
|
|
145
|
+
*
|
|
146
|
+
* `maxPartBytes` (§1B) is enforced here too, at assembly, over EVERY part regardless of
|
|
147
|
+
* provenance — a part that arrived from an MCP server never passed through an edge
|
|
148
|
+
* constructor, so a limit it can walk around is not a limit. Going over follows the exact
|
|
149
|
+
* same provenance split as an unsupported block: an attached part errors, a derived one
|
|
150
|
+
* degrades to the placeholder so a remote server still cannot fail the run.
|
|
151
|
+
*/
|
|
152
|
+
export declare function encodeParts(parts: readonly ContentPart[], opts: EncodeOptions): Record<string, any>[];
|
|
153
|
+
/**
|
|
154
|
+
* Read an inbound OpenAI-shaped content block back into a `ContentPart` (§11). Accepts both
|
|
155
|
+
* a `ContentPart` written literally and the provider-native block the same part encodes to,
|
|
156
|
+
* so a caller's own OpenAI messages translate as faithfully as ours do.
|
|
157
|
+
*/
|
|
158
|
+
export declare function inboundPart(block: unknown): ContentPart | undefined;
|