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/src/wire.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical transcript → provider wire messages (SPEC.md §8A).
|
|
3
|
+
*
|
|
4
|
+
* The loop's `messages` array is the **canonical transcript**: a user turn carrying parts
|
|
5
|
+
* holds `ContentPart[]`, and a tool turn carrying parts holds them under a `parts` key.
|
|
6
|
+
* Neither shape is a wire shape. This module is the single place that turns the canonical
|
|
7
|
+
* transcript into what each style actually accepts, so the four loop sites (run/stream ×
|
|
8
|
+
* openai/anthropic) only ever have to *record* parts, never encode them.
|
|
9
|
+
*
|
|
10
|
+
* It is also where the **tool-result relocation rule** lives. Anthropic defines blocks
|
|
11
|
+
* inside `tool_result.content`, so its parts are emitted natively, keyed to their
|
|
12
|
+
* `tool_use_id`. OpenAI's `tool` message rejects an image outright (a hard 400 — "Image URLs
|
|
13
|
+
* are only allowed for messages with role 'user'"), so there every non-text part from every
|
|
14
|
+
* tool result answering one assistant turn is relocated, in tool-call order, into ONE
|
|
15
|
+
* synthetic `user` message emitted immediately after the last tool message. That synthetic
|
|
16
|
+
* message is an adapter artifact: it is built here, on the way out, and never written back
|
|
17
|
+
* to the transcript, the `ConversationStore` or `translate` output — which is what keeps a
|
|
18
|
+
* mid-conversation provider switch free of OpenAI-shaped residue.
|
|
19
|
+
*/
|
|
20
|
+
import type { ContentPart, PartStyle, UnsupportedPartMode } from "./content.js"
|
|
21
|
+
import { encodeParts, isContentPart, validatePart } from "./content.js"
|
|
22
|
+
|
|
23
|
+
/** Shared shaping options for a wire build. */
|
|
24
|
+
export interface WireOptions {
|
|
25
|
+
onUnsupportedPart?: UnsupportedPartMode
|
|
26
|
+
maxPartBytes?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The text preceding each relocated part, so the model can attribute it to its call. */
|
|
30
|
+
export const relocationHeader = (name: string, id: string) => `Output of tool ${name} (${id}):`
|
|
31
|
+
|
|
32
|
+
/** Split parts into the text ones (which ride along in place) and the rest. */
|
|
33
|
+
function split(parts: readonly ContentPart[]): { texts: ContentPart[]; others: ContentPart[] } {
|
|
34
|
+
const texts: ContentPart[] = []
|
|
35
|
+
const others: ContentPart[] = []
|
|
36
|
+
for (const p of parts) (p.type === "text" ? texts : others).push(p)
|
|
37
|
+
return { texts, others }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function partsOf(m: any): ContentPart[] | undefined {
|
|
41
|
+
const p = m?.parts
|
|
42
|
+
return Array.isArray(p) && p.length ? (p as ContentPart[]) : undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** True when a message's `content` is a canonical `ContentPart[]` rather than a wire array. */
|
|
46
|
+
function isPartArray(content: unknown): content is ContentPart[] {
|
|
47
|
+
return Array.isArray(content) && content.length > 0 && content.every(isContentPart)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the OpenAI-style wire messages. Tool messages carry `output` plus text parts only;
|
|
52
|
+
* their non-text parts are collected and flushed as one synthetic user message the moment
|
|
53
|
+
* the run of tool messages ends.
|
|
54
|
+
*/
|
|
55
|
+
export function toOpenAIWire(messages: any[], opts: WireOptions = {}): any[] {
|
|
56
|
+
const out: any[] = []
|
|
57
|
+
let relocated: { name: string; id: string; parts: ContentPart[] }[] = []
|
|
58
|
+
|
|
59
|
+
const flush = () => {
|
|
60
|
+
if (!relocated.length) return
|
|
61
|
+
const blocks: any[] = []
|
|
62
|
+
for (const r of relocated) {
|
|
63
|
+
blocks.push({ type: "text", text: relocationHeader(r.name, r.id) })
|
|
64
|
+
blocks.push(...encodeParts(r.parts, { style: "openai", provenance: "derived", ...opts }))
|
|
65
|
+
}
|
|
66
|
+
relocated = []
|
|
67
|
+
out.push({ role: "user", content: blocks })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const m of messages ?? []) {
|
|
71
|
+
if (m?.role === "tool") {
|
|
72
|
+
const parts = partsOf(m)
|
|
73
|
+
const { parts: _drop, ...rest } = m ?? {}
|
|
74
|
+
if (!parts) {
|
|
75
|
+
out.push(m)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const { texts, others } = split(parts)
|
|
79
|
+
const msg: any = { ...rest }
|
|
80
|
+
if (texts.length) {
|
|
81
|
+
msg.content = [{ type: "text", text: String(rest.content ?? "") }, ...encodeParts(texts, { style: "openai", provenance: "derived", ...opts })]
|
|
82
|
+
}
|
|
83
|
+
out.push(msg)
|
|
84
|
+
if (others.length) relocated.push({ name: String(rest.name ?? ""), id: String(rest.tool_call_id ?? ""), parts: others })
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
flush()
|
|
88
|
+
if (isPartArray(m?.content)) {
|
|
89
|
+
out.push({ ...m, content: encodeParts(m.content, { style: "openai", provenance: "attached", ...opts }) })
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
out.push(m)
|
|
93
|
+
}
|
|
94
|
+
flush()
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the Anthropic-style wire messages. Nothing is relocated: a `tool_result` block that
|
|
100
|
+
* carries parts gets them natively inside its own `content`, keyed to its `tool_use_id`.
|
|
101
|
+
*/
|
|
102
|
+
export function toAnthropicWire(messages: any[], opts: WireOptions = {}): any[] {
|
|
103
|
+
return (messages ?? []).map((m) => {
|
|
104
|
+
if (isPartArray(m?.content)) {
|
|
105
|
+
return { ...m, content: encodeParts(m.content, { style: "anthropic", provenance: "attached", ...opts }) }
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(m?.content)) return m
|
|
108
|
+
let touched = false
|
|
109
|
+
const content = m.content.map((block: any) => {
|
|
110
|
+
if (block?.type !== "tool_result") return block
|
|
111
|
+
const parts = partsOf(block)
|
|
112
|
+
if (!parts) return block
|
|
113
|
+
touched = true
|
|
114
|
+
const { parts: _drop, ...rest } = block
|
|
115
|
+
return {
|
|
116
|
+
...rest,
|
|
117
|
+
content: [
|
|
118
|
+
{ type: "text", text: String(rest.content ?? "") },
|
|
119
|
+
...encodeParts(parts, { style: "anthropic", provenance: "derived", ...opts }),
|
|
120
|
+
],
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
return touched ? { ...m, content } : m
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Validate every attached part before assembly, so `maxPartBytes` cannot be bypassed. */
|
|
128
|
+
export function checkPromptParts(prompt: unknown, opts: WireOptions): void {
|
|
129
|
+
if (!Array.isArray(prompt)) return
|
|
130
|
+
for (const p of prompt) validatePart(p as ContentPart, opts.maxPartBytes)
|
|
131
|
+
}
|