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/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/5xx/network). Default 2. */
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 (429/5xx/network). */
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: string, ctx: {
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: string, ctx: {
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: string, ctx: {
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 429/5xx/network, honoring Retry-After; aborts via signal.
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: string): Promise<RunResult>;
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 429/5xx/network, honoring Retry-After; aborts via signal.
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 = RETRYABLE.has(res.status);
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({ role: "tool", tool_call_id: s.call.id, content: s.result.output });
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({ type: "tool_result", tool_use_id: s.use.id, content: s.result.output, is_error: s.result.isError });
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({ role: "tool", tool_call_id: c.id, content: result.output });
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({ role: "tool", tool_call_id: c.id, content: result.output });
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({ type: "tool_result", tool_use_id: u.id, content: result.output, is_error: result.isError });
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({ type: "tool_result", tool_use_id: u.id, content: result.output, is_error: result.isError });
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;