toolnexus 0.18.1 → 0.19.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/loop.d.ts +21 -0
- package/dist/agents/loop.js +38 -2
- package/dist/agents/runtime.d.ts +64 -3
- package/dist/agents/runtime.js +88 -19
- package/dist/builtin.js +18 -12
- package/dist/classifier.d.ts +23 -0
- package/dist/classifier.js +30 -24
- package/dist/client.d.ts +98 -12
- package/dist/client.js +129 -31
- 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/loop.ts +40 -2
- package/src/agents/runtime.ts +142 -24
- package/src/builtin.ts +18 -9
- package/src/classifier.ts +42 -26
- package/src/client.ts +156 -43
- 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.
|
|
@@ -390,7 +465,7 @@ export class Client {
|
|
|
390
465
|
async ask(prompt, ctx) {
|
|
391
466
|
// Block-style streaming: run the streaming loop, forward text deltas, still return the
|
|
392
467
|
// final RunResult. Memory (id load/save) is handled by stream() itself, so no duplication.
|
|
393
|
-
if (ctx
|
|
468
|
+
if (ctx?.on_text) {
|
|
394
469
|
let result;
|
|
395
470
|
for await (const ev of this.stream(prompt, { toolkit: ctx.toolkit, id: ctx.id, signal: ctx.signal })) {
|
|
396
471
|
if (ev.type === "text")
|
|
@@ -400,8 +475,8 @@ export class Client {
|
|
|
400
475
|
}
|
|
401
476
|
return result;
|
|
402
477
|
}
|
|
403
|
-
if (!ctx
|
|
404
|
-
return this.run(prompt, { toolkit: ctx
|
|
478
|
+
if (!ctx?.id)
|
|
479
|
+
return this.run(prompt, { toolkit: ctx?.toolkit, signal: ctx?.signal });
|
|
405
480
|
const history = (await this.store.get(ctx.id)) ?? [];
|
|
406
481
|
const result = await this.run(prompt, { toolkit: ctx.toolkit, signal: ctx.signal, history });
|
|
407
482
|
await this.store.save(ctx.id, result.messages);
|
|
@@ -409,7 +484,7 @@ export class Client {
|
|
|
409
484
|
}
|
|
410
485
|
/** A stateful multi-turn conversation that retains history across sends. */
|
|
411
486
|
conversation(ctx) {
|
|
412
|
-
return new Conversation(this, ctx
|
|
487
|
+
return new Conversation(this, ctx?.toolkit, ctx?.signal);
|
|
413
488
|
}
|
|
414
489
|
/**
|
|
415
490
|
* Streaming variant: async-iterate live events (text deltas, tool calls/results, usage, done).
|
|
@@ -418,18 +493,20 @@ export class Client {
|
|
|
418
493
|
*/
|
|
419
494
|
async *stream(prompt, ctx) {
|
|
420
495
|
checkPromptParts(prompt, this.wireOpts());
|
|
421
|
-
const history = ctx
|
|
496
|
+
const history = ctx?.id ? (await this.store.get(ctx.id)) ?? [] : undefined;
|
|
422
497
|
const gen = this.opts.style === "anthropic"
|
|
423
|
-
? this.streamAnthropic(prompt, ctx
|
|
424
|
-
: this.streamOpenAI(prompt, ctx
|
|
498
|
+
? this.streamAnthropic(prompt, ctx?.toolkit, ctx?.signal, history)
|
|
499
|
+
: this.streamOpenAI(prompt, ctx?.toolkit, ctx?.signal, history);
|
|
425
500
|
for await (const ev of gen) {
|
|
426
|
-
if (ev.type === "done" && ctx
|
|
501
|
+
if (ev.type === "done" && ctx?.id)
|
|
427
502
|
await this.store.save(ctx.id, ev.result.messages);
|
|
428
503
|
yield ev;
|
|
429
504
|
}
|
|
430
505
|
}
|
|
506
|
+
/** §0.10 system message. The toolkit is OPTIONAL — with none there is no skills prompt and
|
|
507
|
+
* the system message is the system prompt alone. This deref was issue #86 (ADR 0023). */
|
|
431
508
|
system(toolkit) {
|
|
432
|
-
return [this.opts.systemPrompt ?? "", toolkit
|
|
509
|
+
return [this.opts.systemPrompt ?? "", toolkit?.skillsPrompt() ?? ""].filter(Boolean).join("\n\n");
|
|
433
510
|
}
|
|
434
511
|
/** Build a run-scoped abort signal from the optional run-level timeout + an external signal. */
|
|
435
512
|
makeSignal(external) {
|
|
@@ -484,7 +561,7 @@ export class Client {
|
|
|
484
561
|
/** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
|
|
485
562
|
async runTool(toolkit, name, args, id, turn) {
|
|
486
563
|
const h = this.opts.hooks;
|
|
487
|
-
const source = toolkit
|
|
564
|
+
const source = toolkit?.get(name)?.source ?? "custom";
|
|
488
565
|
const t0 = Date.now();
|
|
489
566
|
let a = args;
|
|
490
567
|
if (h?.beforeTool) {
|
|
@@ -498,7 +575,10 @@ export class Client {
|
|
|
498
575
|
if (ov?.args)
|
|
499
576
|
a = ov.args;
|
|
500
577
|
}
|
|
501
|
-
|
|
578
|
+
// No toolkit ⇒ nothing was declared, so a tool call is the provider inventing one.
|
|
579
|
+
let result = toolkit
|
|
580
|
+
? await toolkit.execute(name, a)
|
|
581
|
+
: { output: `unknown tool: ${name}`, isError: true };
|
|
502
582
|
// A suspension (§10) is not a real result: skip afterTool's failure path on it — the resolved
|
|
503
583
|
// result (post-waitFor) still flows through afterTool in resolvePending — and never count it as
|
|
504
584
|
// a tool error.
|
|
@@ -521,7 +601,7 @@ export class Client {
|
|
|
521
601
|
try {
|
|
522
602
|
const res = await this.llmFetch(url, init, signal);
|
|
523
603
|
if (!res.ok)
|
|
524
|
-
throw
|
|
604
|
+
throw await llmHttpError(res);
|
|
525
605
|
const data = await res.json();
|
|
526
606
|
const tok = perCall(data.usage, style);
|
|
527
607
|
this.emit({ event: "llm", model: this.opts.model, status: "ok", ms: Date.now() - t0, promptTokens: tok.prompt, completionTokens: tok.completion });
|
|
@@ -544,7 +624,7 @@ export class Client {
|
|
|
544
624
|
messages.push({ role: "system", content: system });
|
|
545
625
|
}
|
|
546
626
|
messages.push({ role: "user", content: prompt });
|
|
547
|
-
let tools = toolkit
|
|
627
|
+
let tools = toolkit?.toOpenAI() ?? [];
|
|
548
628
|
const toolCalls = [];
|
|
549
629
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
550
630
|
let turns = 0;
|
|
@@ -624,13 +704,15 @@ export class Client {
|
|
|
624
704
|
if (!answer?.ok)
|
|
625
705
|
return { result: { output: `declined/expired: ${request.prompt}`, isError: true } };
|
|
626
706
|
const t0 = Date.now();
|
|
627
|
-
let result =
|
|
707
|
+
let result = toolkit
|
|
708
|
+
? await toolkit.execute(name, args, { answer })
|
|
709
|
+
: { output: `unknown tool: ${name}`, isError: true };
|
|
628
710
|
if (this.opts.hooks?.afterTool) {
|
|
629
711
|
const ov = await this.opts.hooks.afterTool({ name, args, result, id, turn });
|
|
630
712
|
if (ov?.result)
|
|
631
713
|
result = ov.result;
|
|
632
714
|
}
|
|
633
|
-
this.emitTool(name, toolkit
|
|
715
|
+
this.emitTool(name, toolkit?.get(name)?.source ?? "custom", result, t0);
|
|
634
716
|
if (pendingOf(result))
|
|
635
717
|
result = { output: `unresolved: ${request.prompt}`, isError: true }; // never loop forever
|
|
636
718
|
return { result };
|
|
@@ -644,7 +726,7 @@ export class Client {
|
|
|
644
726
|
const system = this.system(toolkit);
|
|
645
727
|
const runStart = Date.now();
|
|
646
728
|
let messages = history && history.length ? [...history, { role: "user", content: prompt }] : [{ role: "user", content: prompt }];
|
|
647
|
-
let tools = toolkit
|
|
729
|
+
let tools = toolkit?.toAnthropic() ?? [];
|
|
648
730
|
const toolCalls = [];
|
|
649
731
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
650
732
|
let turns = 0;
|
|
@@ -725,7 +807,7 @@ export class Client {
|
|
|
725
807
|
messages.push({ role: "system", content: system });
|
|
726
808
|
}
|
|
727
809
|
messages.push({ role: "user", content: prompt });
|
|
728
|
-
let tools = toolkit
|
|
810
|
+
let tools = toolkit?.toOpenAI() ?? [];
|
|
729
811
|
const toolCalls = [];
|
|
730
812
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
731
813
|
let turns = 0;
|
|
@@ -750,7 +832,7 @@ export class Client {
|
|
|
750
832
|
body: JSON.stringify(this.openaiBody(messages, tools, true)),
|
|
751
833
|
}, signal);
|
|
752
834
|
if (!res.ok || !res.body)
|
|
753
|
-
throw
|
|
835
|
+
throw await llmHttpError(res);
|
|
754
836
|
for await (const line of sseLines(res.body)) {
|
|
755
837
|
if (!line.startsWith("data:"))
|
|
756
838
|
continue;
|
|
@@ -832,7 +914,7 @@ export class Client {
|
|
|
832
914
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
|
833
915
|
const system = this.system(toolkit);
|
|
834
916
|
let messages = history && history.length ? [...history, { role: "user", content: prompt }] : [{ role: "user", content: prompt }];
|
|
835
|
-
let tools = toolkit
|
|
917
|
+
let tools = toolkit?.toAnthropic() ?? [];
|
|
836
918
|
const toolCalls = [];
|
|
837
919
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
838
920
|
let turns = 0;
|
|
@@ -857,7 +939,7 @@ export class Client {
|
|
|
857
939
|
body: JSON.stringify(this.anthropicBody(system, messages, tools, true)),
|
|
858
940
|
}, signal);
|
|
859
941
|
if (!res.ok || !res.body)
|
|
860
|
-
throw
|
|
942
|
+
throw await llmHttpError(res);
|
|
861
943
|
for await (const line of sseLines(res.body)) {
|
|
862
944
|
if (!line.startsWith("data:"))
|
|
863
945
|
continue;
|
|
@@ -942,19 +1024,18 @@ export function createClient(opts) {
|
|
|
942
1024
|
* name can never resolve. */
|
|
943
1025
|
const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1";
|
|
944
1026
|
/**
|
|
945
|
-
*
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
1027
|
+
* Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
|
|
1028
|
+
* the reusable piece of {@link createInProcessClient} — assembles the wire request,
|
|
1029
|
+
* calls `generate`, and re-encodes its answer as an OpenAI-shaped `Response`. Every
|
|
1030
|
+
* caller that wants an in-process model behind a `fetch` slot (the top-level client,
|
|
1031
|
+
* or the agent runtime's `inProcess` option) goes through this ONE function, so there
|
|
1032
|
+
* is no second copy of the request/response assembly to drift.
|
|
951
1033
|
*
|
|
952
1034
|
* Streaming is refused rather than faked — see the thrown error.
|
|
953
1035
|
*/
|
|
954
|
-
export function
|
|
955
|
-
const { generate, ...rest } = opts;
|
|
1036
|
+
export function createInProcessFetch(generate) {
|
|
956
1037
|
if (typeof generate !== "function") {
|
|
957
|
-
throw new Error("toolnexus:
|
|
1038
|
+
throw new Error("toolnexus: createInProcessFetch requires a `generate` function");
|
|
958
1039
|
}
|
|
959
1040
|
const inProcessFetch = async (_url, init) => {
|
|
960
1041
|
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
@@ -999,6 +1080,23 @@ export function createInProcessClient(opts) {
|
|
|
999
1080
|
},
|
|
1000
1081
|
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
1001
1082
|
};
|
|
1083
|
+
return inProcessFetch;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* A client backed by a model running IN THIS PROCESS — no server, no socket, and no
|
|
1087
|
+
* HTTP types to construct.
|
|
1088
|
+
*
|
|
1089
|
+
* This is a second constructor, not a second seam: it builds on the same injectable
|
|
1090
|
+
* transport that `fetch`/`httpClient` uses, so the tool-calling loop, MCP servers,
|
|
1091
|
+
* skills, sub-agents, hooks, metrics and the completion gate all behave identically.
|
|
1092
|
+
* The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
|
|
1093
|
+
* is just that adapter wired into a `Client`.
|
|
1094
|
+
*
|
|
1095
|
+
* Streaming is refused rather than faked — see {@link createInProcessFetch}.
|
|
1096
|
+
*/
|
|
1097
|
+
export function createInProcessClient(opts) {
|
|
1098
|
+
const { generate, ...rest } = opts;
|
|
1099
|
+
const inProcessFetch = createInProcessFetch(generate);
|
|
1002
1100
|
return new Client({
|
|
1003
1101
|
// Retries default to ZERO here, unlike a network client. There is no wire, so
|
|
1004
1102
|
// 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
|