toolnexus 0.28.0 → 0.29.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/wire.d.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  * to the transcript, the `ConversationStore` or `translate` output — which is what keeps a
18
18
  * mid-conversation provider switch free of OpenAI-shaped residue.
19
19
  */
20
- import type { UnsupportedPartMode } from "./content.js";
20
+ import type { ContentPart, UnsupportedPartMode } from "./content.js";
21
21
  /** Shared shaping options for a wire build. */
22
22
  export interface WireOptions {
23
23
  onUnsupportedPart?: UnsupportedPartMode;
@@ -36,5 +36,15 @@ export declare function toOpenAIWire(messages: any[], opts?: WireOptions): any[]
36
36
  * carries parts gets them natively inside its own `content`, keyed to its `tool_use_id`.
37
37
  */
38
38
  export declare function toAnthropicWire(messages: any[], opts?: WireOptions): any[];
39
+ /**
40
+ * The parts of a tool result that reach the model (SPEC §7G.6). A media tool result
41
+ * (`metadata.source === "media"`) contributes only its **image** parts; its video (`file`) and
42
+ * `audio` parts stay in `toolCalls[].parts` / `RunResult.files` and the model sees just the
43
+ * output line. Every other tool result keeps all its parts.
44
+ */
45
+ export declare function modelParts(result: {
46
+ parts?: ContentPart[];
47
+ metadata?: Record<string, unknown>;
48
+ }): ContentPart[];
39
49
  /** Validate every attached part before assembly, so `maxPartBytes` cannot be bypassed. */
40
50
  export declare function checkPromptParts(prompt: unknown, opts: WireOptions): void;
package/dist/wire.js CHANGED
@@ -41,7 +41,7 @@ export function toOpenAIWire(messages, opts = {}) {
41
41
  const parts = partsOf(m);
42
42
  const { parts: _drop, ...rest } = m ?? {};
43
43
  if (!parts) {
44
- out.push(m);
44
+ out.push(m && "parts" in m ? rest : m);
45
45
  continue;
46
46
  }
47
47
  const { texts, others } = split(parts);
@@ -80,8 +80,13 @@ export function toAnthropicWire(messages, opts = {}) {
80
80
  if (block?.type !== "tool_result")
81
81
  return block;
82
82
  const parts = partsOf(block);
83
- if (!parts)
84
- return block;
83
+ if (!parts) {
84
+ if (!("parts" in block))
85
+ return block;
86
+ touched = true;
87
+ const { parts: _drop, ...rest } = block;
88
+ return rest;
89
+ }
85
90
  touched = true;
86
91
  const { parts: _drop, ...rest } = block;
87
92
  return {
@@ -95,6 +100,18 @@ export function toAnthropicWire(messages, opts = {}) {
95
100
  return touched ? { ...m, content } : m;
96
101
  });
97
102
  }
103
+ /**
104
+ * The parts of a tool result that reach the model (SPEC §7G.6). A media tool result
105
+ * (`metadata.source === "media"`) contributes only its **image** parts; its video (`file`) and
106
+ * `audio` parts stay in `toolCalls[].parts` / `RunResult.files` and the model sees just the
107
+ * output line. Every other tool result keeps all its parts.
108
+ */
109
+ export function modelParts(result) {
110
+ const parts = result.parts ?? [];
111
+ if (result.metadata?.source !== "media")
112
+ return parts;
113
+ return parts.filter((p) => p.type === "image");
114
+ }
98
115
  /** Validate every attached part before assembly, so `maxPartBytes` cannot be bypassed. */
99
116
  export function checkPromptParts(prompt, opts) {
100
117
  if (!Array.isArray(prompt))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolnexus",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Dynamic MCP servers + agent skills as uniform tools for any LLM (opencode-style).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/client.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * ("openai" | "anthropic"), and it runs the tool-calling agent loop against a
4
4
  * Toolkit. See ../../SPEC.md §8.
5
5
  */
6
+ import { mediaPartPath } from "./media.js"
6
7
  import type { Toolkit } from "./toolkit.js"
7
8
  import type { Tool, ToolHints, ToolResult, Request, Answer } from "./types.js"
8
9
  import { pendingOf } from "./types.js"
@@ -12,7 +13,7 @@ import { buildToolPlan, toolSearch, unwrapCallTool, type ToolPlan, type ToolTier
12
13
  import { isRetryableStatus, retryAfterMs } from "./retry.js"
13
14
  import { isConcurrencySafe, resolveMaxToolConcurrency, scheduleCalls } from "./toolconcurrency.js"
14
15
  import type { ContentPart, PromptInput, UnsupportedPartMode } from "./content.js"
15
- import { toOpenAIWire, toAnthropicWire, checkPromptParts, type WireOptions } from "./wire.js"
16
+ import { toOpenAIWire, toAnthropicWire, checkPromptParts, modelParts, type WireOptions } from "./wire.js"
16
17
  import type { TranslateRequest, TranslateResult, TranslatedToolCall } from "./translate.js"
17
18
  import {
18
19
  finishReasonFor,
@@ -394,18 +395,20 @@ export interface ToolCallRecord {
394
395
  }
395
396
 
396
397
  /** §8 RunResult.files entry: a non-text part a tool call produced this run. */
397
- export type RunFile = { type: "image" | "file" | "audio"; mimeType: string; url?: string; data?: string; name?: string; from: string; callId: string }
398
+ export type RunFile = { type: "image" | "file" | "audio"; mimeType: string; url?: string; data?: string; name?: string; path?: string; from: string; callId: string }
398
399
 
399
400
  /** Model tool-call id of each record, kept off the record itself (§8 shape has no id). */
400
401
  const callIds = new WeakMap<ToolCallRecord, string>()
401
402
 
402
403
  /** RunResult.files: every non-text part of every tool call, in call order (§8). */
403
- function runFiles(toolCalls: ToolCallRecord[]): RunFile[] {
404
+ export function runFiles(toolCalls: ToolCallRecord[]): RunFile[] {
404
405
  const out: RunFile[] = []
405
406
  for (const r of toolCalls) {
406
407
  for (const p of r.parts ?? []) {
407
408
  if (p.type === "text") continue
408
- out.push({ ...p, from: r.name, callId: callIds.get(r) ?? "" })
409
+ // D20: a media file's local path rides on the run file, never on the part.
410
+ const local = mediaPartPath(p)
411
+ out.push({ ...p, ...(local !== undefined ? { path: local } : {}), from: r.name, callId: callIds.get(r) ?? "" })
409
412
  }
410
413
  }
411
414
  return out
@@ -2182,9 +2185,10 @@ function objArgs(v: unknown): Record<string, unknown> {
2182
2185
  */
2183
2186
  function toolMessage(id: string | undefined, name: string, result: ToolResult): any {
2184
2187
  const m: any = { role: "tool", tool_call_id: id, content: result.output }
2185
- if (result.parts?.length) {
2188
+ const parts = modelParts(result)
2189
+ if (parts.length) {
2186
2190
  m.name = name
2187
- m.parts = result.parts
2191
+ m.parts = parts
2188
2192
  }
2189
2193
  return m
2190
2194
  }
@@ -2192,7 +2196,8 @@ function toolMessage(id: string | undefined, name: string, result: ToolResult):
2192
2196
  /** A canonical Anthropic `tool_result` block; parts ride along and are emitted natively. */
2193
2197
  function toolResultBlock(id: string | undefined, result: ToolResult): any {
2194
2198
  const b: any = { type: "tool_result", tool_use_id: id, content: result.output, is_error: result.isError }
2195
- if (result.parts?.length) b.parts = result.parts
2199
+ const parts = modelParts(result)
2200
+ if (parts.length) b.parts = parts
2196
2201
  return b
2197
2202
  }
2198
2203