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/src/client.ts
CHANGED
|
@@ -56,7 +56,18 @@ export interface ClientOptions {
|
|
|
56
56
|
/** §10 Suspension resolver. When a tool returns Pending (metadata.pending), the client calls
|
|
57
57
|
* this, then retries the tool with Context.answer = the resolution. Its interior is unconstrained
|
|
58
58
|
* (open a browser, message a channel, watch a file, forward to another agent). Omit for a durable
|
|
59
|
-
* host: run() does not hang — it returns { status:"pending", pending:request } to resume later.
|
|
59
|
+
* host: run() does not hang — it returns { status:"pending", pending:request } to resume later.
|
|
60
|
+
*
|
|
61
|
+
* IDEMPOTENCY CONTRACT (SPEC §7D rewind-to-checkpoint). A durable resume REPLAYS the suspended
|
|
62
|
+
* turn from its PRE-TURN checkpoint: "continues from its checkpoint" means the transcript is
|
|
63
|
+
* REWOUND, not carried. EVERY tool that ran in that turn RUNS AGAIN, and the suspended tool
|
|
64
|
+
* itself is always re-executed (with `ctx.answer`) — that is §10's resolution mechanism, not a
|
|
65
|
+
* bug. Reattachment by task key makes a re-run `task` call idempotent; a leaf agent's own
|
|
66
|
+
* side-effecting tools (`git push`, a charge, a delete) have NO equivalent and are the host's
|
|
67
|
+
* responsibility. Any tool reachable in a suspendable turn must be idempotent.
|
|
68
|
+
* Replaying the leaf's stored transcript instead is DEFERRED to its own change; it would not
|
|
69
|
+
* remove this requirement anyway.
|
|
70
|
+
*/
|
|
60
71
|
waitFor?: (request: Request) => Promise<Answer>
|
|
61
72
|
/** §8 Gap 1. Extra top-level keys shallow-merged into EVERY LLM body after the client builds
|
|
62
73
|
* its own — a requestParams key WINS on collision (e.g. max_tokens overrides the anthropic
|
|
@@ -84,6 +95,77 @@ export interface ClientOptions {
|
|
|
84
95
|
onUnsupportedPart?: UnsupportedPartMode
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The §8 CLIENT status vocabulary, as a value. It is a DIFFERENT, smaller closed set than the
|
|
100
|
+
* §7D AGENT vocabulary (`agents.TASK_STATUSES`), which also contains `"timeout"`, `"closed"`,
|
|
101
|
+
* `"interrupted"` and `"error"`. Two fields are called `status`; they are not interchangeable.
|
|
102
|
+
* A run that fails at a deadline THROWS here — it never comes back as a `RunResult`.
|
|
103
|
+
*/
|
|
104
|
+
export const RUN_STATUSES = ["done", "pending", "incomplete"] as const
|
|
105
|
+
export type RunStatus = (typeof RUN_STATUSES)[number]
|
|
106
|
+
|
|
107
|
+
/** Account-identifier keys blanked out of a provider error body before it reaches a message,
|
|
108
|
+
* a log or a metric. The shape survives; the value does not (ADR 0027 D3.2). */
|
|
109
|
+
export const REDACTED_BODY_KEYS = ["user_id", "account_id", "org_id", "organization"] as const
|
|
110
|
+
/** What a redacted value is replaced with — identical in all seven ports. */
|
|
111
|
+
export const REDACTION_PLACEHOLDER = "\u00abredacted\u00bb"
|
|
112
|
+
/** A redacted body is capped at this many characters IN THE MESSAGE ONLY. The typed `body`
|
|
113
|
+
* field carries the full redacted body — a host that opted into a typed error asked for the
|
|
114
|
+
* whole thing (ADR 0027 D3 / addendum A5). */
|
|
115
|
+
export const ERROR_BODY_CAP = 200
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Redact known account identifiers, then cap. A CAP IS NOT REDACTION: the body that leaked
|
|
119
|
+
* `user_2…` in issue #92 was 96 bytes, well inside the cap, so both steps are needed and
|
|
120
|
+
* redaction runs FIRST (capping first could split a key/value pair and hide it from the regex).
|
|
121
|
+
*/
|
|
122
|
+
export function redactErrorBody(body: string): string {
|
|
123
|
+
let out = body
|
|
124
|
+
for (const key of REDACTED_BODY_KEYS) {
|
|
125
|
+
// JSON ("key": "value" / "key": 123) and form/query (key=value) spellings alike.
|
|
126
|
+
out = out.replace(new RegExp(`("${key}"\\s*:\\s*)("(?:[^"\\\\]|\\\\.)*"|-?\\d+(?:\\.\\d+)?|true|false|null)`, "g"), `$1"${REDACTION_PLACEHOLDER}"`)
|
|
127
|
+
out = out.replace(new RegExp(`\\b${key}=([^&\\s]+)`, "g"), `${key}=${REDACTION_PLACEHOLDER}`)
|
|
128
|
+
}
|
|
129
|
+
return out.trim()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Cap a already-redacted body for interpolation into a MESSAGE. Redaction is not a cap and a
|
|
133
|
+
* cap is not redaction: they are independent and both are needed. */
|
|
134
|
+
export function capErrorBody(body: string): string {
|
|
135
|
+
return body.length > ERROR_BODY_CAP ? body.slice(0, ERROR_BODY_CAP) + "\u2026" : body
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A provider failure carried as a VALUE, not a sentence (ADR 0027 D3.1). A host that must
|
|
140
|
+
* decide what to log can read `status` / `retryAfter` instead of parsing a message.
|
|
141
|
+
*
|
|
142
|
+
* `body` is ALREADY redacted and capped, and is `""` on 401/403 — an auth body routinely
|
|
143
|
+
* reflects the credential or the header that was sent. `rawBody` is never populated by the
|
|
144
|
+
* library; the SPEC credentials guarantee now covers error messages too.
|
|
145
|
+
*/
|
|
146
|
+
export class LlmHttpError extends Error {
|
|
147
|
+
readonly name = "LlmHttpError"
|
|
148
|
+
constructor(
|
|
149
|
+
/** The HTTP status the provider answered with. */
|
|
150
|
+
readonly status: number,
|
|
151
|
+
/** The provider's FULL redacted body; `""` on 401/403 and when empty. Uncapped — the cap
|
|
152
|
+
* applies to `message` only. */
|
|
153
|
+
readonly body: string,
|
|
154
|
+
/** The `Retry-After` header verbatim, when the provider sent one. */
|
|
155
|
+
readonly retryAfter?: string,
|
|
156
|
+
) {
|
|
157
|
+
super(`LLM ${status}${body ? ": " + capErrorBody(body) : ""}`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Build the typed error from a non-2xx provider response (reads the body once). */
|
|
162
|
+
export async function llmHttpError(res: Response): Promise<LlmHttpError> {
|
|
163
|
+
const raw = await res.text().catch(() => "")
|
|
164
|
+
// 401/403: never echo the body — see `Classifier.cause`, now one policy, not two.
|
|
165
|
+
const body = res.status === 401 || res.status === 403 ? "" : redactErrorBody(raw)
|
|
166
|
+
return new LlmHttpError(res.status, body, res.headers.get("retry-after") ?? undefined)
|
|
167
|
+
}
|
|
168
|
+
|
|
87
169
|
/** §8 Resilience. What to do with a failed LLM call. */
|
|
88
170
|
export type ErrorTier = "retry" | "fail"
|
|
89
171
|
|
|
@@ -443,11 +525,17 @@ export class Client {
|
|
|
443
525
|
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) })
|
|
444
526
|
}
|
|
445
527
|
|
|
446
|
-
|
|
528
|
+
/**
|
|
529
|
+
* Run the loop. `ctx` — and `ctx.toolkit` inside it — are OPTIONAL: an absent toolkit is a
|
|
530
|
+
* COMPLETION (SPEC §0.10). The system message is then the system prompt alone, and the
|
|
531
|
+
* request body carries no `tools` and no `tool_choice` key (not an empty array). An empty
|
|
532
|
+
* toolkit and no toolkit are observably identical on the wire.
|
|
533
|
+
*/
|
|
534
|
+
async run(prompt: PromptInput, ctx?: { toolkit?: Toolkit; signal?: AbortSignal; history?: any[] }): Promise<RunResult> {
|
|
447
535
|
checkPromptParts(prompt, this.wireOpts())
|
|
448
536
|
return this.opts.style === "anthropic"
|
|
449
|
-
? this.runAnthropic(prompt, ctx
|
|
450
|
-
: this.runOpenAI(prompt, ctx
|
|
537
|
+
? this.runAnthropic(prompt, ctx?.toolkit, ctx?.signal, ctx?.history)
|
|
538
|
+
: this.runOpenAI(prompt, ctx?.toolkit, ctx?.signal, ctx?.history)
|
|
451
539
|
}
|
|
452
540
|
|
|
453
541
|
/**
|
|
@@ -609,10 +697,10 @@ export class Client {
|
|
|
609
697
|
* the answer — so the next `ask` with the same `id` continues it. Without an
|
|
610
698
|
* `id` it is a stateless one-shot (identical to `run`).
|
|
611
699
|
*/
|
|
612
|
-
async ask(prompt: PromptInput, ctx
|
|
700
|
+
async ask(prompt: PromptInput, ctx?: { toolkit?: Toolkit; id?: string; on_text?: (delta: string) => void; signal?: AbortSignal }): Promise<RunResult> {
|
|
613
701
|
// Block-style streaming: run the streaming loop, forward text deltas, still return the
|
|
614
702
|
// final RunResult. Memory (id load/save) is handled by stream() itself, so no duplication.
|
|
615
|
-
if (ctx
|
|
703
|
+
if (ctx?.on_text) {
|
|
616
704
|
let result: RunResult | undefined
|
|
617
705
|
for await (const ev of this.stream(prompt, { toolkit: ctx.toolkit, id: ctx.id, signal: ctx.signal })) {
|
|
618
706
|
if (ev.type === "text") ctx.on_text(ev.delta)
|
|
@@ -620,7 +708,7 @@ export class Client {
|
|
|
620
708
|
}
|
|
621
709
|
return result!
|
|
622
710
|
}
|
|
623
|
-
if (!ctx
|
|
711
|
+
if (!ctx?.id) return this.run(prompt, { toolkit: ctx?.toolkit, signal: ctx?.signal })
|
|
624
712
|
const history = (await this.store.get(ctx.id)) ?? []
|
|
625
713
|
const result = await this.run(prompt, { toolkit: ctx.toolkit, signal: ctx.signal, history })
|
|
626
714
|
await this.store.save(ctx.id, result.messages)
|
|
@@ -628,8 +716,8 @@ export class Client {
|
|
|
628
716
|
}
|
|
629
717
|
|
|
630
718
|
/** A stateful multi-turn conversation that retains history across sends. */
|
|
631
|
-
conversation(ctx
|
|
632
|
-
return new Conversation(this, ctx
|
|
719
|
+
conversation(ctx?: { toolkit?: Toolkit; signal?: AbortSignal }): Conversation {
|
|
720
|
+
return new Conversation(this, ctx?.toolkit, ctx?.signal)
|
|
633
721
|
}
|
|
634
722
|
|
|
635
723
|
/**
|
|
@@ -637,20 +725,22 @@ export class Client {
|
|
|
637
725
|
* With an `id`, it is stateful (like `ask`): the thread's transcript is loaded as history before
|
|
638
726
|
* streaming, and saved back to the ConversationStore on the terminal `done` event. No `id` ⇒ stateless.
|
|
639
727
|
*/
|
|
640
|
-
async *stream(prompt: PromptInput, ctx
|
|
728
|
+
async *stream(prompt: PromptInput, ctx?: { toolkit?: Toolkit; id?: string; signal?: AbortSignal }): AsyncGenerator<StreamEvent, void, unknown> {
|
|
641
729
|
checkPromptParts(prompt, this.wireOpts())
|
|
642
|
-
const history = ctx
|
|
730
|
+
const history = ctx?.id ? (await this.store.get(ctx.id)) ?? [] : undefined
|
|
643
731
|
const gen = this.opts.style === "anthropic"
|
|
644
|
-
? this.streamAnthropic(prompt, ctx
|
|
645
|
-
: this.streamOpenAI(prompt, ctx
|
|
732
|
+
? this.streamAnthropic(prompt, ctx?.toolkit, ctx?.signal, history)
|
|
733
|
+
: this.streamOpenAI(prompt, ctx?.toolkit, ctx?.signal, history)
|
|
646
734
|
for await (const ev of gen) {
|
|
647
|
-
if (ev.type === "done" && ctx
|
|
735
|
+
if (ev.type === "done" && ctx?.id) await this.store.save(ctx.id, ev.result.messages)
|
|
648
736
|
yield ev
|
|
649
737
|
}
|
|
650
738
|
}
|
|
651
739
|
|
|
652
|
-
|
|
653
|
-
|
|
740
|
+
/** §0.10 system message. The toolkit is OPTIONAL — with none there is no skills prompt and
|
|
741
|
+
* the system message is the system prompt alone. This deref was issue #86 (ADR 0023). */
|
|
742
|
+
private system(toolkit?: Toolkit): string {
|
|
743
|
+
return [this.opts.systemPrompt ?? "", toolkit?.skillsPrompt() ?? ""].filter(Boolean).join("\n\n")
|
|
654
744
|
}
|
|
655
745
|
|
|
656
746
|
/** Build a run-scoped abort signal from the optional run-level timeout + an external signal. */
|
|
@@ -699,9 +789,9 @@ export class Client {
|
|
|
699
789
|
}
|
|
700
790
|
|
|
701
791
|
/** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
|
|
702
|
-
private async runTool(toolkit: Toolkit, name: string, args: Record<string, unknown>, id: string | undefined, turn: number) {
|
|
792
|
+
private async runTool(toolkit: Toolkit | undefined, name: string, args: Record<string, unknown>, id: string | undefined, turn: number) {
|
|
703
793
|
const h = this.opts.hooks
|
|
704
|
-
const source = toolkit
|
|
794
|
+
const source = toolkit?.get(name)?.source ?? "custom"
|
|
705
795
|
const t0 = Date.now()
|
|
706
796
|
let a = args
|
|
707
797
|
if (h?.beforeTool) {
|
|
@@ -714,7 +804,10 @@ export class Client {
|
|
|
714
804
|
}
|
|
715
805
|
if (ov?.args) a = ov.args
|
|
716
806
|
}
|
|
717
|
-
|
|
807
|
+
// No toolkit ⇒ nothing was declared, so a tool call is the provider inventing one.
|
|
808
|
+
let result: ToolResult = toolkit
|
|
809
|
+
? await toolkit.execute(name, a)
|
|
810
|
+
: { output: `unknown tool: ${name}`, isError: true }
|
|
718
811
|
// A suspension (§10) is not a real result: skip afterTool's failure path on it — the resolved
|
|
719
812
|
// result (post-waitFor) still flows through afterTool in resolvePending — and never count it as
|
|
720
813
|
// a tool error.
|
|
@@ -737,7 +830,7 @@ export class Client {
|
|
|
737
830
|
const t0 = Date.now()
|
|
738
831
|
try {
|
|
739
832
|
const res = await this.llmFetch(url, init, signal)
|
|
740
|
-
if (!res.ok) throw
|
|
833
|
+
if (!res.ok) throw await llmHttpError(res)
|
|
741
834
|
const data: any = await res.json()
|
|
742
835
|
const tok = perCall(data.usage, style)
|
|
743
836
|
this.emit({ event: "llm", model: this.opts.model, status: "ok", ms: Date.now() - t0, promptTokens: tok.prompt, completionTokens: tok.completion })
|
|
@@ -749,7 +842,7 @@ export class Client {
|
|
|
749
842
|
}
|
|
750
843
|
|
|
751
844
|
// ---- OpenAI-style: POST {baseUrl}/chat/completions ----
|
|
752
|
-
private async runOpenAI(prompt: PromptInput, toolkit: Toolkit, external?: AbortSignal, history?: any[]): Promise<RunResult> {
|
|
845
|
+
private async runOpenAI(prompt: PromptInput, toolkit: Toolkit | undefined, external?: AbortSignal, history?: any[]): Promise<RunResult> {
|
|
753
846
|
const key = resolveKey(this.opts)
|
|
754
847
|
const signal = this.makeSignal(external)
|
|
755
848
|
const runStart = Date.now()
|
|
@@ -759,7 +852,7 @@ export class Client {
|
|
|
759
852
|
if (system) messages.push({ role: "system", content: system })
|
|
760
853
|
}
|
|
761
854
|
messages.push({ role: "user", content: prompt })
|
|
762
|
-
let tools = toolkit
|
|
855
|
+
let tools = toolkit?.toOpenAI() ?? []
|
|
763
856
|
const toolCalls: ToolCallRecord[] = []
|
|
764
857
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
765
858
|
let turns = 0
|
|
@@ -832,24 +925,26 @@ export class Client {
|
|
|
832
925
|
* no `waitFor` is configured (the run should stop and surface the request).
|
|
833
926
|
*/
|
|
834
927
|
private async resolvePending(
|
|
835
|
-
toolkit: Toolkit, name: string, args: Record<string, unknown>, request: Request, id: string | undefined, turn: number,
|
|
928
|
+
toolkit: Toolkit | undefined, name: string, args: Record<string, unknown>, request: Request, id: string | undefined, turn: number,
|
|
836
929
|
): Promise<{ result: ToolResult; halted?: Request }> {
|
|
837
930
|
if (!this.opts.waitFor) return { result: { output: request.prompt, isError: true }, halted: request }
|
|
838
931
|
const answer = await this.opts.waitFor(request)
|
|
839
932
|
if (!answer?.ok) return { result: { output: `declined/expired: ${request.prompt}`, isError: true } }
|
|
840
933
|
const t0 = Date.now()
|
|
841
|
-
let result =
|
|
934
|
+
let result: ToolResult = toolkit
|
|
935
|
+
? await toolkit.execute(name, args, { answer })
|
|
936
|
+
: { output: `unknown tool: ${name}`, isError: true }
|
|
842
937
|
if (this.opts.hooks?.afterTool) {
|
|
843
938
|
const ov = await this.opts.hooks.afterTool({ name, args, result, id, turn })
|
|
844
939
|
if (ov?.result) result = ov.result
|
|
845
940
|
}
|
|
846
|
-
this.emitTool(name, toolkit
|
|
941
|
+
this.emitTool(name, toolkit?.get(name)?.source ?? "custom", result, t0)
|
|
847
942
|
if (pendingOf(result)) result = { output: `unresolved: ${request.prompt}`, isError: true } // never loop forever
|
|
848
943
|
return { result }
|
|
849
944
|
}
|
|
850
945
|
|
|
851
946
|
// ---- Anthropic-style: POST {baseUrl}/messages ----
|
|
852
|
-
private async runAnthropic(prompt: PromptInput, toolkit: Toolkit, external?: AbortSignal, history?: any[]): Promise<RunResult> {
|
|
947
|
+
private async runAnthropic(prompt: PromptInput, toolkit: Toolkit | undefined, external?: AbortSignal, history?: any[]): Promise<RunResult> {
|
|
853
948
|
const key = resolveKey(this.opts)
|
|
854
949
|
const signal = this.makeSignal(external)
|
|
855
950
|
const base = this.opts.baseUrl.replace(/\/$/, "")
|
|
@@ -857,7 +952,7 @@ export class Client {
|
|
|
857
952
|
const system = this.system(toolkit)
|
|
858
953
|
const runStart = Date.now()
|
|
859
954
|
let messages: any[] = history && history.length ? [...history, { role: "user", content: prompt }] : [{ role: "user", content: prompt }]
|
|
860
|
-
let tools = toolkit
|
|
955
|
+
let tools = toolkit?.toAnthropic() ?? []
|
|
861
956
|
const toolCalls: ToolCallRecord[] = []
|
|
862
957
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
863
958
|
let turns = 0
|
|
@@ -923,7 +1018,7 @@ export class Client {
|
|
|
923
1018
|
}
|
|
924
1019
|
|
|
925
1020
|
// ---- Streaming: OpenAI-style ----
|
|
926
|
-
private async *streamOpenAI(prompt: PromptInput, toolkit: Toolkit, external?: AbortSignal, history?: any[]): AsyncGenerator<StreamEvent, void, unknown> {
|
|
1021
|
+
private async *streamOpenAI(prompt: PromptInput, toolkit: Toolkit | undefined, external?: AbortSignal, history?: any[]): AsyncGenerator<StreamEvent, void, unknown> {
|
|
927
1022
|
const key = resolveKey(this.opts)
|
|
928
1023
|
const signal = this.makeSignal(external)
|
|
929
1024
|
const runStart = Date.now()
|
|
@@ -933,7 +1028,7 @@ export class Client {
|
|
|
933
1028
|
if (system) messages.push({ role: "system", content: system })
|
|
934
1029
|
}
|
|
935
1030
|
messages.push({ role: "user", content: prompt })
|
|
936
|
-
let tools = toolkit
|
|
1031
|
+
let tools = toolkit?.toOpenAI() ?? []
|
|
937
1032
|
const toolCalls: ToolCallRecord[] = []
|
|
938
1033
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
939
1034
|
let turns = 0
|
|
@@ -956,7 +1051,7 @@ export class Client {
|
|
|
956
1051
|
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...this.opts.headers },
|
|
957
1052
|
body: JSON.stringify(this.openaiBody(messages, tools, true)),
|
|
958
1053
|
}, signal)
|
|
959
|
-
if (!res.ok || !res.body) throw
|
|
1054
|
+
if (!res.ok || !res.body) throw await llmHttpError(res)
|
|
960
1055
|
for await (const line of sseLines(res.body)) {
|
|
961
1056
|
if (!line.startsWith("data:")) continue
|
|
962
1057
|
const payload = line.slice(5).trim()
|
|
@@ -1019,7 +1114,7 @@ export class Client {
|
|
|
1019
1114
|
}
|
|
1020
1115
|
|
|
1021
1116
|
// ---- Streaming: Anthropic-style ----
|
|
1022
|
-
private async *streamAnthropic(prompt: PromptInput, toolkit: Toolkit, external?: AbortSignal, history?: any[]): AsyncGenerator<StreamEvent, void, unknown> {
|
|
1117
|
+
private async *streamAnthropic(prompt: PromptInput, toolkit: Toolkit | undefined, external?: AbortSignal, history?: any[]): AsyncGenerator<StreamEvent, void, unknown> {
|
|
1023
1118
|
const key = resolveKey(this.opts)
|
|
1024
1119
|
const signal = this.makeSignal(external)
|
|
1025
1120
|
const runStart = Date.now()
|
|
@@ -1027,7 +1122,7 @@ export class Client {
|
|
|
1027
1122
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`
|
|
1028
1123
|
const system = this.system(toolkit)
|
|
1029
1124
|
let messages: any[] = history && history.length ? [...history, { role: "user", content: prompt }] : [{ role: "user", content: prompt }]
|
|
1030
|
-
let tools = toolkit
|
|
1125
|
+
let tools = toolkit?.toAnthropic() ?? []
|
|
1031
1126
|
const toolCalls: ToolCallRecord[] = []
|
|
1032
1127
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1033
1128
|
let turns = 0
|
|
@@ -1050,7 +1145,7 @@ export class Client {
|
|
|
1050
1145
|
headers: { "x-api-key": key, "anthropic-version": "2023-06-01", "Content-Type": "application/json", ...this.opts.headers },
|
|
1051
1146
|
body: JSON.stringify(this.anthropicBody(system, messages, tools, true)),
|
|
1052
1147
|
}, signal)
|
|
1053
|
-
if (!res.ok || !res.body) throw
|
|
1148
|
+
if (!res.ok || !res.body) throw await llmHttpError(res)
|
|
1054
1149
|
for await (const line of sseLines(res.body)) {
|
|
1055
1150
|
if (!line.startsWith("data:")) continue
|
|
1056
1151
|
const j = safeParse(line.slice(5).trim())
|
|
@@ -1162,19 +1257,18 @@ export type InProcessOptions = Omit<ClientOptions, "baseUrl" | "apiKey" | "style
|
|
|
1162
1257
|
const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1"
|
|
1163
1258
|
|
|
1164
1259
|
/**
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1260
|
+
* Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
|
|
1261
|
+
* the reusable piece of {@link createInProcessClient} — assembles the wire request,
|
|
1262
|
+
* calls `generate`, and re-encodes its answer as an OpenAI-shaped `Response`. Every
|
|
1263
|
+
* caller that wants an in-process model behind a `fetch` slot (the top-level client,
|
|
1264
|
+
* or the agent runtime's `inProcess` option) goes through this ONE function, so there
|
|
1265
|
+
* is no second copy of the request/response assembly to drift.
|
|
1171
1266
|
*
|
|
1172
1267
|
* Streaming is refused rather than faked — see the thrown error.
|
|
1173
1268
|
*/
|
|
1174
|
-
export function
|
|
1175
|
-
const { generate, ...rest } = opts
|
|
1269
|
+
export function createInProcessFetch(generate: (request: InProcessRequest) => InProcessResponse | Promise<InProcessResponse>): typeof fetch {
|
|
1176
1270
|
if (typeof generate !== "function") {
|
|
1177
|
-
throw new Error("toolnexus:
|
|
1271
|
+
throw new Error("toolnexus: createInProcessFetch requires a `generate` function")
|
|
1178
1272
|
}
|
|
1179
1273
|
|
|
1180
1274
|
const inProcessFetch: typeof fetch = async (_url, init) => {
|
|
@@ -1229,6 +1323,25 @@ export function createInProcessClient(opts: InProcessOptions): Client {
|
|
|
1229
1323
|
)
|
|
1230
1324
|
}
|
|
1231
1325
|
|
|
1326
|
+
return inProcessFetch
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* A client backed by a model running IN THIS PROCESS — no server, no socket, and no
|
|
1331
|
+
* HTTP types to construct.
|
|
1332
|
+
*
|
|
1333
|
+
* This is a second constructor, not a second seam: it builds on the same injectable
|
|
1334
|
+
* transport that `fetch`/`httpClient` uses, so the tool-calling loop, MCP servers,
|
|
1335
|
+
* skills, sub-agents, hooks, metrics and the completion gate all behave identically.
|
|
1336
|
+
* The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
|
|
1337
|
+
* is just that adapter wired into a `Client`.
|
|
1338
|
+
*
|
|
1339
|
+
* Streaming is refused rather than faked — see {@link createInProcessFetch}.
|
|
1340
|
+
*/
|
|
1341
|
+
export function createInProcessClient(opts: InProcessOptions): Client {
|
|
1342
|
+
const { generate, ...rest } = opts
|
|
1343
|
+
const inProcessFetch = createInProcessFetch(generate)
|
|
1344
|
+
|
|
1232
1345
|
return new Client({
|
|
1233
1346
|
// Retries default to ZERO here, unlike a network client. There is no wire, so
|
|
1234
1347
|
// there is no transient failure to ride out: whatever `generate` throws will
|
|
@@ -1249,7 +1362,7 @@ export function createInProcessClient(opts: InProcessOptions): Client {
|
|
|
1249
1362
|
export class Conversation {
|
|
1250
1363
|
/** Full running transcript (system + user + assistant + tool messages). */
|
|
1251
1364
|
messages: any[] = []
|
|
1252
|
-
constructor(private readonly client: Client, private readonly toolkit
|
|
1365
|
+
constructor(private readonly client: Client, private readonly toolkit?: Toolkit, private readonly signal?: AbortSignal) {}
|
|
1253
1366
|
|
|
1254
1367
|
/** Send the next user turn; prior history is retained automatically. */
|
|
1255
1368
|
async send(prompt: PromptInput): Promise<RunResult> {
|
package/src/index.ts
CHANGED
package/src/order.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
/**
|
|
19
|
+
* Unicode CODE-POINT comparison — the one string ordering rule, identical in all seven ports.
|
|
20
|
+
* Iterates code points rather than code units, so a surrogate pair sorts after U+E000–U+FFFF
|
|
21
|
+
* (where `<`, `Array.sort()` and `StringComparer.Ordinal` all disagree with it).
|
|
22
|
+
*/
|
|
23
|
+
export function compareCodePoints(a: string, b: string): number {
|
|
24
|
+
if (a === b) 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) return y.done ? 0 : -1
|
|
31
|
+
if (y.done) return 1
|
|
32
|
+
const cx = x.value.codePointAt(0)!
|
|
33
|
+
const cy = y.value.codePointAt(0)!
|
|
34
|
+
if (cx !== cy) return cx < cy ? -1 : 1
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The SKILL DISCOVERY order (addendum A15): DEPTH (path segments) ascending, then code point,
|
|
40
|
+
* over a path relative to the root. First-wins then keeps the shallowest copy of a duplicated
|
|
41
|
+
* name, uniformly — code point alone makes the winner depend on the skill's own first letter
|
|
42
|
+
* (`docx` beats `synced/…`, but `xlsx` loses to it).
|
|
43
|
+
*/
|
|
44
|
+
export function compareDiscovery(a: string, b: string, sep: string): number {
|
|
45
|
+
const da = a.split(sep).length
|
|
46
|
+
const db = b.split(sep).length
|
|
47
|
+
if (da !== db) return da < db ? -1 : 1
|
|
48
|
+
return compareCodePoints(a, b)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sort a directory's entries BY NAME, in code-point order, in place.
|
|
53
|
+
*
|
|
54
|
+
* By NAME at each level, never by full path: that is what makes ports walking the same tree with
|
|
55
|
+
* the same stack produce the same sequence, which matters because a capped walk's cap decides
|
|
56
|
+
* WHICH entries are reached.
|
|
57
|
+
*/
|
|
58
|
+
export function sortEntriesByName<T extends { name: string }>(entries: T[]): T[] {
|
|
59
|
+
return entries.sort((x, y) => compareCodePoints(x.name, y.name))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The listing path form (addendum A28): a relative path with `/` as the separator ON EVERY
|
|
64
|
+
* PLATFORM. Every capped or ordered listing EMITS this string and SORTS ON THE SAME STRING —
|
|
65
|
+
* ordering by one thing and showing another is how a listing ends up looking unsorted to its
|
|
66
|
+
* reader while passing its own test. Takes an already-relative path (`path.relative(...)`).
|
|
67
|
+
*/
|
|
68
|
+
export function toPosixPath(rel: string, sep: string): string {
|
|
69
|
+
return sep === "/" ? rel : rel.split(sep).join("/")
|
|
70
|
+
}
|