toolnexus 0.18.1 → 0.20.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/acp.d.ts +42 -0
- package/dist/acp.js +246 -0
- package/dist/agents/agent.d.ts +12 -1
- package/dist/agents/agent.js +3 -2
- package/dist/agents/home.js +4 -2
- package/dist/agents/loop.d.ts +21 -0
- package/dist/agents/loop.js +38 -2
- package/dist/agents/runtime.d.ts +76 -6
- package/dist/agents/runtime.js +92 -22
- package/dist/builtin.d.ts +32 -2
- package/dist/builtin.js +342 -58
- package/dist/classifier.d.ts +23 -0
- package/dist/classifier.js +33 -25
- package/dist/client.d.ts +116 -16
- package/dist/client.js +197 -43
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/order.d.ts +46 -0
- package/dist/order.js +71 -0
- package/dist/skill.d.ts +13 -0
- package/dist/skill.js +110 -27
- package/dist/types.d.ts +11 -0
- package/dist/types.js +22 -0
- package/package.json +2 -2
- package/src/acp.ts +308 -0
- package/src/agents/agent.ts +15 -3
- package/src/agents/home.ts +4 -2
- package/src/agents/loop.ts +40 -2
- package/src/agents/runtime.ts +154 -29
- package/src/builtin.ts +379 -55
- package/src/classifier.ts +45 -27
- package/src/client.ts +220 -51
- package/src/index.ts +1 -0
- package/src/order.ts +70 -0
- package/src/skill.ts +121 -27
- package/src/types.ts +22 -0
package/dist/client.js
CHANGED
|
@@ -2,6 +2,75 @@ import { pendingOf } from "./types.js";
|
|
|
2
2
|
import { isRetryableStatus, retryAfterMs } from "./retry.js";
|
|
3
3
|
import { toOpenAIWire, toAnthropicWire, checkPromptParts } from "./wire.js";
|
|
4
4
|
import { finishReasonFor, argsString, openAIMessagesToAnthropic, openAIToolsToAnthropic, openAIToolChoiceToAnthropic, hasSystemMessage, } from "./translate.js";
|
|
5
|
+
/**
|
|
6
|
+
* The §8 CLIENT status vocabulary, as a value. It is a DIFFERENT, smaller closed set than the
|
|
7
|
+
* §7D AGENT vocabulary (`agents.TASK_STATUSES`), which also contains `"timeout"`, `"closed"`,
|
|
8
|
+
* `"interrupted"` and `"error"`. Two fields are called `status`; they are not interchangeable.
|
|
9
|
+
* A run that fails at a deadline THROWS here — it never comes back as a `RunResult`.
|
|
10
|
+
*/
|
|
11
|
+
export const RUN_STATUSES = ["done", "pending", "incomplete"];
|
|
12
|
+
/** Account-identifier keys blanked out of a provider error body before it reaches a message,
|
|
13
|
+
* a log or a metric. The shape survives; the value does not (ADR 0027 D3.2). */
|
|
14
|
+
export const REDACTED_BODY_KEYS = ["user_id", "account_id", "org_id", "organization"];
|
|
15
|
+
/** What a redacted value is replaced with — identical in all seven ports. */
|
|
16
|
+
export const REDACTION_PLACEHOLDER = "\u00abredacted\u00bb";
|
|
17
|
+
/** A redacted body is capped at this many characters IN THE MESSAGE ONLY. The typed `body`
|
|
18
|
+
* field carries the full redacted body — a host that opted into a typed error asked for the
|
|
19
|
+
* whole thing (ADR 0027 D3 / addendum A5). */
|
|
20
|
+
export const ERROR_BODY_CAP = 200;
|
|
21
|
+
/**
|
|
22
|
+
* Redact known account identifiers, then cap. A CAP IS NOT REDACTION: the body that leaked
|
|
23
|
+
* `user_2…` in issue #92 was 96 bytes, well inside the cap, so both steps are needed and
|
|
24
|
+
* redaction runs FIRST (capping first could split a key/value pair and hide it from the regex).
|
|
25
|
+
*/
|
|
26
|
+
export function redactErrorBody(body) {
|
|
27
|
+
let out = body;
|
|
28
|
+
for (const key of REDACTED_BODY_KEYS) {
|
|
29
|
+
// JSON ("key": "value" / "key": 123) and form/query (key=value) spellings alike.
|
|
30
|
+
out = out.replace(new RegExp(`("${key}"\\s*:\\s*)("(?:[^"\\\\]|\\\\.)*"|-?\\d+(?:\\.\\d+)?|true|false|null)`, "g"), `$1"${REDACTION_PLACEHOLDER}"`);
|
|
31
|
+
out = out.replace(new RegExp(`\\b${key}=([^&\\s]+)`, "g"), `${key}=${REDACTION_PLACEHOLDER}`);
|
|
32
|
+
}
|
|
33
|
+
return out.trim();
|
|
34
|
+
}
|
|
35
|
+
/** Cap a already-redacted body for interpolation into a MESSAGE. Redaction is not a cap and a
|
|
36
|
+
* cap is not redaction: they are independent and both are needed. */
|
|
37
|
+
export function capErrorBody(body) {
|
|
38
|
+
return body.length > ERROR_BODY_CAP ? body.slice(0, ERROR_BODY_CAP) + "\u2026" : body;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A provider failure carried as a VALUE, not a sentence (ADR 0027 D3.1). A host that must
|
|
42
|
+
* decide what to log can read `status` / `retryAfter` instead of parsing a message.
|
|
43
|
+
*
|
|
44
|
+
* `body` is ALREADY redacted and capped, and is `""` on 401/403 — an auth body routinely
|
|
45
|
+
* reflects the credential or the header that was sent. `rawBody` is never populated by the
|
|
46
|
+
* library; the SPEC credentials guarantee now covers error messages too.
|
|
47
|
+
*/
|
|
48
|
+
export class LlmHttpError extends Error {
|
|
49
|
+
status;
|
|
50
|
+
body;
|
|
51
|
+
retryAfter;
|
|
52
|
+
name = "LlmHttpError";
|
|
53
|
+
constructor(
|
|
54
|
+
/** The HTTP status the provider answered with. */
|
|
55
|
+
status,
|
|
56
|
+
/** The provider's FULL redacted body; `""` on 401/403 and when empty. Uncapped — the cap
|
|
57
|
+
* applies to `message` only. */
|
|
58
|
+
body,
|
|
59
|
+
/** The `Retry-After` header verbatim, when the provider sent one. */
|
|
60
|
+
retryAfter) {
|
|
61
|
+
super(`LLM ${status}${body ? ": " + capErrorBody(body) : ""}`);
|
|
62
|
+
this.status = status;
|
|
63
|
+
this.body = body;
|
|
64
|
+
this.retryAfter = retryAfter;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Build the typed error from a non-2xx provider response (reads the body once). */
|
|
68
|
+
export async function llmHttpError(res) {
|
|
69
|
+
const raw = await res.text().catch(() => "");
|
|
70
|
+
// 401/403: never echo the body — see `Classifier.cause`, now one policy, not two.
|
|
71
|
+
const body = res.status === 401 || res.status === 403 ? "" : redactErrorBody(raw);
|
|
72
|
+
return new LlmHttpError(res.status, body, res.headers.get("retry-after") ?? undefined);
|
|
73
|
+
}
|
|
5
74
|
/** Default conversation provider — keeps transcripts in memory for the client's lifetime. */
|
|
6
75
|
export class InMemoryConversationStore {
|
|
7
76
|
map = new Map();
|
|
@@ -229,11 +298,17 @@ export class Client {
|
|
|
229
298
|
emitRunError(runStart, toolCalls, turns, usage, e) {
|
|
230
299
|
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) });
|
|
231
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Run the loop. `ctx` — and `ctx.toolkit` inside it — are OPTIONAL: an absent toolkit is a
|
|
303
|
+
* COMPLETION (SPEC §0.10). The system message is then the system prompt alone, and the
|
|
304
|
+
* request body carries no `tools` and no `tool_choice` key (not an empty array). An empty
|
|
305
|
+
* toolkit and no toolkit are observably identical on the wire.
|
|
306
|
+
*/
|
|
232
307
|
async run(prompt, ctx) {
|
|
233
308
|
checkPromptParts(prompt, this.wireOpts());
|
|
234
309
|
return this.opts.style === "anthropic"
|
|
235
|
-
? this.runAnthropic(prompt, ctx
|
|
236
|
-
: this.runOpenAI(prompt, ctx
|
|
310
|
+
? this.runAnthropic(prompt, ctx?.toolkit, ctx?.signal, ctx?.history)
|
|
311
|
+
: this.runOpenAI(prompt, ctx?.toolkit, ctx?.signal, ctx?.history);
|
|
237
312
|
}
|
|
238
313
|
/**
|
|
239
314
|
* Single-turn translation (§11): exactly ONE provider call, returned in OpenAI shape.
|
|
@@ -274,7 +349,7 @@ export class Client {
|
|
|
274
349
|
if (req.maxTokens)
|
|
275
350
|
body.max_tokens = req.maxTokens;
|
|
276
351
|
const signal = this.makeSignal(req.signal);
|
|
277
|
-
{
|
|
352
|
+
try {
|
|
278
353
|
const data = await this.llmCallJson(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
279
354
|
method: "POST",
|
|
280
355
|
headers: { "content-type": "application/json", authorization: `Bearer ${key}`, ...this.opts.headers },
|
|
@@ -299,6 +374,9 @@ export class Client {
|
|
|
299
374
|
raw: data,
|
|
300
375
|
};
|
|
301
376
|
}
|
|
377
|
+
finally {
|
|
378
|
+
this.releaseSignal(signal);
|
|
379
|
+
}
|
|
302
380
|
}
|
|
303
381
|
/** Anthropic-style upstream: the real translation. */
|
|
304
382
|
async translateAnthropic(req) {
|
|
@@ -332,7 +410,7 @@ export class Client {
|
|
|
332
410
|
if (tc)
|
|
333
411
|
body.tool_choice = tc;
|
|
334
412
|
const signal = this.makeSignal(req.signal);
|
|
335
|
-
{
|
|
413
|
+
try {
|
|
336
414
|
const data = await this.llmCallJson(endpoint, {
|
|
337
415
|
method: "POST",
|
|
338
416
|
headers: {
|
|
@@ -369,6 +447,9 @@ export class Client {
|
|
|
369
447
|
raw: data,
|
|
370
448
|
};
|
|
371
449
|
}
|
|
450
|
+
finally {
|
|
451
|
+
this.releaseSignal(signal);
|
|
452
|
+
}
|
|
372
453
|
}
|
|
373
454
|
/**
|
|
374
455
|
* Renders a translate result's tool calls as an OpenAI `tool_calls` array, ready to put
|
|
@@ -390,7 +471,7 @@ export class Client {
|
|
|
390
471
|
async ask(prompt, ctx) {
|
|
391
472
|
// Block-style streaming: run the streaming loop, forward text deltas, still return the
|
|
392
473
|
// final RunResult. Memory (id load/save) is handled by stream() itself, so no duplication.
|
|
393
|
-
if (ctx
|
|
474
|
+
if (ctx?.on_text) {
|
|
394
475
|
let result;
|
|
395
476
|
for await (const ev of this.stream(prompt, { toolkit: ctx.toolkit, id: ctx.id, signal: ctx.signal })) {
|
|
396
477
|
if (ev.type === "text")
|
|
@@ -400,8 +481,8 @@ export class Client {
|
|
|
400
481
|
}
|
|
401
482
|
return result;
|
|
402
483
|
}
|
|
403
|
-
if (!ctx
|
|
404
|
-
return this.run(prompt, { toolkit: ctx
|
|
484
|
+
if (!ctx?.id)
|
|
485
|
+
return this.run(prompt, { toolkit: ctx?.toolkit, signal: ctx?.signal });
|
|
405
486
|
const history = (await this.store.get(ctx.id)) ?? [];
|
|
406
487
|
const result = await this.run(prompt, { toolkit: ctx.toolkit, signal: ctx.signal, history });
|
|
407
488
|
await this.store.save(ctx.id, result.messages);
|
|
@@ -409,7 +490,7 @@ export class Client {
|
|
|
409
490
|
}
|
|
410
491
|
/** A stateful multi-turn conversation that retains history across sends. */
|
|
411
492
|
conversation(ctx) {
|
|
412
|
-
return new Conversation(this, ctx
|
|
493
|
+
return new Conversation(this, ctx?.toolkit, ctx?.signal);
|
|
413
494
|
}
|
|
414
495
|
/**
|
|
415
496
|
* Streaming variant: async-iterate live events (text deltas, tool calls/results, usage, done).
|
|
@@ -418,25 +499,43 @@ export class Client {
|
|
|
418
499
|
*/
|
|
419
500
|
async *stream(prompt, ctx) {
|
|
420
501
|
checkPromptParts(prompt, this.wireOpts());
|
|
421
|
-
const history = ctx
|
|
502
|
+
const history = ctx?.id ? (await this.store.get(ctx.id)) ?? [] : undefined;
|
|
422
503
|
const gen = this.opts.style === "anthropic"
|
|
423
|
-
? this.streamAnthropic(prompt, ctx
|
|
424
|
-
: this.streamOpenAI(prompt, ctx
|
|
504
|
+
? this.streamAnthropic(prompt, ctx?.toolkit, ctx?.signal, history)
|
|
505
|
+
: this.streamOpenAI(prompt, ctx?.toolkit, ctx?.signal, history);
|
|
425
506
|
for await (const ev of gen) {
|
|
426
|
-
if (ev.type === "done" && ctx
|
|
507
|
+
if (ev.type === "done" && ctx?.id)
|
|
427
508
|
await this.store.save(ctx.id, ev.result.messages);
|
|
428
509
|
yield ev;
|
|
429
510
|
}
|
|
430
511
|
}
|
|
512
|
+
/** §0.10 system message. The toolkit is OPTIONAL — with none there is no skills prompt and
|
|
513
|
+
* the system message is the system prompt alone. This deref was issue #86 (ADR 0023). */
|
|
431
514
|
system(toolkit) {
|
|
432
|
-
return [this.opts.systemPrompt ?? "", toolkit
|
|
515
|
+
return [this.opts.systemPrompt ?? "", toolkit?.skillsPrompt() ?? ""].filter(Boolean).join("\n\n");
|
|
433
516
|
}
|
|
434
|
-
/**
|
|
517
|
+
/** Run-scoped timeout timers, cleared by `releaseSignal` when the run settles. */
|
|
518
|
+
runTimers = new WeakMap();
|
|
519
|
+
/** Drop a run's timeout timer. Idempotent, and safe on a signal that never had one. */
|
|
520
|
+
releaseSignal(signal) {
|
|
521
|
+
const t = this.runTimers.get(signal);
|
|
522
|
+
if (t !== undefined) {
|
|
523
|
+
clearTimeout(t);
|
|
524
|
+
this.runTimers.delete(signal);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/** Build a run-scoped abort signal from the optional run-level timeout + an external signal.
|
|
528
|
+
*
|
|
529
|
+
* The timer is deliberately NOT `unref()`d. An unref'd timer cannot keep the event loop
|
|
530
|
+
* alive, so a run whose only pending work is a request that never settles — an injected
|
|
531
|
+
* `fetch`, an in-process transport, a stalled stream — drains the loop and the timeout never
|
|
532
|
+
* fires at all, which is the one case `timeoutMs` exists for. It is RELEASED instead: the
|
|
533
|
+
* caller clears it however the run leaves, so a finished run holds nothing open. */
|
|
435
534
|
makeSignal(external) {
|
|
436
535
|
const ctrl = new AbortController();
|
|
437
536
|
if (this.opts.timeoutMs) {
|
|
438
537
|
const t = setTimeout(() => ctrl.abort(new Error(`run timeout after ${this.opts.timeoutMs}ms`)), this.opts.timeoutMs);
|
|
439
|
-
|
|
538
|
+
this.runTimers.set(ctrl.signal, t);
|
|
440
539
|
}
|
|
441
540
|
if (external) {
|
|
442
541
|
if (external.aborted)
|
|
@@ -484,7 +583,7 @@ export class Client {
|
|
|
484
583
|
/** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
|
|
485
584
|
async runTool(toolkit, name, args, id, turn) {
|
|
486
585
|
const h = this.opts.hooks;
|
|
487
|
-
const source = toolkit
|
|
586
|
+
const source = toolkit?.get(name)?.source ?? "custom";
|
|
488
587
|
const t0 = Date.now();
|
|
489
588
|
let a = args;
|
|
490
589
|
if (h?.beforeTool) {
|
|
@@ -498,7 +597,10 @@ export class Client {
|
|
|
498
597
|
if (ov?.args)
|
|
499
598
|
a = ov.args;
|
|
500
599
|
}
|
|
501
|
-
|
|
600
|
+
// No toolkit ⇒ nothing was declared, so a tool call is the provider inventing one.
|
|
601
|
+
let result = toolkit
|
|
602
|
+
? await toolkit.execute(name, a)
|
|
603
|
+
: { output: `unknown tool: ${name}`, isError: true };
|
|
502
604
|
// A suspension (§10) is not a real result: skip afterTool's failure path on it — the resolved
|
|
503
605
|
// result (post-waitFor) still flows through afterTool in resolvePending — and never count it as
|
|
504
606
|
// a tool error.
|
|
@@ -521,7 +623,7 @@ export class Client {
|
|
|
521
623
|
try {
|
|
522
624
|
const res = await this.llmFetch(url, init, signal);
|
|
523
625
|
if (!res.ok)
|
|
524
|
-
throw
|
|
626
|
+
throw await llmHttpError(res);
|
|
525
627
|
const data = await res.json();
|
|
526
628
|
const tok = perCall(data.usage, style);
|
|
527
629
|
this.emit({ event: "llm", model: this.opts.model, status: "ok", ms: Date.now() - t0, promptTokens: tok.prompt, completionTokens: tok.completion });
|
|
@@ -532,10 +634,47 @@ export class Client {
|
|
|
532
634
|
throw e;
|
|
533
635
|
}
|
|
534
636
|
}
|
|
535
|
-
//
|
|
637
|
+
// The run-scoped timeout timer is owned by these four wrappers, so it is cleared however the
|
|
638
|
+
// run leaves: return, throw, or a consumer abandoning the stream half-way.
|
|
536
639
|
async runOpenAI(prompt, toolkit, external, history) {
|
|
537
|
-
const key = resolveKey(this.opts);
|
|
538
640
|
const signal = this.makeSignal(external);
|
|
641
|
+
try {
|
|
642
|
+
return await this.runOpenAIWithSignal(signal, prompt, toolkit, history);
|
|
643
|
+
}
|
|
644
|
+
finally {
|
|
645
|
+
this.releaseSignal(signal);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async runAnthropic(prompt, toolkit, external, history) {
|
|
649
|
+
const signal = this.makeSignal(external);
|
|
650
|
+
try {
|
|
651
|
+
return await this.runAnthropicWithSignal(signal, prompt, toolkit, history);
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
this.releaseSignal(signal);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async *streamOpenAI(prompt, toolkit, external, history) {
|
|
658
|
+
const signal = this.makeSignal(external);
|
|
659
|
+
try {
|
|
660
|
+
yield* this.streamOpenAIWithSignal(signal, prompt, toolkit, history);
|
|
661
|
+
}
|
|
662
|
+
finally {
|
|
663
|
+
this.releaseSignal(signal);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async *streamAnthropic(prompt, toolkit, external, history) {
|
|
667
|
+
const signal = this.makeSignal(external);
|
|
668
|
+
try {
|
|
669
|
+
yield* this.streamAnthropicWithSignal(signal, prompt, toolkit, history);
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
this.releaseSignal(signal);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// ---- OpenAI-style: POST {baseUrl}/chat/completions ----
|
|
676
|
+
async runOpenAIWithSignal(signal, prompt, toolkit, history) {
|
|
677
|
+
const key = resolveKey(this.opts);
|
|
539
678
|
const runStart = Date.now();
|
|
540
679
|
let messages = history && history.length ? [...history] : [];
|
|
541
680
|
if (!messages.length) {
|
|
@@ -544,7 +683,7 @@ export class Client {
|
|
|
544
683
|
messages.push({ role: "system", content: system });
|
|
545
684
|
}
|
|
546
685
|
messages.push({ role: "user", content: prompt });
|
|
547
|
-
let tools = toolkit
|
|
686
|
+
let tools = toolkit?.toOpenAI() ?? [];
|
|
548
687
|
const toolCalls = [];
|
|
549
688
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
550
689
|
let turns = 0;
|
|
@@ -624,27 +763,28 @@ export class Client {
|
|
|
624
763
|
if (!answer?.ok)
|
|
625
764
|
return { result: { output: `declined/expired: ${request.prompt}`, isError: true } };
|
|
626
765
|
const t0 = Date.now();
|
|
627
|
-
let result =
|
|
766
|
+
let result = toolkit
|
|
767
|
+
? await toolkit.execute(name, args, { answer })
|
|
768
|
+
: { output: `unknown tool: ${name}`, isError: true };
|
|
628
769
|
if (this.opts.hooks?.afterTool) {
|
|
629
770
|
const ov = await this.opts.hooks.afterTool({ name, args, result, id, turn });
|
|
630
771
|
if (ov?.result)
|
|
631
772
|
result = ov.result;
|
|
632
773
|
}
|
|
633
|
-
this.emitTool(name, toolkit
|
|
774
|
+
this.emitTool(name, toolkit?.get(name)?.source ?? "custom", result, t0);
|
|
634
775
|
if (pendingOf(result))
|
|
635
776
|
result = { output: `unresolved: ${request.prompt}`, isError: true }; // never loop forever
|
|
636
777
|
return { result };
|
|
637
778
|
}
|
|
638
779
|
// ---- Anthropic-style: POST {baseUrl}/messages ----
|
|
639
|
-
async
|
|
780
|
+
async runAnthropicWithSignal(signal, prompt, toolkit, history) {
|
|
640
781
|
const key = resolveKey(this.opts);
|
|
641
|
-
const signal = this.makeSignal(external);
|
|
642
782
|
const base = this.opts.baseUrl.replace(/\/$/, "");
|
|
643
783
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
|
644
784
|
const system = this.system(toolkit);
|
|
645
785
|
const runStart = Date.now();
|
|
646
786
|
let messages = history && history.length ? [...history, { role: "user", content: prompt }] : [{ role: "user", content: prompt }];
|
|
647
|
-
let tools = toolkit
|
|
787
|
+
let tools = toolkit?.toAnthropic() ?? [];
|
|
648
788
|
const toolCalls = [];
|
|
649
789
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
650
790
|
let turns = 0;
|
|
@@ -714,9 +854,8 @@ export class Client {
|
|
|
714
854
|
}
|
|
715
855
|
}
|
|
716
856
|
// ---- Streaming: OpenAI-style ----
|
|
717
|
-
async *
|
|
857
|
+
async *streamOpenAIWithSignal(signal, prompt, toolkit, history) {
|
|
718
858
|
const key = resolveKey(this.opts);
|
|
719
|
-
const signal = this.makeSignal(external);
|
|
720
859
|
const runStart = Date.now();
|
|
721
860
|
let messages = history && history.length ? [...history] : [];
|
|
722
861
|
if (!messages.length) {
|
|
@@ -725,7 +864,7 @@ export class Client {
|
|
|
725
864
|
messages.push({ role: "system", content: system });
|
|
726
865
|
}
|
|
727
866
|
messages.push({ role: "user", content: prompt });
|
|
728
|
-
let tools = toolkit
|
|
867
|
+
let tools = toolkit?.toOpenAI() ?? [];
|
|
729
868
|
const toolCalls = [];
|
|
730
869
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
731
870
|
let turns = 0;
|
|
@@ -750,7 +889,7 @@ export class Client {
|
|
|
750
889
|
body: JSON.stringify(this.openaiBody(messages, tools, true)),
|
|
751
890
|
}, signal);
|
|
752
891
|
if (!res.ok || !res.body)
|
|
753
|
-
throw
|
|
892
|
+
throw await llmHttpError(res);
|
|
754
893
|
for await (const line of sseLines(res.body)) {
|
|
755
894
|
if (!line.startsWith("data:"))
|
|
756
895
|
continue;
|
|
@@ -824,15 +963,14 @@ export class Client {
|
|
|
824
963
|
}
|
|
825
964
|
}
|
|
826
965
|
// ---- Streaming: Anthropic-style ----
|
|
827
|
-
async *
|
|
966
|
+
async *streamAnthropicWithSignal(signal, prompt, toolkit, history) {
|
|
828
967
|
const key = resolveKey(this.opts);
|
|
829
|
-
const signal = this.makeSignal(external);
|
|
830
968
|
const runStart = Date.now();
|
|
831
969
|
const base = this.opts.baseUrl.replace(/\/$/, "");
|
|
832
970
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
|
833
971
|
const system = this.system(toolkit);
|
|
834
972
|
let messages = history && history.length ? [...history, { role: "user", content: prompt }] : [{ role: "user", content: prompt }];
|
|
835
|
-
let tools = toolkit
|
|
973
|
+
let tools = toolkit?.toAnthropic() ?? [];
|
|
836
974
|
const toolCalls = [];
|
|
837
975
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
838
976
|
let turns = 0;
|
|
@@ -857,7 +995,7 @@ export class Client {
|
|
|
857
995
|
body: JSON.stringify(this.anthropicBody(system, messages, tools, true)),
|
|
858
996
|
}, signal);
|
|
859
997
|
if (!res.ok || !res.body)
|
|
860
|
-
throw
|
|
998
|
+
throw await llmHttpError(res);
|
|
861
999
|
for await (const line of sseLines(res.body)) {
|
|
862
1000
|
if (!line.startsWith("data:"))
|
|
863
1001
|
continue;
|
|
@@ -942,19 +1080,18 @@ export function createClient(opts) {
|
|
|
942
1080
|
* name can never resolve. */
|
|
943
1081
|
const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1";
|
|
944
1082
|
/**
|
|
945
|
-
*
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
1083
|
+
* Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
|
|
1084
|
+
* the reusable piece of {@link createInProcessClient} — assembles the wire request,
|
|
1085
|
+
* calls `generate`, and re-encodes its answer as an OpenAI-shaped `Response`. Every
|
|
1086
|
+
* caller that wants an in-process model behind a `fetch` slot (the top-level client,
|
|
1087
|
+
* or the agent runtime's `inProcess` option) goes through this ONE function, so there
|
|
1088
|
+
* is no second copy of the request/response assembly to drift.
|
|
951
1089
|
*
|
|
952
1090
|
* Streaming is refused rather than faked — see the thrown error.
|
|
953
1091
|
*/
|
|
954
|
-
export function
|
|
955
|
-
const { generate, ...rest } = opts;
|
|
1092
|
+
export function createInProcessFetch(generate) {
|
|
956
1093
|
if (typeof generate !== "function") {
|
|
957
|
-
throw new Error("toolnexus:
|
|
1094
|
+
throw new Error("toolnexus: createInProcessFetch requires a `generate` function");
|
|
958
1095
|
}
|
|
959
1096
|
const inProcessFetch = async (_url, init) => {
|
|
960
1097
|
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
@@ -999,6 +1136,23 @@ export function createInProcessClient(opts) {
|
|
|
999
1136
|
},
|
|
1000
1137
|
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
1001
1138
|
};
|
|
1139
|
+
return inProcessFetch;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* A client backed by a model running IN THIS PROCESS — no server, no socket, and no
|
|
1143
|
+
* HTTP types to construct.
|
|
1144
|
+
*
|
|
1145
|
+
* This is a second constructor, not a second seam: it builds on the same injectable
|
|
1146
|
+
* transport that `fetch`/`httpClient` uses, so the tool-calling loop, MCP servers,
|
|
1147
|
+
* skills, sub-agents, hooks, metrics and the completion gate all behave identically.
|
|
1148
|
+
* The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
|
|
1149
|
+
* is just that adapter wired into a `Client`.
|
|
1150
|
+
*
|
|
1151
|
+
* Streaming is refused rather than faked — see {@link createInProcessFetch}.
|
|
1152
|
+
*/
|
|
1153
|
+
export function createInProcessClient(opts) {
|
|
1154
|
+
const { generate, ...rest } = opts;
|
|
1155
|
+
const inProcessFetch = createInProcessFetch(generate);
|
|
1002
1156
|
return new Client({
|
|
1003
1157
|
// Retries default to ZERO here, unlike a network client. There is no wire, so
|
|
1004
1158
|
// there is no transient failure to ride out: whatever `generate` throws will
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/order.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic ordering for everything the library lists (addenda A1a, A15, A22, A24).
|
|
3
|
+
*
|
|
4
|
+
* INTERNAL — deliberately not re-exported from `index.ts`. These are not API; they are the one
|
|
5
|
+
* place the ordering rules live so no caller has to remember them.
|
|
6
|
+
*
|
|
7
|
+
* The rules, and why each exists:
|
|
8
|
+
*
|
|
9
|
+
* - **Never `localeCompare`.** It orders by the machine's ICU data, so the same build emits a
|
|
10
|
+
* different order on a different machine. A cross-port byte-identity claim cannot survive it.
|
|
11
|
+
* - **Never a bare `Array.sort()`.** JS's default compares UTF-16 CODE UNITS, which disagrees
|
|
12
|
+
* with code point above U+FFFF — the same divergence C#'s `StringComparer.Ordinal` has.
|
|
13
|
+
* - **Never an ABSENT sort over a directory read.** `readdirSync` is sorted on most platforms,
|
|
14
|
+
* but Node guarantees nothing, and a walk that breaks at a cap turns that accident into a
|
|
15
|
+
* CONTENT difference: which files appear, not merely their order.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Unicode CODE-POINT comparison — the one string ordering rule, identical in all seven ports.
|
|
19
|
+
* Iterates code points rather than code units, so a surrogate pair sorts after U+E000–U+FFFF
|
|
20
|
+
* (where `<`, `Array.sort()` and `StringComparer.Ordinal` all disagree with it).
|
|
21
|
+
*/
|
|
22
|
+
export declare function compareCodePoints(a: string, b: string): number;
|
|
23
|
+
/**
|
|
24
|
+
* The SKILL DISCOVERY order (addendum A15): DEPTH (path segments) ascending, then code point,
|
|
25
|
+
* over a path relative to the root. First-wins then keeps the shallowest copy of a duplicated
|
|
26
|
+
* name, uniformly — code point alone makes the winner depend on the skill's own first letter
|
|
27
|
+
* (`docx` beats `synced/…`, but `xlsx` loses to it).
|
|
28
|
+
*/
|
|
29
|
+
export declare function compareDiscovery(a: string, b: string, sep: string): number;
|
|
30
|
+
/**
|
|
31
|
+
* Sort a directory's entries BY NAME, in code-point order, in place.
|
|
32
|
+
*
|
|
33
|
+
* By NAME at each level, never by full path: that is what makes ports walking the same tree with
|
|
34
|
+
* the same stack produce the same sequence, which matters because a capped walk's cap decides
|
|
35
|
+
* WHICH entries are reached.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sortEntriesByName<T extends {
|
|
38
|
+
name: string;
|
|
39
|
+
}>(entries: T[]): T[];
|
|
40
|
+
/**
|
|
41
|
+
* The listing path form (addendum A28): a relative path with `/` as the separator ON EVERY
|
|
42
|
+
* PLATFORM. Every capped or ordered listing EMITS this string and SORTS ON THE SAME STRING —
|
|
43
|
+
* ordering by one thing and showing another is how a listing ends up looking unsorted to its
|
|
44
|
+
* reader while passing its own test. Takes an already-relative path (`path.relative(...)`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function toPosixPath(rel: string, sep: string): string;
|
package/dist/order.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic ordering for everything the library lists (addenda A1a, A15, A22, A24).
|
|
3
|
+
*
|
|
4
|
+
* INTERNAL — deliberately not re-exported from `index.ts`. These are not API; they are the one
|
|
5
|
+
* place the ordering rules live so no caller has to remember them.
|
|
6
|
+
*
|
|
7
|
+
* The rules, and why each exists:
|
|
8
|
+
*
|
|
9
|
+
* - **Never `localeCompare`.** It orders by the machine's ICU data, so the same build emits a
|
|
10
|
+
* different order on a different machine. A cross-port byte-identity claim cannot survive it.
|
|
11
|
+
* - **Never a bare `Array.sort()`.** JS's default compares UTF-16 CODE UNITS, which disagrees
|
|
12
|
+
* with code point above U+FFFF — the same divergence C#'s `StringComparer.Ordinal` has.
|
|
13
|
+
* - **Never an ABSENT sort over a directory read.** `readdirSync` is sorted on most platforms,
|
|
14
|
+
* but Node guarantees nothing, and a walk that breaks at a cap turns that accident into a
|
|
15
|
+
* CONTENT difference: which files appear, not merely their order.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Unicode CODE-POINT comparison — the one string ordering rule, identical in all seven ports.
|
|
19
|
+
* Iterates code points rather than code units, so a surrogate pair sorts after U+E000–U+FFFF
|
|
20
|
+
* (where `<`, `Array.sort()` and `StringComparer.Ordinal` all disagree with it).
|
|
21
|
+
*/
|
|
22
|
+
export function compareCodePoints(a, b) {
|
|
23
|
+
if (a === b)
|
|
24
|
+
return 0;
|
|
25
|
+
const ai = a[Symbol.iterator]();
|
|
26
|
+
const bi = b[Symbol.iterator]();
|
|
27
|
+
for (;;) {
|
|
28
|
+
const x = ai.next();
|
|
29
|
+
const y = bi.next();
|
|
30
|
+
if (x.done)
|
|
31
|
+
return y.done ? 0 : -1;
|
|
32
|
+
if (y.done)
|
|
33
|
+
return 1;
|
|
34
|
+
const cx = x.value.codePointAt(0);
|
|
35
|
+
const cy = y.value.codePointAt(0);
|
|
36
|
+
if (cx !== cy)
|
|
37
|
+
return cx < cy ? -1 : 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The SKILL DISCOVERY order (addendum A15): DEPTH (path segments) ascending, then code point,
|
|
42
|
+
* over a path relative to the root. First-wins then keeps the shallowest copy of a duplicated
|
|
43
|
+
* name, uniformly — code point alone makes the winner depend on the skill's own first letter
|
|
44
|
+
* (`docx` beats `synced/…`, but `xlsx` loses to it).
|
|
45
|
+
*/
|
|
46
|
+
export function compareDiscovery(a, b, sep) {
|
|
47
|
+
const da = a.split(sep).length;
|
|
48
|
+
const db = b.split(sep).length;
|
|
49
|
+
if (da !== db)
|
|
50
|
+
return da < db ? -1 : 1;
|
|
51
|
+
return compareCodePoints(a, b);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Sort a directory's entries BY NAME, in code-point order, in place.
|
|
55
|
+
*
|
|
56
|
+
* By NAME at each level, never by full path: that is what makes ports walking the same tree with
|
|
57
|
+
* the same stack produce the same sequence, which matters because a capped walk's cap decides
|
|
58
|
+
* WHICH entries are reached.
|
|
59
|
+
*/
|
|
60
|
+
export function sortEntriesByName(entries) {
|
|
61
|
+
return entries.sort((x, y) => compareCodePoints(x.name, y.name));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The listing path form (addendum A28): a relative path with `/` as the separator ON EVERY
|
|
65
|
+
* PLATFORM. Every capped or ordered listing EMITS this string and SORTS ON THE SAME STRING —
|
|
66
|
+
* ordering by one thing and showing another is how a listing ends up looking unsorted to its
|
|
67
|
+
* reader while passing its own test. Takes an already-relative path (`path.relative(...)`).
|
|
68
|
+
*/
|
|
69
|
+
export function toPosixPath(rel, sep) {
|
|
70
|
+
return sep === "/" ? rel : rel.split(sep).join("/");
|
|
71
|
+
}
|
package/dist/skill.d.ts
CHANGED
|
@@ -32,6 +32,12 @@ export type SkillSkipReason = "missing-name" | "malformed-frontmatter" | "duplic
|
|
|
32
32
|
export interface SkillSkip {
|
|
33
33
|
location: string;
|
|
34
34
|
reason: SkillSkipReason;
|
|
35
|
+
/**
|
|
36
|
+
* The NATIVE parser's own message ("Tabs are not allowed as indentation at line 3"), which is
|
|
37
|
+
* one keystroke from fixed and used to be discarded. Kept as a SEPARATE field so `reason`
|
|
38
|
+
* stays byte-identical across ports while this stays native — never compare it for parity.
|
|
39
|
+
*/
|
|
40
|
+
detail?: string;
|
|
35
41
|
}
|
|
36
42
|
/** Result of a list-only validate pass (SPEC.md §3, S3). */
|
|
37
43
|
export interface SkillInventory {
|
|
@@ -52,6 +58,13 @@ export interface SkillSource {
|
|
|
52
58
|
tool: Tool;
|
|
53
59
|
/** Markdown catalog for the system prompt (mirrors opencode Skill.fmt). */
|
|
54
60
|
prompt(): string;
|
|
61
|
+
/**
|
|
62
|
+
* Candidate SKILL.md files that did NOT become skills, with the typed reason and the native
|
|
63
|
+
* parser `detail` (ADR 0028). Previously only `listSkills` reported these, so the path every
|
|
64
|
+
* README and example uses — `loadSkills` — could load 69 of 75 and say nothing. An omission
|
|
65
|
+
* that stops being reported is indistinguishable from something that was finished.
|
|
66
|
+
*/
|
|
67
|
+
skipped: SkillSkip[];
|
|
55
68
|
}
|
|
56
69
|
/**
|
|
57
70
|
* Discover + validate skills from the same sources loadSkills accepts, returning
|