toolnexus 0.15.0 → 0.17.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 +0 -14
- package/dist/agents/runtime.d.ts +4 -3
- package/dist/agents/runtime.js +8 -17
- package/dist/builtin.js +24 -3
- package/dist/client.d.ts +59 -4
- package/dist/client.js +113 -8
- package/dist/content.d.ts +158 -0
- package/dist/content.js +379 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp.d.ts +9 -0
- package/dist/mcp.js +59 -4
- package/dist/mcpserve.js +27 -1
- 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 +2 -2
- package/src/agents/agent.ts +2 -1
- package/src/agents/compaction.ts +39 -1
- package/src/agents/loop.ts +5 -4
- package/src/agents/runtime.ts +13 -5
- package/src/builtin.ts +23 -3
- package/src/client.ts +182 -16
- package/src/content.ts +448 -0
- package/src/index.ts +2 -0
- package/src/mcp.ts +55 -4
- package/src/mcpserve.ts +26 -3
- package/src/translate.ts +25 -1
- package/src/types.ts +9 -0
- package/src/wire.ts +131 -0
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pendingOf } from "./types.js";
|
|
2
|
+
import { toOpenAIWire, toAnthropicWire, checkPromptParts } from "./wire.js";
|
|
2
3
|
import { finishReasonFor, argsString, openAIMessagesToAnthropic, openAIToolsToAnthropic, openAIToolChoiceToAnthropic, hasSystemMessage, } from "./translate.js";
|
|
3
4
|
/** Default conversation provider — keeps transcripts in memory for the client's lifetime. */
|
|
4
5
|
export class InMemoryConversationStore {
|
|
@@ -207,7 +208,7 @@ export class Client {
|
|
|
207
208
|
/** §8 Gap 5. OpenAI-style body; omits tools/tool_choice when the tool list is empty. Key order
|
|
208
209
|
* matches the pre-change body so a no-options call is byte-identical. */
|
|
209
210
|
openaiBody(messages, tools, stream) {
|
|
210
|
-
const b = { model: this.opts.model, messages };
|
|
211
|
+
const b = { model: this.opts.model, messages: toOpenAIWire(messages, this.wireOpts()) };
|
|
211
212
|
if (tools.length) {
|
|
212
213
|
b.tools = tools;
|
|
213
214
|
b.tool_choice = "auto";
|
|
@@ -220,13 +221,17 @@ export class Client {
|
|
|
220
221
|
}
|
|
221
222
|
/** §8 Gap 5. Anthropic-style body; omits tools when the tool list is empty. */
|
|
222
223
|
anthropicBody(system, messages, tools, stream) {
|
|
223
|
-
const b = { model: this.opts.model, max_tokens: 4096, system, messages };
|
|
224
|
+
const b = { model: this.opts.model, max_tokens: 4096, system, messages: toAnthropicWire(messages, this.wireOpts()) };
|
|
224
225
|
if (tools.length)
|
|
225
226
|
b.tools = tools;
|
|
226
227
|
if (stream)
|
|
227
228
|
b.stream = true;
|
|
228
229
|
return this.finalizeBody(b);
|
|
229
230
|
}
|
|
231
|
+
/** The §8A/§1B shaping options every wire build shares. */
|
|
232
|
+
wireOpts() {
|
|
233
|
+
return { onUnsupportedPart: this.opts.onUnsupportedPart, maxPartBytes: this.opts.maxPartBytes };
|
|
234
|
+
}
|
|
230
235
|
/** Prometheus text exposition of cumulative metrics (§8). Always valid, empty-but-valid before activity. */
|
|
231
236
|
metrics() {
|
|
232
237
|
return this.registry.render();
|
|
@@ -247,6 +252,7 @@ export class Client {
|
|
|
247
252
|
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
253
|
}
|
|
249
254
|
async run(prompt, ctx) {
|
|
255
|
+
checkPromptParts(prompt, this.wireOpts());
|
|
250
256
|
return this.opts.style === "anthropic"
|
|
251
257
|
? this.runAnthropic(prompt, ctx.toolkit, ctx.signal, ctx.history)
|
|
252
258
|
: this.runOpenAI(prompt, ctx.toolkit, ctx.signal, ctx.history);
|
|
@@ -433,6 +439,7 @@ export class Client {
|
|
|
433
439
|
* streaming, and saved back to the ConversationStore on the terminal `done` event. No `id` ⇒ stateless.
|
|
434
440
|
*/
|
|
435
441
|
async *stream(prompt, ctx) {
|
|
442
|
+
checkPromptParts(prompt, this.wireOpts());
|
|
436
443
|
const history = ctx.id ? (await this.store.get(ctx.id)) ?? [] : undefined;
|
|
437
444
|
const gen = this.opts.style === "anthropic"
|
|
438
445
|
? this.streamAnthropic(prompt, ctx.toolkit, ctx.signal, history)
|
|
@@ -602,7 +609,7 @@ export class Client {
|
|
|
602
609
|
// on resume). Mirrors the streaming path.
|
|
603
610
|
for (const s of settled) {
|
|
604
611
|
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(
|
|
612
|
+
messages.push(toolMessage(s.call.id, s.call.function.name, s.result));
|
|
606
613
|
if (s.halted)
|
|
607
614
|
return this.pendingRun(runStart, s.halted, messages, toolCalls, turns, usage);
|
|
608
615
|
}
|
|
@@ -710,7 +717,7 @@ export class Client {
|
|
|
710
717
|
let halted;
|
|
711
718
|
for (const s of settled) {
|
|
712
719
|
toolCalls.push({ name: s.use.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata });
|
|
713
|
-
content.push(
|
|
720
|
+
content.push(toolResultBlock(s.use.id, s.result));
|
|
714
721
|
if (s.halted) {
|
|
715
722
|
halted = s.halted;
|
|
716
723
|
break;
|
|
@@ -820,13 +827,13 @@ export class Client {
|
|
|
820
827
|
result = r.result;
|
|
821
828
|
if (r.halted) {
|
|
822
829
|
toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
823
|
-
messages.push(
|
|
830
|
+
messages.push(toolMessage(c.id, c.name, result));
|
|
824
831
|
yield { type: "done", result: this.pendingRun(runStart, r.halted, messages, toolCalls, turns, usage) };
|
|
825
832
|
return;
|
|
826
833
|
}
|
|
827
834
|
}
|
|
828
835
|
toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
829
|
-
messages.push(
|
|
836
|
+
messages.push(toolMessage(c.id, c.name, result));
|
|
830
837
|
yield { type: "tool_result", id: c.id, name: c.name, output: result.output, isError: result.isError };
|
|
831
838
|
}
|
|
832
839
|
}
|
|
@@ -927,14 +934,14 @@ export class Client {
|
|
|
927
934
|
result = r.result;
|
|
928
935
|
if (r.halted) {
|
|
929
936
|
toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
930
|
-
results.push(
|
|
937
|
+
results.push(toolResultBlock(u.id, result));
|
|
931
938
|
messages.push({ role: "user", content: results });
|
|
932
939
|
yield { type: "done", result: this.pendingRun(runStart, r.halted, messages, toolCalls, turns, usage) };
|
|
933
940
|
return;
|
|
934
941
|
}
|
|
935
942
|
}
|
|
936
943
|
toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
937
|
-
results.push(
|
|
944
|
+
results.push(toolResultBlock(u.id, result));
|
|
938
945
|
yield { type: "tool_result", id: u.id, name: u.name, output: result.output, isError: result.isError };
|
|
939
946
|
}
|
|
940
947
|
messages.push({ role: "user", content: results });
|
|
@@ -950,6 +957,84 @@ export class Client {
|
|
|
950
957
|
export function createClient(opts) {
|
|
951
958
|
return new Client(opts);
|
|
952
959
|
}
|
|
960
|
+
/** The sentinel base URL. Never dialled — the transport below answers every request
|
|
961
|
+
* before the network is reached — but the client builds a URL string internally, so
|
|
962
|
+
* it must be syntactically valid. `.invalid` is reserved by RFC 2606 precisely so a
|
|
963
|
+
* name can never resolve. */
|
|
964
|
+
const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1";
|
|
965
|
+
/**
|
|
966
|
+
* A client backed by a model running IN THIS PROCESS — no server, no socket, and no
|
|
967
|
+
* HTTP types to construct.
|
|
968
|
+
*
|
|
969
|
+
* This is a second constructor, not a second seam: it builds on the same injectable
|
|
970
|
+
* transport that `fetch`/`httpClient` uses, so the tool-calling loop, MCP servers,
|
|
971
|
+
* skills, sub-agents, hooks, metrics and the completion gate all behave identically.
|
|
972
|
+
*
|
|
973
|
+
* Streaming is refused rather than faked — see the thrown error.
|
|
974
|
+
*/
|
|
975
|
+
export function createInProcessClient(opts) {
|
|
976
|
+
const { generate, ...rest } = opts;
|
|
977
|
+
if (typeof generate !== "function") {
|
|
978
|
+
throw new Error("toolnexus: createInProcessClient requires a `generate` function");
|
|
979
|
+
}
|
|
980
|
+
const inProcessFetch = async (_url, init) => {
|
|
981
|
+
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
982
|
+
// A generate() returns a whole answer, so a stream would be one chunk pretending
|
|
983
|
+
// to be many. Content and delta-count assertions cannot tell that from a real
|
|
984
|
+
// stream, so refuse instead of degrading silently.
|
|
985
|
+
if (body.stream) {
|
|
986
|
+
throw new Error("toolnexus: createInProcessClient does not support streaming — `generate` returns a " +
|
|
987
|
+
"complete answer. Use the non-streaming run, or supply a `fetch`/transport that streams.");
|
|
988
|
+
}
|
|
989
|
+
const answer = await generate({
|
|
990
|
+
messages: body.messages ?? [],
|
|
991
|
+
tools: body.tools,
|
|
992
|
+
model: body.model,
|
|
993
|
+
body,
|
|
994
|
+
});
|
|
995
|
+
const message = { role: "assistant" };
|
|
996
|
+
if (answer.toolCalls && answer.toolCalls.length > 0) {
|
|
997
|
+
message.tool_calls = answer.toolCalls.map((c, i) => ({
|
|
998
|
+
id: c.id ?? `call_${i}`,
|
|
999
|
+
type: "function",
|
|
1000
|
+
function: {
|
|
1001
|
+
name: c.name,
|
|
1002
|
+
// A string is already encoded; anything else is encoded here so a model
|
|
1003
|
+
// author never has to think about the wire.
|
|
1004
|
+
arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {}),
|
|
1005
|
+
},
|
|
1006
|
+
}));
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
message.content = answer.content ?? "";
|
|
1010
|
+
}
|
|
1011
|
+
const u = answer.usage ?? {};
|
|
1012
|
+
const prompt = u.promptTokens ?? 0;
|
|
1013
|
+
const completion = u.completionTokens ?? 0;
|
|
1014
|
+
return new Response(JSON.stringify({
|
|
1015
|
+
choices: [{ index: 0, message, finish_reason: message.tool_calls ? "tool_calls" : "stop" }],
|
|
1016
|
+
usage: {
|
|
1017
|
+
prompt_tokens: prompt,
|
|
1018
|
+
completion_tokens: completion,
|
|
1019
|
+
total_tokens: u.totalTokens ?? prompt + completion,
|
|
1020
|
+
},
|
|
1021
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
1022
|
+
};
|
|
1023
|
+
return new Client({
|
|
1024
|
+
// Retries default to ZERO here, unlike a network client. There is no wire, so
|
|
1025
|
+
// there is no transient failure to ride out: whatever `generate` throws will
|
|
1026
|
+
// throw again. Retrying it only buys backoff before the user sees their own bug —
|
|
1027
|
+
// measured at 3.7s for the streaming refusal before this default. A host whose
|
|
1028
|
+
// model really is flaky (a GPU that OOMs, say) can still set `retries`.
|
|
1029
|
+
retries: 0,
|
|
1030
|
+
...rest,
|
|
1031
|
+
baseUrl: IN_PROCESS_BASE_URL,
|
|
1032
|
+
style: "openai",
|
|
1033
|
+
// An in-process model has no endpoint to authenticate to, so the host must never need a key — but the client resolves one from the environment and fails when it finds none. A sentinel keeps that resolution from ever running. Caught by CI, which has no OPENROUTER_API_KEY; every local run passed because a developer shell has one.
|
|
1034
|
+
apiKey: "in-process",
|
|
1035
|
+
fetch: inProcessFetch,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
953
1038
|
/** Stateful multi-turn conversation: each send() continues the same transcript (memory). */
|
|
954
1039
|
export class Conversation {
|
|
955
1040
|
client;
|
|
@@ -1008,6 +1093,26 @@ function safeJson(s) {
|
|
|
1008
1093
|
return {};
|
|
1009
1094
|
}
|
|
1010
1095
|
}
|
|
1096
|
+
/**
|
|
1097
|
+
* A canonical `tool` turn. With no parts this is byte-identical to the pre-0.17 message;
|
|
1098
|
+
* with parts it also carries them (and the tool `name` the §8A relocation header needs) —
|
|
1099
|
+
* both consumed and stripped by the wire build, so neither reaches OpenAI.
|
|
1100
|
+
*/
|
|
1101
|
+
function toolMessage(id, name, result) {
|
|
1102
|
+
const m = { role: "tool", tool_call_id: id, content: result.output };
|
|
1103
|
+
if (result.parts?.length) {
|
|
1104
|
+
m.name = name;
|
|
1105
|
+
m.parts = result.parts;
|
|
1106
|
+
}
|
|
1107
|
+
return m;
|
|
1108
|
+
}
|
|
1109
|
+
/** A canonical Anthropic `tool_result` block; parts ride along and are emitted natively. */
|
|
1110
|
+
function toolResultBlock(id, result) {
|
|
1111
|
+
const b = { type: "tool_result", tool_use_id: id, content: result.output, is_error: result.isError };
|
|
1112
|
+
if (result.parts?.length)
|
|
1113
|
+
b.parts = result.parts;
|
|
1114
|
+
return b;
|
|
1115
|
+
}
|
|
1011
1116
|
function lastText(messages) {
|
|
1012
1117
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1013
1118
|
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;
|