toolnexus 0.9.5 → 0.12.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 +31 -1
- package/dist/agents/agent.d.ts +66 -0
- package/dist/agents/agent.js +101 -0
- package/dist/agents/compaction.d.ts +77 -0
- package/dist/agents/compaction.js +96 -0
- package/dist/agents/home.d.ts +90 -0
- package/dist/agents/home.js +203 -0
- package/dist/agents/index.d.ts +10 -0
- package/dist/agents/index.js +10 -0
- package/dist/agents/runtime.d.ts +372 -0
- package/dist/agents/runtime.js +737 -0
- package/dist/client.d.ts +31 -3
- package/dist/client.js +161 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/translate.d.ts +107 -0
- package/dist/translate.js +195 -0
- package/package.json +2 -2
- package/src/agents/agent.ts +137 -0
- package/src/agents/compaction.ts +148 -0
- package/src/agents/home.ts +250 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/runtime.ts +909 -0
- package/src/client.ts +182 -11
- package/src/index.ts +2 -0
- package/src/translate.ts +242 -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 { TranslateRequest, TranslateResult } from "./translate.js";
|
|
8
9
|
export type ClientStyle = "openai" | "anthropic";
|
|
9
10
|
export interface ClientOptions {
|
|
10
11
|
baseUrl: string;
|
|
@@ -184,10 +185,14 @@ export interface RunResult {
|
|
|
184
185
|
usage: Usage;
|
|
185
186
|
/** The model used. */
|
|
186
187
|
model: string;
|
|
187
|
-
/** "done" normally; "pending" iff a tool suspended and no waitFor was configured (§10)
|
|
188
|
-
|
|
188
|
+
/** "done" normally; "pending" iff a tool suspended and no waitFor was configured (§10);
|
|
189
|
+
* "incomplete" iff a limit stopped the run loudly (§7D) — a maxTurns stop with no final
|
|
190
|
+
* text is never a silent "done". */
|
|
191
|
+
status: "done" | "pending" | "incomplete";
|
|
189
192
|
/** The unresolved suspension — present iff status === "pending" (§10). */
|
|
190
193
|
pending?: Request;
|
|
194
|
+
/** The limit that stopped the run (e.g. "maxTurns") — present iff status === "incomplete". */
|
|
195
|
+
limit?: string;
|
|
191
196
|
}
|
|
192
197
|
/** Events yielded by client.stream(). */
|
|
193
198
|
export type StreamEvent = {
|
|
@@ -242,7 +247,8 @@ export declare class Client {
|
|
|
242
247
|
metrics(): string;
|
|
243
248
|
/** Feed a semantic metric event to the built-in registry and the optional onMetric sink. */
|
|
244
249
|
private emit;
|
|
245
|
-
/** Emit the terminal `run` metric event and build the RunResult.
|
|
250
|
+
/** Emit the terminal `run` metric event and build the RunResult. `exhausted` marks a
|
|
251
|
+
* maxTurns loop exit — with no final text that is a LOUD "incomplete", never "done" (§7D). */
|
|
246
252
|
private endRun;
|
|
247
253
|
/** Emit a `run` error metric event (once, on a thrown run). */
|
|
248
254
|
private emitRunError;
|
|
@@ -251,6 +257,28 @@ export declare class Client {
|
|
|
251
257
|
signal?: AbortSignal;
|
|
252
258
|
history?: any[];
|
|
253
259
|
}): Promise<RunResult>;
|
|
260
|
+
/**
|
|
261
|
+
* Single-turn translation (§11): exactly ONE provider call, returned in OpenAI shape.
|
|
262
|
+
* No agent loop, no tool execution, no conversation state — every call is
|
|
263
|
+
* self-contained, so this may be run statelessly and concurrently.
|
|
264
|
+
*
|
|
265
|
+
* The inbound half of the adapters: `toAnthropic`/`toGemini` send declarations out,
|
|
266
|
+
* this reads the provider's tool calls back in. A `toolkit` passed in the request is
|
|
267
|
+
* DECLARED and never executed.
|
|
268
|
+
*
|
|
269
|
+
* Retries/backoff, requestParams merging and the `llm` metric are shared with the loop.
|
|
270
|
+
* `beforeLLM`/`afterLLM` fire once; tool hooks do not, because no tool runs.
|
|
271
|
+
*/
|
|
272
|
+
translate(req: TranslateRequest): Promise<TranslateResult>;
|
|
273
|
+
/** OpenAI-style upstream: near-passthrough. */
|
|
274
|
+
private translateOpenAI;
|
|
275
|
+
/** Anthropic-style upstream: the real translation. */
|
|
276
|
+
private translateAnthropic;
|
|
277
|
+
/**
|
|
278
|
+
* Renders a translate result's tool calls as an OpenAI `tool_calls` array, ready to put
|
|
279
|
+
* on an assistant message. Convenience for assembling a response envelope.
|
|
280
|
+
*/
|
|
281
|
+
static toolCallsJSON(res: TranslateResult): any[];
|
|
254
282
|
/**
|
|
255
283
|
* Stateful ask. With an `id`, the client's `store` remembers the conversation:
|
|
256
284
|
* it loads that id's transcript, runs, saves the updated transcript, and returns
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pendingOf } from "./types.js";
|
|
2
|
+
import { finishReasonFor, argsString, openAIMessagesToAnthropic, openAIToolsToAnthropic, openAIToolChoiceToAnthropic, hasSystemMessage, } from "./translate.js";
|
|
2
3
|
/** Default conversation provider — keeps transcripts in memory for the client's lifetime. */
|
|
3
4
|
export class InMemoryConversationStore {
|
|
4
5
|
map = new Map();
|
|
@@ -213,10 +214,11 @@ export class Client {
|
|
|
213
214
|
this.registry.record(ev);
|
|
214
215
|
this.opts.onMetric?.(ev);
|
|
215
216
|
}
|
|
216
|
-
/** Emit the terminal `run` metric event and build the RunResult.
|
|
217
|
-
|
|
217
|
+
/** Emit the terminal `run` metric event and build the RunResult. `exhausted` marks a
|
|
218
|
+
* maxTurns loop exit — with no final text that is a LOUD "incomplete", never "done" (§7D). */
|
|
219
|
+
endRun(runStart, text, messages, toolCalls, turns, usage, exhausted = false) {
|
|
218
220
|
this.emit({ event: "run", model: this.opts.model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart });
|
|
219
|
-
return this.result(text, messages, toolCalls, turns, usage);
|
|
221
|
+
return this.result(text, messages, toolCalls, turns, usage, exhausted);
|
|
220
222
|
}
|
|
221
223
|
/** Emit a `run` error metric event (once, on a thrown run). */
|
|
222
224
|
emitRunError(runStart, toolCalls, turns, usage, e) {
|
|
@@ -227,6 +229,152 @@ export class Client {
|
|
|
227
229
|
? this.runAnthropic(prompt, ctx.toolkit, ctx.signal, ctx.history)
|
|
228
230
|
: this.runOpenAI(prompt, ctx.toolkit, ctx.signal, ctx.history);
|
|
229
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Single-turn translation (§11): exactly ONE provider call, returned in OpenAI shape.
|
|
234
|
+
* No agent loop, no tool execution, no conversation state — every call is
|
|
235
|
+
* self-contained, so this may be run statelessly and concurrently.
|
|
236
|
+
*
|
|
237
|
+
* The inbound half of the adapters: `toAnthropic`/`toGemini` send declarations out,
|
|
238
|
+
* this reads the provider's tool calls back in. A `toolkit` passed in the request is
|
|
239
|
+
* DECLARED and never executed.
|
|
240
|
+
*
|
|
241
|
+
* Retries/backoff, requestParams merging and the `llm` metric are shared with the loop.
|
|
242
|
+
* `beforeLLM`/`afterLLM` fire once; tool hooks do not, because no tool runs.
|
|
243
|
+
*/
|
|
244
|
+
async translate(req) {
|
|
245
|
+
return this.opts.style === "anthropic" ? this.translateAnthropic(req) : this.translateOpenAI(req);
|
|
246
|
+
}
|
|
247
|
+
/** OpenAI-style upstream: near-passthrough. */
|
|
248
|
+
async translateOpenAI(req) {
|
|
249
|
+
const key = resolveKey(this.opts);
|
|
250
|
+
const declared = [...(req.toolkit ? req.toolkit.toOpenAI() : []), ...(req.tools ?? [])];
|
|
251
|
+
let messages = (req.messages ?? []).slice();
|
|
252
|
+
const sys = req.system ?? this.opts.systemPrompt ?? "";
|
|
253
|
+
if (sys && !hasSystemMessage(messages))
|
|
254
|
+
messages = [{ role: "system", content: sys }, ...messages];
|
|
255
|
+
let tools = declared;
|
|
256
|
+
if (this.opts.hooks?.beforeLLM) {
|
|
257
|
+
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn: 0 });
|
|
258
|
+
if (ov?.messages)
|
|
259
|
+
messages = ov.messages;
|
|
260
|
+
if (ov?.tools)
|
|
261
|
+
tools = ov.tools;
|
|
262
|
+
}
|
|
263
|
+
const body = { model: this.opts.model, messages };
|
|
264
|
+
if (tools.length)
|
|
265
|
+
body.tools = tools;
|
|
266
|
+
if (req.toolChoice !== undefined)
|
|
267
|
+
body.tool_choice = req.toolChoice;
|
|
268
|
+
if (req.maxTokens)
|
|
269
|
+
body.max_tokens = req.maxTokens;
|
|
270
|
+
const signal = this.makeSignal(req.signal);
|
|
271
|
+
{
|
|
272
|
+
const data = await this.llmCallJson(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${key}`, ...this.opts.headers },
|
|
275
|
+
body: JSON.stringify(this.finalizeBody(body)),
|
|
276
|
+
}, signal, "openai");
|
|
277
|
+
if (this.opts.hooks?.afterLLM)
|
|
278
|
+
await this.opts.hooks.afterLLM({ response: data, model: this.opts.model, turn: 0 });
|
|
279
|
+
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
280
|
+
addUsage(usage, data.usage, "openai");
|
|
281
|
+
const choice = data.choices?.[0];
|
|
282
|
+
const toolCalls = (choice?.message?.tool_calls ?? []).map((tc) => ({
|
|
283
|
+
id: String(tc.id ?? ""),
|
|
284
|
+
name: String(tc.function?.name ?? ""),
|
|
285
|
+
arguments: argsString(tc.function?.arguments),
|
|
286
|
+
}));
|
|
287
|
+
return {
|
|
288
|
+
text: choice?.message?.content ?? "",
|
|
289
|
+
toolCalls,
|
|
290
|
+
finishReason: choice?.finish_reason ?? finishReasonFor(toolCalls.length > 0),
|
|
291
|
+
usage,
|
|
292
|
+
model: this.opts.model,
|
|
293
|
+
raw: data,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** Anthropic-style upstream: the real translation. */
|
|
298
|
+
async translateAnthropic(req) {
|
|
299
|
+
const key = resolveKey(this.opts);
|
|
300
|
+
const base = this.opts.baseUrl.replace(/\/$/, "");
|
|
301
|
+
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
|
302
|
+
const converted = openAIMessagesToAnthropic(req.messages ?? []);
|
|
303
|
+
let messages = converted.messages;
|
|
304
|
+
const system = req.system ?? this.opts.systemPrompt ?? converted.system;
|
|
305
|
+
let tools = [
|
|
306
|
+
...(req.toolkit ? req.toolkit.toAnthropic() : []),
|
|
307
|
+
...openAIToolsToAnthropic(req.tools),
|
|
308
|
+
];
|
|
309
|
+
if (this.opts.hooks?.beforeLLM) {
|
|
310
|
+
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn: 0 });
|
|
311
|
+
if (ov?.messages)
|
|
312
|
+
messages = ov.messages;
|
|
313
|
+
if (ov?.tools)
|
|
314
|
+
tools = ov.tools;
|
|
315
|
+
}
|
|
316
|
+
const body = {
|
|
317
|
+
model: this.opts.model,
|
|
318
|
+
max_tokens: req.maxTokens && req.maxTokens > 0 ? req.maxTokens : 4096,
|
|
319
|
+
messages,
|
|
320
|
+
};
|
|
321
|
+
if (system)
|
|
322
|
+
body.system = system;
|
|
323
|
+
if (tools.length)
|
|
324
|
+
body.tools = tools;
|
|
325
|
+
const tc = openAIToolChoiceToAnthropic(req.toolChoice);
|
|
326
|
+
if (tc)
|
|
327
|
+
body.tool_choice = tc;
|
|
328
|
+
const signal = this.makeSignal(req.signal);
|
|
329
|
+
{
|
|
330
|
+
const data = await this.llmCallJson(endpoint, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: {
|
|
333
|
+
"content-type": "application/json",
|
|
334
|
+
"x-api-key": key,
|
|
335
|
+
"anthropic-version": "2023-06-01",
|
|
336
|
+
...this.opts.headers,
|
|
337
|
+
},
|
|
338
|
+
body: JSON.stringify(this.finalizeBody(body)),
|
|
339
|
+
}, signal, "anthropic");
|
|
340
|
+
if (this.opts.hooks?.afterLLM)
|
|
341
|
+
await this.opts.hooks.afterLLM({ response: data, model: this.opts.model, turn: 0 });
|
|
342
|
+
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
343
|
+
addUsage(usage, data.usage, "anthropic");
|
|
344
|
+
const text = [];
|
|
345
|
+
const toolCalls = [];
|
|
346
|
+
for (const b of data.content ?? []) {
|
|
347
|
+
if (b?.type === "text")
|
|
348
|
+
text.push(b.text ?? "");
|
|
349
|
+
else if (b?.type === "tool_use") {
|
|
350
|
+
toolCalls.push({
|
|
351
|
+
id: String(b.id ?? ""),
|
|
352
|
+
name: String(b.name ?? ""),
|
|
353
|
+
arguments: JSON.stringify(b.input ?? {}),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
text: text.join(""),
|
|
359
|
+
toolCalls,
|
|
360
|
+
finishReason: finishReasonFor(toolCalls.length > 0, data.stop_reason),
|
|
361
|
+
usage,
|
|
362
|
+
model: this.opts.model,
|
|
363
|
+
raw: data,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Renders a translate result's tool calls as an OpenAI `tool_calls` array, ready to put
|
|
369
|
+
* on an assistant message. Convenience for assembling a response envelope.
|
|
370
|
+
*/
|
|
371
|
+
static toolCallsJSON(res) {
|
|
372
|
+
return res.toolCalls.map((tc) => ({
|
|
373
|
+
id: tc.id,
|
|
374
|
+
type: "function",
|
|
375
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
230
378
|
/**
|
|
231
379
|
* Stateful ask. With an `id`, the client's `store` remembers the conversation:
|
|
232
380
|
* it loads that id's transcript, runs, saves the updated transcript, and returns
|
|
@@ -436,15 +584,19 @@ export class Client {
|
|
|
436
584
|
return this.pendingRun(runStart, s.halted, messages, toolCalls, turns, usage);
|
|
437
585
|
}
|
|
438
586
|
}
|
|
439
|
-
return this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage);
|
|
587
|
+
return this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage, true);
|
|
440
588
|
}
|
|
441
589
|
catch (e) {
|
|
442
590
|
this.emitRunError(runStart, toolCalls, turns, usage, e);
|
|
443
591
|
throw e;
|
|
444
592
|
}
|
|
445
593
|
}
|
|
446
|
-
result(text, messages, toolCalls, turns, usage) {
|
|
447
|
-
|
|
594
|
+
result(text, messages, toolCalls, turns, usage, exhausted = false) {
|
|
595
|
+
const incomplete = exhausted && text === "";
|
|
596
|
+
const r = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model: this.opts.model, status: incomplete ? "incomplete" : "done" };
|
|
597
|
+
if (incomplete)
|
|
598
|
+
r.limit = "maxTurns";
|
|
599
|
+
return r;
|
|
448
600
|
}
|
|
449
601
|
/** §10: a run halted because a tool suspended and no `waitFor` was configured. */
|
|
450
602
|
pendingRun(runStart, request, messages, toolCalls, turns, usage) {
|
|
@@ -545,7 +697,7 @@ export class Client {
|
|
|
545
697
|
if (halted)
|
|
546
698
|
return this.pendingRun(runStart, halted, messages, toolCalls, turns, usage);
|
|
547
699
|
}
|
|
548
|
-
return this.endRun(runStart, "", messages, toolCalls, turns, usage);
|
|
700
|
+
return this.endRun(runStart, "", messages, toolCalls, turns, usage, true);
|
|
549
701
|
}
|
|
550
702
|
catch (e) {
|
|
551
703
|
this.emitRunError(runStart, toolCalls, turns, usage, e);
|
|
@@ -655,7 +807,7 @@ export class Client {
|
|
|
655
807
|
yield { type: "tool_result", id: c.id, name: c.name, output: result.output, isError: result.isError };
|
|
656
808
|
}
|
|
657
809
|
}
|
|
658
|
-
yield { type: "done", result: this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage) };
|
|
810
|
+
yield { type: "done", result: this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage, true) };
|
|
659
811
|
}
|
|
660
812
|
catch (e) {
|
|
661
813
|
this.emitRunError(runStart, toolCalls, turns, usage, e);
|
|
@@ -764,7 +916,7 @@ export class Client {
|
|
|
764
916
|
}
|
|
765
917
|
messages.push({ role: "user", content: results });
|
|
766
918
|
}
|
|
767
|
-
yield { type: "done", result: this.endRun(runStart, "", messages, toolCalls, turns, usage) };
|
|
919
|
+
yield { type: "done", result: this.endRun(runStart, "", messages, toolCalls, turns, usage, true) };
|
|
768
920
|
}
|
|
769
921
|
catch (e) {
|
|
770
922
|
this.emitRunError(runStart, toolCalls, turns, usage, e);
|
package/dist/index.d.ts
CHANGED
|
@@ -8,5 +8,7 @@ export * from "./a2a.js";
|
|
|
8
8
|
export * from "./serve.js";
|
|
9
9
|
export * from "./mcpserve.js";
|
|
10
10
|
export * from "./adapters.js";
|
|
11
|
+
export * from "./translate.js";
|
|
11
12
|
export * from "./toolkit.js";
|
|
12
13
|
export * from "./client.js";
|
|
14
|
+
export * as agents from "./agents/index.js";
|
package/dist/index.js
CHANGED
|
@@ -8,5 +8,7 @@ export * from "./a2a.js";
|
|
|
8
8
|
export * from "./serve.js";
|
|
9
9
|
export * from "./mcpserve.js";
|
|
10
10
|
export * from "./adapters.js";
|
|
11
|
+
export * from "./translate.js";
|
|
11
12
|
export * from "./toolkit.js";
|
|
12
13
|
export * from "./client.js";
|
|
14
|
+
export * as agents from "./agents/index.js";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-turn translation — the translator path (SPEC.md §11, ADR-0011).
|
|
3
|
+
*
|
|
4
|
+
* `client.translate(request)` is toolnexus used as a pure wire-format translator: OpenAI
|
|
5
|
+
* shapes in, exactly ONE provider call, OpenAI shapes out. No agent loop, no tool
|
|
6
|
+
* execution, no conversation state — so a caller can run it statelessly.
|
|
7
|
+
*
|
|
8
|
+
* It is the INBOUND half of the §5 adapters: `toOpenAI`/`toAnthropic`/`toGemini` send
|
|
9
|
+
* declarations out, this reads the provider's tool calls back in.
|
|
10
|
+
*
|
|
11
|
+
* Use it when the CALLER owns the conversation and executes tools itself (the standard
|
|
12
|
+
* OpenAI function-calling posture). When toolnexus owns the conversation, use the agent
|
|
13
|
+
* loop with relay tools + `runWithAnswer` (§10) instead.
|
|
14
|
+
*/
|
|
15
|
+
import type { Toolkit } from "./toolkit.js";
|
|
16
|
+
import type { Usage } from "./client.js";
|
|
17
|
+
/** An OpenAI-shaped chat request, handed over verbatim (§11). */
|
|
18
|
+
export interface TranslateRequest {
|
|
19
|
+
/**
|
|
20
|
+
* The OpenAI `messages` array, verbatim — including assistant turns carrying
|
|
21
|
+
* `tool_calls` and `tool`-role results carrying `tool_call_id`.
|
|
22
|
+
*/
|
|
23
|
+
messages: any[];
|
|
24
|
+
/**
|
|
25
|
+
* The OpenAI `tools` array, verbatim
|
|
26
|
+
* (`{type:"function",function:{name,description,parameters}}`). Declaration-only —
|
|
27
|
+
* nothing here is ever executed.
|
|
28
|
+
*/
|
|
29
|
+
tools?: any[];
|
|
30
|
+
/**
|
|
31
|
+
* Declares an ordinary toolkit's tools to the provider WITHOUT executing any of them —
|
|
32
|
+
* MCP tools, skills, native functions, A2A agents, builtins. Use this when you have a
|
|
33
|
+
* toolkit and want the model's tool CALLS handed back to you to dispatch yourself.
|
|
34
|
+
* Composes with `tools`; toolkit declarations come first.
|
|
35
|
+
*/
|
|
36
|
+
toolkit?: Toolkit;
|
|
37
|
+
/** The OpenAI `tool_choice`, verbatim. Omitted when absent. */
|
|
38
|
+
toolChoice?: any;
|
|
39
|
+
/** Overrides the system prompt. Absent uses any `system` message found in `messages`. */
|
|
40
|
+
system?: string;
|
|
41
|
+
/** Overrides the per-provider default max tokens. */
|
|
42
|
+
maxTokens?: number;
|
|
43
|
+
/** Aborts the in-flight provider call. */
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
/** One tool call the model asked for, in OpenAI shape (§11). */
|
|
47
|
+
export interface TranslatedToolCall {
|
|
48
|
+
/** The tool-call id the caller must echo on its `tool` result message. */
|
|
49
|
+
id: string;
|
|
50
|
+
/** The function name. */
|
|
51
|
+
name: string;
|
|
52
|
+
/** Arguments as a JSON **string** — the OpenAI wire form, echoable byte-for-byte. */
|
|
53
|
+
arguments: string;
|
|
54
|
+
}
|
|
55
|
+
/** An OpenAI-shaped single-turn result (§11). */
|
|
56
|
+
export interface TranslateResult {
|
|
57
|
+
/** Assistant text ("" when the model only called tools). */
|
|
58
|
+
text: string;
|
|
59
|
+
/** The tool calls the model emitted, in provider order. None is ever dropped. */
|
|
60
|
+
toolCalls: TranslatedToolCall[];
|
|
61
|
+
/** The OpenAI finish reason. A turn with any tool call is always `"tool_calls"`. */
|
|
62
|
+
finishReason: "stop" | "tool_calls" | "length" | "content_filter";
|
|
63
|
+
/** This single call's token usage. */
|
|
64
|
+
usage: Usage;
|
|
65
|
+
/** The model that answered. */
|
|
66
|
+
model: string;
|
|
67
|
+
/** The provider's decoded response, for fields this type does not model. */
|
|
68
|
+
raw?: any;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Maps a provider stop reason onto an OpenAI finish reason. Tool calls win: a turn that
|
|
72
|
+
* emitted any tool call is always `"tool_calls"` to a conforming client.
|
|
73
|
+
*/
|
|
74
|
+
export declare function finishReasonFor(hasToolCalls: boolean, providerStop?: string): TranslateResult["finishReason"];
|
|
75
|
+
/** Flattens an OpenAI `content` value to text: the string form and the parts-array form. */
|
|
76
|
+
export declare function contentText(content: unknown): string;
|
|
77
|
+
/** Parses a tool-call arguments value into an object, tolerating both wire forms. */
|
|
78
|
+
export declare function argsObject(args: unknown): Record<string, unknown>;
|
|
79
|
+
/** Renders a tool-call arguments value as the JSON string the OpenAI wire format uses. */
|
|
80
|
+
export declare function argsString(args: unknown): string;
|
|
81
|
+
/** Reads an assistant message's OpenAI `tool_calls`. */
|
|
82
|
+
export declare function toolCallsOf(m: any): TranslatedToolCall[];
|
|
83
|
+
/**
|
|
84
|
+
* Converts an OpenAI `messages` array into Anthropic-native messages plus the extracted
|
|
85
|
+
* system prompt, preserving the tool structure a text flattening destroys (§11):
|
|
86
|
+
*
|
|
87
|
+
* - an assistant turn's `tool_calls` become `tool_use` blocks (arguments re-parsed to objects),
|
|
88
|
+
* - a `tool`-role result becomes a `tool_result` block keyed by `tool_call_id`, MERGED into a
|
|
89
|
+
* single user turn when consecutive (providers want one result-bearing turn per assistant turn),
|
|
90
|
+
* - `system`/`developer` messages are hoisted out, since Anthropic takes system separately.
|
|
91
|
+
*/
|
|
92
|
+
export declare function openAIMessagesToAnthropic(messages: any[]): {
|
|
93
|
+
messages: any[];
|
|
94
|
+
system: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Converts an OpenAI `tools` array into Anthropic tool declarations. Entries that are
|
|
98
|
+
* already provider-native pass through; anything unrecognized is skipped.
|
|
99
|
+
*/
|
|
100
|
+
export declare function openAIToolsToAnthropic(tools: any[] | undefined): any[];
|
|
101
|
+
/**
|
|
102
|
+
* Maps OpenAI `tool_choice` onto Anthropic's shape. Returns undefined for absent/"auto"
|
|
103
|
+
* (the provider default) and for anything unrecognized.
|
|
104
|
+
*/
|
|
105
|
+
export declare function openAIToolChoiceToAnthropic(choice: unknown): any;
|
|
106
|
+
/** True when the message list already carries a system-ish message. */
|
|
107
|
+
export declare function hasSystemMessage(messages: any[]): boolean;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-turn translation — the translator path (SPEC.md §11, ADR-0011).
|
|
3
|
+
*
|
|
4
|
+
* `client.translate(request)` is toolnexus used as a pure wire-format translator: OpenAI
|
|
5
|
+
* shapes in, exactly ONE provider call, OpenAI shapes out. No agent loop, no tool
|
|
6
|
+
* execution, no conversation state — so a caller can run it statelessly.
|
|
7
|
+
*
|
|
8
|
+
* It is the INBOUND half of the §5 adapters: `toOpenAI`/`toAnthropic`/`toGemini` send
|
|
9
|
+
* declarations out, this reads the provider's tool calls back in.
|
|
10
|
+
*
|
|
11
|
+
* Use it when the CALLER owns the conversation and executes tools itself (the standard
|
|
12
|
+
* OpenAI function-calling posture). When toolnexus owns the conversation, use the agent
|
|
13
|
+
* loop with relay tools + `runWithAnswer` (§10) instead.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Maps a provider stop reason onto an OpenAI finish reason. Tool calls win: a turn that
|
|
17
|
+
* emitted any tool call is always `"tool_calls"` to a conforming client.
|
|
18
|
+
*/
|
|
19
|
+
export function finishReasonFor(hasToolCalls, providerStop) {
|
|
20
|
+
if (hasToolCalls)
|
|
21
|
+
return "tool_calls";
|
|
22
|
+
switch (providerStop) {
|
|
23
|
+
case "max_tokens":
|
|
24
|
+
case "length":
|
|
25
|
+
return "length";
|
|
26
|
+
case "refusal":
|
|
27
|
+
case "content_filter":
|
|
28
|
+
return "content_filter";
|
|
29
|
+
default:
|
|
30
|
+
return "stop";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Flattens an OpenAI `content` value to text: the string form and the parts-array form. */
|
|
34
|
+
export function contentText(content) {
|
|
35
|
+
if (typeof content === "string")
|
|
36
|
+
return content;
|
|
37
|
+
if (Array.isArray(content)) {
|
|
38
|
+
return content
|
|
39
|
+
.map((p) => (p && typeof p === "object" && typeof p.text === "string" ? p.text : ""))
|
|
40
|
+
.join("");
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
/** Parses a tool-call arguments value into an object, tolerating both wire forms. */
|
|
45
|
+
export function argsObject(args) {
|
|
46
|
+
if (args && typeof args === "object")
|
|
47
|
+
return args;
|
|
48
|
+
if (typeof args === "string" && args.trim()) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(args);
|
|
51
|
+
if (parsed && typeof parsed === "object")
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* fall through to {} — a malformed arguments string is not fatal */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
/** Renders a tool-call arguments value as the JSON string the OpenAI wire format uses. */
|
|
61
|
+
export function argsString(args) {
|
|
62
|
+
if (typeof args === "string")
|
|
63
|
+
return args;
|
|
64
|
+
if (args && typeof args === "object")
|
|
65
|
+
return JSON.stringify(args);
|
|
66
|
+
return "{}";
|
|
67
|
+
}
|
|
68
|
+
/** Reads an assistant message's OpenAI `tool_calls`. */
|
|
69
|
+
export function toolCallsOf(m) {
|
|
70
|
+
const raw = m?.tool_calls;
|
|
71
|
+
if (!Array.isArray(raw))
|
|
72
|
+
return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const tc of raw) {
|
|
75
|
+
const fn = tc?.function;
|
|
76
|
+
if (!fn)
|
|
77
|
+
continue;
|
|
78
|
+
out.push({ id: String(tc.id ?? ""), name: String(fn.name ?? ""), arguments: argsString(fn.arguments) });
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Converts an OpenAI `messages` array into Anthropic-native messages plus the extracted
|
|
84
|
+
* system prompt, preserving the tool structure a text flattening destroys (§11):
|
|
85
|
+
*
|
|
86
|
+
* - an assistant turn's `tool_calls` become `tool_use` blocks (arguments re-parsed to objects),
|
|
87
|
+
* - a `tool`-role result becomes a `tool_result` block keyed by `tool_call_id`, MERGED into a
|
|
88
|
+
* single user turn when consecutive (providers want one result-bearing turn per assistant turn),
|
|
89
|
+
* - `system`/`developer` messages are hoisted out, since Anthropic takes system separately.
|
|
90
|
+
*/
|
|
91
|
+
export function openAIMessagesToAnthropic(messages) {
|
|
92
|
+
const out = [];
|
|
93
|
+
const systemParts = [];
|
|
94
|
+
let pendingResults = [];
|
|
95
|
+
const flush = () => {
|
|
96
|
+
if (pendingResults.length) {
|
|
97
|
+
out.push({ role: "user", content: pendingResults });
|
|
98
|
+
pendingResults = [];
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
for (const m of messages ?? []) {
|
|
102
|
+
if (!m || typeof m !== "object")
|
|
103
|
+
continue;
|
|
104
|
+
switch (m.role) {
|
|
105
|
+
case "system":
|
|
106
|
+
case "developer": {
|
|
107
|
+
flush();
|
|
108
|
+
const s = contentText(m.content);
|
|
109
|
+
if (s)
|
|
110
|
+
systemParts.push(s);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "tool":
|
|
114
|
+
case "function": {
|
|
115
|
+
const block = { type: "tool_result", content: contentText(m.content) };
|
|
116
|
+
if (m.tool_call_id)
|
|
117
|
+
block.tool_use_id = String(m.tool_call_id);
|
|
118
|
+
pendingResults.push(block);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "assistant": {
|
|
122
|
+
flush();
|
|
123
|
+
const blocks = [];
|
|
124
|
+
const s = contentText(m.content);
|
|
125
|
+
if (s)
|
|
126
|
+
blocks.push({ type: "text", text: s });
|
|
127
|
+
for (const tc of toolCallsOf(m)) {
|
|
128
|
+
blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: argsObject(tc.arguments) });
|
|
129
|
+
}
|
|
130
|
+
if (!blocks.length)
|
|
131
|
+
break; // an empty assistant turn would be rejected
|
|
132
|
+
out.push({ role: "assistant", content: blocks });
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
default: {
|
|
136
|
+
flush();
|
|
137
|
+
const s = contentText(m.content);
|
|
138
|
+
if (s)
|
|
139
|
+
out.push({ role: "user", content: s });
|
|
140
|
+
else if (Array.isArray(m.content) && m.content.length)
|
|
141
|
+
out.push({ role: "user", content: m.content });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
flush();
|
|
146
|
+
return { messages: out, system: systemParts.join("\n\n") };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Converts an OpenAI `tools` array into Anthropic tool declarations. Entries that are
|
|
150
|
+
* already provider-native pass through; anything unrecognized is skipped.
|
|
151
|
+
*/
|
|
152
|
+
export function openAIToolsToAnthropic(tools) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const t of tools ?? []) {
|
|
155
|
+
if (!t || typeof t !== "object")
|
|
156
|
+
continue;
|
|
157
|
+
const fn = t.function;
|
|
158
|
+
if (!fn) {
|
|
159
|
+
if (t.name)
|
|
160
|
+
out.push(t); // already native
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!fn.name)
|
|
164
|
+
continue;
|
|
165
|
+
const decl = { name: fn.name };
|
|
166
|
+
if (fn.description)
|
|
167
|
+
decl.description = fn.description;
|
|
168
|
+
decl.input_schema = fn.parameters ?? { type: "object", properties: {} };
|
|
169
|
+
out.push(decl);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Maps OpenAI `tool_choice` onto Anthropic's shape. Returns undefined for absent/"auto"
|
|
175
|
+
* (the provider default) and for anything unrecognized.
|
|
176
|
+
*/
|
|
177
|
+
export function openAIToolChoiceToAnthropic(choice) {
|
|
178
|
+
if (typeof choice === "string") {
|
|
179
|
+
if (choice === "required" || choice === "any")
|
|
180
|
+
return { type: "any" };
|
|
181
|
+
if (choice === "none")
|
|
182
|
+
return { type: "none" };
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
if (choice && typeof choice === "object") {
|
|
186
|
+
const name = choice.function?.name;
|
|
187
|
+
if (name)
|
|
188
|
+
return { type: "tool", name };
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
/** True when the message list already carries a system-ish message. */
|
|
193
|
+
export function hasSystemMessage(messages) {
|
|
194
|
+
return (messages ?? []).some((m) => m?.role === "system" || m?.role === "developer");
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolnexus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"test": "npm run build && node --experimental-strip-types --test test/unit.test.ts",
|
|
20
|
+
"test": "npm run build && node --experimental-strip-types --test test/unit.test.ts test/agents.test.ts test/agent-hooks.test.ts test/home.test.ts test/compaction.test.ts",
|
|
21
21
|
"example": "node --experimental-strip-types examples/basic.ts"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|