toolnexus 0.19.0 → 0.21.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 +40 -0
- package/dist/agents/home.js +4 -2
- package/dist/agents/runtime.d.ts +12 -3
- package/dist/agents/runtime.js +4 -3
- package/dist/batteries.d.ts +223 -0
- package/dist/batteries.js +488 -0
- package/dist/builtin.d.ts +32 -2
- package/dist/builtin.js +324 -46
- package/dist/classifier.d.ts +14 -0
- package/dist/classifier.js +37 -2
- package/dist/client.d.ts +35 -17
- package/dist/client.js +153 -67
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/judge.d.ts +98 -0
- package/dist/judge.js +199 -0
- package/package.json +2 -2
- package/src/agents/home.ts +4 -2
- package/src/agents/runtime.ts +12 -5
- package/src/batteries.ts +614 -0
- package/src/builtin.ts +361 -46
- package/src/classifier.ts +39 -2
- package/src/client.ts +166 -67
- package/src/index.ts +26 -0
- package/src/judge.ts +305 -0
package/src/client.ts
CHANGED
|
@@ -240,9 +240,26 @@ function delay(ms: number, signal: AbortSignal): Promise<void> {
|
|
|
240
240
|
* Lifecycle hooks around the agent loop. Each may observe, and (where noted)
|
|
241
241
|
* mutate or short-circuit. All may be async.
|
|
242
242
|
*/
|
|
243
|
+
/** The `beforeLLM` hook event. */
|
|
244
|
+
export interface BeforeLLMEvent {
|
|
245
|
+
messages: any[]
|
|
246
|
+
tools: any[]
|
|
247
|
+
model: string
|
|
248
|
+
turn: number
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** What a `beforeLLM` hook may return. `model` (non-empty) applies to that turn only. */
|
|
252
|
+
export interface LLMOverride {
|
|
253
|
+
messages?: any[]
|
|
254
|
+
tools?: any[]
|
|
255
|
+
model?: string | null
|
|
256
|
+
}
|
|
257
|
+
|
|
243
258
|
export interface Hooks {
|
|
244
|
-
/** Before each model call. Return { messages?, tools? } to replace them
|
|
245
|
-
|
|
259
|
+
/** Before each model call. Return { messages?, tools? } to replace them, and { model } to send a
|
|
260
|
+
* different model for THIS turn only (request body + afterLLM event); absent/null/empty keeps the
|
|
261
|
+
* configured model verbatim (SPEC §8, e.g. a ModelRouterClassifier). */
|
|
262
|
+
beforeLLM?(ev: BeforeLLMEvent): void | LLMOverride | Promise<void | LLMOverride>
|
|
246
263
|
/** After each model call (observe: logging, cost, tracing). */
|
|
247
264
|
afterLLM?(ev: { response: any; model: string; turn: number }): void | Promise<void>
|
|
248
265
|
/** Before a tool runs. Return { result } to SHORT-CIRCUIT (deny/cache), { args } to rewrite. */
|
|
@@ -476,8 +493,8 @@ export class Client {
|
|
|
476
493
|
|
|
477
494
|
/** §8 Gap 5. OpenAI-style body; omits tools/tool_choice when the tool list is empty. Key order
|
|
478
495
|
* matches the pre-change body so a no-options call is byte-identical. */
|
|
479
|
-
private openaiBody(messages: any[], tools: any[], stream: boolean): Record<string, any> {
|
|
480
|
-
const b: Record<string, any> = { model
|
|
496
|
+
private openaiBody(messages: any[], tools: any[], stream: boolean, model: string = this.opts.model): Record<string, any> {
|
|
497
|
+
const b: Record<string, any> = { model, messages: toOpenAIWire(messages, this.wireOpts()) }
|
|
481
498
|
if (tools.length) {
|
|
482
499
|
b.tools = tools
|
|
483
500
|
b.tool_choice = "auto"
|
|
@@ -490,8 +507,8 @@ export class Client {
|
|
|
490
507
|
}
|
|
491
508
|
|
|
492
509
|
/** §8 Gap 5. Anthropic-style body; omits tools when the tool list is empty. */
|
|
493
|
-
private anthropicBody(system: string, messages: any[], tools: any[], stream: boolean): Record<string, any> {
|
|
494
|
-
const b: Record<string, any> = { model
|
|
510
|
+
private anthropicBody(system: string, messages: any[], tools: any[], stream: boolean, model: string = this.opts.model): Record<string, any> {
|
|
511
|
+
const b: Record<string, any> = { model, max_tokens: 4096, system, messages: toAnthropicWire(messages, this.wireOpts()) }
|
|
495
512
|
if (tools.length) b.tools = tools
|
|
496
513
|
if (stream) b.stream = true
|
|
497
514
|
return this.finalizeBody(b)
|
|
@@ -515,14 +532,14 @@ export class Client {
|
|
|
515
532
|
|
|
516
533
|
/** Emit the terminal `run` metric event and build the RunResult. `exhausted` marks a
|
|
517
534
|
* maxTurns loop exit — with no final text that is a LOUD "incomplete", never "done" (§7D). */
|
|
518
|
-
private endRun(runStart: number, text: string, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage, exhausted = false): RunResult {
|
|
519
|
-
this.emit({ event: "run", model
|
|
520
|
-
return this.result(text, messages, toolCalls, turns, usage, exhausted)
|
|
535
|
+
private endRun(runStart: number, model: string, text: string, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage, exhausted = false): RunResult {
|
|
536
|
+
this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart })
|
|
537
|
+
return this.result(model, text, messages, toolCalls, turns, usage, exhausted)
|
|
521
538
|
}
|
|
522
539
|
|
|
523
540
|
/** Emit a `run` error metric event (once, on a thrown run). */
|
|
524
|
-
private emitRunError(runStart: number, toolCalls: ToolCallRecord[], turns: number, usage: Usage, e: unknown): void {
|
|
525
|
-
this.emit({ event: "run", model
|
|
541
|
+
private emitRunError(runStart: number, model: string, toolCalls: ToolCallRecord[], turns: number, usage: Usage, e: unknown): void {
|
|
542
|
+
this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart, error: e instanceof Error ? e.message : String(e) })
|
|
526
543
|
}
|
|
527
544
|
|
|
528
545
|
/**
|
|
@@ -563,19 +580,21 @@ export class Client {
|
|
|
563
580
|
if (sys && !hasSystemMessage(messages)) messages = [{ role: "system", content: sys }, ...messages]
|
|
564
581
|
|
|
565
582
|
let tools = declared
|
|
583
|
+
let turnModel = this.opts.model
|
|
566
584
|
if (this.opts.hooks?.beforeLLM) {
|
|
567
585
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn: 0 })
|
|
568
586
|
if (ov?.messages) messages = ov.messages
|
|
569
587
|
if (ov?.tools) tools = ov.tools
|
|
588
|
+
if (ov?.model) turnModel = ov.model
|
|
570
589
|
}
|
|
571
590
|
|
|
572
|
-
const body: Record<string, any> = { model:
|
|
591
|
+
const body: Record<string, any> = { model: turnModel, messages }
|
|
573
592
|
if (tools.length) body.tools = tools
|
|
574
593
|
if (req.toolChoice !== undefined) body.tool_choice = req.toolChoice
|
|
575
594
|
if (req.maxTokens) body.max_tokens = req.maxTokens
|
|
576
595
|
|
|
577
596
|
const signal = this.makeSignal(req.signal)
|
|
578
|
-
{
|
|
597
|
+
try {
|
|
579
598
|
const data = await this.llmCallJson(
|
|
580
599
|
`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`,
|
|
581
600
|
{
|
|
@@ -585,8 +604,9 @@ export class Client {
|
|
|
585
604
|
},
|
|
586
605
|
signal,
|
|
587
606
|
"openai",
|
|
607
|
+
turnModel,
|
|
588
608
|
)
|
|
589
|
-
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model:
|
|
609
|
+
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model: turnModel, turn: 0 })
|
|
590
610
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
591
611
|
addUsage(usage, data.usage, "openai")
|
|
592
612
|
const choice = data.choices?.[0]
|
|
@@ -600,9 +620,11 @@ export class Client {
|
|
|
600
620
|
toolCalls,
|
|
601
621
|
finishReason: choice?.finish_reason ?? finishReasonFor(toolCalls.length > 0),
|
|
602
622
|
usage,
|
|
603
|
-
model:
|
|
623
|
+
model: turnModel,
|
|
604
624
|
raw: data,
|
|
605
625
|
}
|
|
626
|
+
} finally {
|
|
627
|
+
this.releaseSignal(signal)
|
|
606
628
|
}
|
|
607
629
|
}
|
|
608
630
|
|
|
@@ -620,14 +642,16 @@ export class Client {
|
|
|
620
642
|
...openAIToolsToAnthropic(req.tools),
|
|
621
643
|
]
|
|
622
644
|
|
|
645
|
+
let turnModel = this.opts.model
|
|
623
646
|
if (this.opts.hooks?.beforeLLM) {
|
|
624
647
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn: 0 })
|
|
625
648
|
if (ov?.messages) messages = ov.messages
|
|
626
649
|
if (ov?.tools) tools = ov.tools
|
|
650
|
+
if (ov?.model) turnModel = ov.model
|
|
627
651
|
}
|
|
628
652
|
|
|
629
653
|
const body: Record<string, any> = {
|
|
630
|
-
model:
|
|
654
|
+
model: turnModel,
|
|
631
655
|
max_tokens: req.maxTokens && req.maxTokens > 0 ? req.maxTokens : 4096,
|
|
632
656
|
messages,
|
|
633
657
|
}
|
|
@@ -637,7 +661,7 @@ export class Client {
|
|
|
637
661
|
if (tc) body.tool_choice = tc
|
|
638
662
|
|
|
639
663
|
const signal = this.makeSignal(req.signal)
|
|
640
|
-
{
|
|
664
|
+
try {
|
|
641
665
|
const data = await this.llmCallJson(
|
|
642
666
|
endpoint,
|
|
643
667
|
{
|
|
@@ -652,8 +676,9 @@ export class Client {
|
|
|
652
676
|
},
|
|
653
677
|
signal,
|
|
654
678
|
"anthropic",
|
|
679
|
+
turnModel,
|
|
655
680
|
)
|
|
656
|
-
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model:
|
|
681
|
+
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model: turnModel, turn: 0 })
|
|
657
682
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
658
683
|
addUsage(usage, data.usage, "anthropic")
|
|
659
684
|
const text: string[] = []
|
|
@@ -673,9 +698,11 @@ export class Client {
|
|
|
673
698
|
toolCalls,
|
|
674
699
|
finishReason: finishReasonFor(toolCalls.length > 0, data.stop_reason),
|
|
675
700
|
usage,
|
|
676
|
-
model:
|
|
701
|
+
model: turnModel,
|
|
677
702
|
raw: data,
|
|
678
703
|
}
|
|
704
|
+
} finally {
|
|
705
|
+
this.releaseSignal(signal)
|
|
679
706
|
}
|
|
680
707
|
}
|
|
681
708
|
|
|
@@ -743,12 +770,30 @@ export class Client {
|
|
|
743
770
|
return [this.opts.systemPrompt ?? "", toolkit?.skillsPrompt() ?? ""].filter(Boolean).join("\n\n")
|
|
744
771
|
}
|
|
745
772
|
|
|
746
|
-
/**
|
|
773
|
+
/** Run-scoped timeout timers, cleared by `releaseSignal` when the run settles. */
|
|
774
|
+
private readonly runTimers = new WeakMap<AbortSignal, ReturnType<typeof setTimeout>>()
|
|
775
|
+
|
|
776
|
+
/** Drop a run's timeout timer. Idempotent, and safe on a signal that never had one. */
|
|
777
|
+
private releaseSignal(signal: AbortSignal): void {
|
|
778
|
+
const t = this.runTimers.get(signal)
|
|
779
|
+
if (t !== undefined) {
|
|
780
|
+
clearTimeout(t)
|
|
781
|
+
this.runTimers.delete(signal)
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Build a run-scoped abort signal from the optional run-level timeout + an external signal.
|
|
786
|
+
*
|
|
787
|
+
* The timer is deliberately NOT `unref()`d. An unref'd timer cannot keep the event loop
|
|
788
|
+
* alive, so a run whose only pending work is a request that never settles — an injected
|
|
789
|
+
* `fetch`, an in-process transport, a stalled stream — drains the loop and the timeout never
|
|
790
|
+
* fires at all, which is the one case `timeoutMs` exists for. It is RELEASED instead: the
|
|
791
|
+
* caller clears it however the run leaves, so a finished run holds nothing open. */
|
|
747
792
|
private makeSignal(external?: AbortSignal): AbortSignal {
|
|
748
793
|
const ctrl = new AbortController()
|
|
749
794
|
if (this.opts.timeoutMs) {
|
|
750
795
|
const t = setTimeout(() => ctrl.abort(new Error(`run timeout after ${this.opts.timeoutMs}ms`)), this.opts.timeoutMs)
|
|
751
|
-
|
|
796
|
+
this.runTimers.set(ctrl.signal, t)
|
|
752
797
|
}
|
|
753
798
|
if (external) {
|
|
754
799
|
if (external.aborted) ctrl.abort(external.reason)
|
|
@@ -826,25 +871,62 @@ export class Client {
|
|
|
826
871
|
}
|
|
827
872
|
|
|
828
873
|
/** One non-streaming LLM call, with an `llm` metric event (ok/error + per-call tokens + ms). */
|
|
829
|
-
private async llmCallJson(url: string, init: RequestInit, signal: AbortSignal, style: ClientStyle): Promise<any> {
|
|
874
|
+
private async llmCallJson(url: string, init: RequestInit, signal: AbortSignal, style: ClientStyle, model: string): Promise<any> {
|
|
830
875
|
const t0 = Date.now()
|
|
831
876
|
try {
|
|
832
877
|
const res = await this.llmFetch(url, init, signal)
|
|
833
878
|
if (!res.ok) throw await llmHttpError(res)
|
|
834
879
|
const data: any = await res.json()
|
|
835
880
|
const tok = perCall(data.usage, style)
|
|
836
|
-
this.emit({ event: "llm", model
|
|
881
|
+
this.emit({ event: "llm", model, status: "ok", ms: Date.now() - t0, promptTokens: tok.prompt, completionTokens: tok.completion })
|
|
837
882
|
return data
|
|
838
883
|
} catch (e) {
|
|
839
|
-
this.emit({ event: "llm", model
|
|
884
|
+
this.emit({ event: "llm", model, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 })
|
|
840
885
|
throw e
|
|
841
886
|
}
|
|
842
887
|
}
|
|
843
888
|
|
|
844
|
-
//
|
|
889
|
+
// The run-scoped timeout timer is owned by these four wrappers, so it is cleared however the
|
|
890
|
+
// run leaves: return, throw, or a consumer abandoning the stream half-way.
|
|
845
891
|
private async runOpenAI(prompt: PromptInput, toolkit: Toolkit | undefined, external?: AbortSignal, history?: any[]): Promise<RunResult> {
|
|
846
|
-
const key = resolveKey(this.opts)
|
|
847
892
|
const signal = this.makeSignal(external)
|
|
893
|
+
try {
|
|
894
|
+
return await this.runOpenAIWithSignal(signal, prompt, toolkit, history)
|
|
895
|
+
} finally {
|
|
896
|
+
this.releaseSignal(signal)
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private async runAnthropic(prompt: PromptInput, toolkit: Toolkit | undefined, external?: AbortSignal, history?: any[]): Promise<RunResult> {
|
|
901
|
+
const signal = this.makeSignal(external)
|
|
902
|
+
try {
|
|
903
|
+
return await this.runAnthropicWithSignal(signal, prompt, toolkit, history)
|
|
904
|
+
} finally {
|
|
905
|
+
this.releaseSignal(signal)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private async *streamOpenAI(prompt: PromptInput, toolkit: Toolkit | undefined, external?: AbortSignal, history?: any[]): AsyncGenerator<StreamEvent, void, unknown> {
|
|
910
|
+
const signal = this.makeSignal(external)
|
|
911
|
+
try {
|
|
912
|
+
yield* this.streamOpenAIWithSignal(signal, prompt, toolkit, history)
|
|
913
|
+
} finally {
|
|
914
|
+
this.releaseSignal(signal)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
private async *streamAnthropic(prompt: PromptInput, toolkit: Toolkit | undefined, external?: AbortSignal, history?: any[]): AsyncGenerator<StreamEvent, void, unknown> {
|
|
919
|
+
const signal = this.makeSignal(external)
|
|
920
|
+
try {
|
|
921
|
+
yield* this.streamAnthropicWithSignal(signal, prompt, toolkit, history)
|
|
922
|
+
} finally {
|
|
923
|
+
this.releaseSignal(signal)
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// ---- OpenAI-style: POST {baseUrl}/chat/completions ----
|
|
928
|
+
private async runOpenAIWithSignal(signal: AbortSignal, prompt: PromptInput, toolkit: Toolkit | undefined, history?: any[]): Promise<RunResult> {
|
|
929
|
+
const key = resolveKey(this.opts)
|
|
848
930
|
const runStart = Date.now()
|
|
849
931
|
let messages: any[] = history && history.length ? [...history] : []
|
|
850
932
|
if (!messages.length) {
|
|
@@ -856,26 +938,31 @@ export class Client {
|
|
|
856
938
|
const toolCalls: ToolCallRecord[] = []
|
|
857
939
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
858
940
|
let turns = 0
|
|
941
|
+
// The model of the last call this run made (a beforeLLM override when one applied) — §8.
|
|
942
|
+
let lastModel = this.opts.model
|
|
859
943
|
|
|
860
944
|
try {
|
|
861
945
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
862
946
|
turns++
|
|
947
|
+
let turnModel = this.opts.model
|
|
863
948
|
if (this.opts.hooks?.beforeLLM) {
|
|
864
949
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn })
|
|
865
950
|
if (ov?.messages) messages = ov.messages
|
|
866
951
|
if (ov?.tools) tools = ov.tools
|
|
952
|
+
if (ov?.model) turnModel = ov.model
|
|
867
953
|
}
|
|
954
|
+
lastModel = turnModel
|
|
868
955
|
const data: any = await this.llmCallJson(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
869
956
|
method: "POST",
|
|
870
957
|
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...this.opts.headers },
|
|
871
|
-
body: JSON.stringify(this.openaiBody(messages, tools, false)),
|
|
872
|
-
}, signal, "openai")
|
|
958
|
+
body: JSON.stringify(this.openaiBody(messages, tools, false, turnModel)),
|
|
959
|
+
}, signal, "openai", turnModel)
|
|
873
960
|
addUsage(usage, data.usage, "openai")
|
|
874
|
-
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model:
|
|
961
|
+
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model: turnModel, turn })
|
|
875
962
|
const msg = data.choices[0].message
|
|
876
963
|
messages.push(msg)
|
|
877
964
|
const calls = msg.tool_calls ?? []
|
|
878
|
-
if (calls.length === 0) return this.endRun(runStart, msg.content ?? "", messages, toolCalls, turns, usage)
|
|
965
|
+
if (calls.length === 0) return this.endRun(runStart, lastModel, msg.content ?? "", messages, toolCalls, turns, usage)
|
|
879
966
|
// execute all tool calls in this turn concurrently (true parallel tool calling)
|
|
880
967
|
const settled = await Promise.all(
|
|
881
968
|
calls.map(async (call: any) => {
|
|
@@ -896,27 +983,27 @@ export class Client {
|
|
|
896
983
|
for (const s of settled) {
|
|
897
984
|
toolCalls.push({ name: s.call.function.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata })
|
|
898
985
|
messages.push(toolMessage(s.call.id, s.call.function.name, s.result))
|
|
899
|
-
if (s.halted) return this.pendingRun(runStart, s.halted, messages, toolCalls, turns, usage)
|
|
986
|
+
if (s.halted) return this.pendingRun(runStart, lastModel, s.halted, messages, toolCalls, turns, usage)
|
|
900
987
|
}
|
|
901
988
|
}
|
|
902
|
-
return this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage, true)
|
|
989
|
+
return this.endRun(runStart, lastModel, lastText(messages), messages, toolCalls, turns, usage, true)
|
|
903
990
|
} catch (e) {
|
|
904
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
991
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e)
|
|
905
992
|
throw e
|
|
906
993
|
}
|
|
907
994
|
}
|
|
908
995
|
|
|
909
|
-
private result(text: string, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage, exhausted = false): RunResult {
|
|
996
|
+
private result(model: string, text: string, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage, exhausted = false): RunResult {
|
|
910
997
|
const incomplete = exhausted && text === ""
|
|
911
|
-
const r: RunResult = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model
|
|
998
|
+
const r: RunResult = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" }
|
|
912
999
|
if (incomplete) r.limit = "maxTurns"
|
|
913
1000
|
return r
|
|
914
1001
|
}
|
|
915
1002
|
|
|
916
1003
|
/** §10: a run halted because a tool suspended and no `waitFor` was configured. */
|
|
917
|
-
private pendingRun(runStart: number, request: Request, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage): RunResult {
|
|
918
|
-
this.emit({ event: "run", model
|
|
919
|
-
return { text: request.prompt, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model
|
|
1004
|
+
private pendingRun(runStart: number, model: string, request: Request, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage): RunResult {
|
|
1005
|
+
this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart })
|
|
1006
|
+
return { text: request.prompt, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request }
|
|
920
1007
|
}
|
|
921
1008
|
|
|
922
1009
|
/**
|
|
@@ -944,9 +1031,8 @@ export class Client {
|
|
|
944
1031
|
}
|
|
945
1032
|
|
|
946
1033
|
// ---- Anthropic-style: POST {baseUrl}/messages ----
|
|
947
|
-
private async
|
|
1034
|
+
private async runAnthropicWithSignal(signal: AbortSignal, prompt: PromptInput, toolkit: Toolkit | undefined, history?: any[]): Promise<RunResult> {
|
|
948
1035
|
const key = resolveKey(this.opts)
|
|
949
|
-
const signal = this.makeSignal(external)
|
|
950
1036
|
const base = this.opts.baseUrl.replace(/\/$/, "")
|
|
951
1037
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`
|
|
952
1038
|
const system = this.system(toolkit)
|
|
@@ -956,15 +1042,20 @@ export class Client {
|
|
|
956
1042
|
const toolCalls: ToolCallRecord[] = []
|
|
957
1043
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
958
1044
|
let turns = 0
|
|
1045
|
+
// The model of the last call this run made (a beforeLLM override when one applied) — §8.
|
|
1046
|
+
let lastModel = this.opts.model
|
|
959
1047
|
|
|
960
1048
|
try {
|
|
961
1049
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
962
1050
|
turns++
|
|
1051
|
+
let turnModel = this.opts.model
|
|
963
1052
|
if (this.opts.hooks?.beforeLLM) {
|
|
964
1053
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn })
|
|
965
1054
|
if (ov?.messages) messages = ov.messages
|
|
966
1055
|
if (ov?.tools) tools = ov.tools
|
|
1056
|
+
if (ov?.model) turnModel = ov.model
|
|
967
1057
|
}
|
|
1058
|
+
lastModel = turnModel
|
|
968
1059
|
const data: any = await this.llmCallJson(endpoint, {
|
|
969
1060
|
method: "POST",
|
|
970
1061
|
headers: {
|
|
@@ -973,15 +1064,15 @@ export class Client {
|
|
|
973
1064
|
"Content-Type": "application/json",
|
|
974
1065
|
...this.opts.headers,
|
|
975
1066
|
},
|
|
976
|
-
body: JSON.stringify(this.anthropicBody(system, messages, tools, false)),
|
|
977
|
-
}, signal, "anthropic")
|
|
1067
|
+
body: JSON.stringify(this.anthropicBody(system, messages, tools, false, turnModel)),
|
|
1068
|
+
}, signal, "anthropic", turnModel)
|
|
978
1069
|
addUsage(usage, data.usage, "anthropic")
|
|
979
|
-
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model:
|
|
1070
|
+
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: data, model: turnModel, turn })
|
|
980
1071
|
messages.push({ role: "assistant", content: data.content })
|
|
981
1072
|
const uses = (data.content ?? []).filter((b: any) => b.type === "tool_use")
|
|
982
1073
|
if (uses.length === 0) {
|
|
983
1074
|
const text = (data.content ?? []).filter((b: any) => b.type === "text").map((b: any) => b.text).join("")
|
|
984
|
-
return this.endRun(runStart, text, messages, toolCalls, turns, usage)
|
|
1075
|
+
return this.endRun(runStart, lastModel, text, messages, toolCalls, turns, usage)
|
|
985
1076
|
}
|
|
986
1077
|
// execute all tool_use blocks in this turn concurrently (true parallel tool calling)
|
|
987
1078
|
const settled = await Promise.all(
|
|
@@ -1008,19 +1099,18 @@ export class Client {
|
|
|
1008
1099
|
if (s.halted) { halted = s.halted; break }
|
|
1009
1100
|
}
|
|
1010
1101
|
messages.push({ role: "user", content })
|
|
1011
|
-
if (halted) return this.pendingRun(runStart, halted, messages, toolCalls, turns, usage)
|
|
1102
|
+
if (halted) return this.pendingRun(runStart, lastModel, halted, messages, toolCalls, turns, usage)
|
|
1012
1103
|
}
|
|
1013
|
-
return this.endRun(runStart, "", messages, toolCalls, turns, usage, true)
|
|
1104
|
+
return this.endRun(runStart, lastModel, "", messages, toolCalls, turns, usage, true)
|
|
1014
1105
|
} catch (e) {
|
|
1015
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
1106
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e)
|
|
1016
1107
|
throw e
|
|
1017
1108
|
}
|
|
1018
1109
|
}
|
|
1019
1110
|
|
|
1020
1111
|
// ---- Streaming: OpenAI-style ----
|
|
1021
|
-
private async *
|
|
1112
|
+
private async *streamOpenAIWithSignal(signal: AbortSignal, prompt: PromptInput, toolkit: Toolkit | undefined, history?: any[]): AsyncGenerator<StreamEvent, void, unknown> {
|
|
1022
1113
|
const key = resolveKey(this.opts)
|
|
1023
|
-
const signal = this.makeSignal(external)
|
|
1024
1114
|
const runStart = Date.now()
|
|
1025
1115
|
let messages: any[] = history && history.length ? [...history] : []
|
|
1026
1116
|
if (!messages.length) {
|
|
@@ -1032,15 +1122,20 @@ export class Client {
|
|
|
1032
1122
|
const toolCalls: ToolCallRecord[] = []
|
|
1033
1123
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1034
1124
|
let turns = 0
|
|
1125
|
+
// The model of the last call this run made (a beforeLLM override when one applied) — §8.
|
|
1126
|
+
let lastModel = this.opts.model
|
|
1035
1127
|
|
|
1036
1128
|
try {
|
|
1037
1129
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
1038
1130
|
turns++
|
|
1131
|
+
let turnModel = this.opts.model
|
|
1039
1132
|
if (this.opts.hooks?.beforeLLM) {
|
|
1040
1133
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn })
|
|
1041
1134
|
if (ov?.messages) messages = ov.messages
|
|
1042
1135
|
if (ov?.tools) tools = ov.tools
|
|
1136
|
+
if (ov?.model) turnModel = ov.model
|
|
1043
1137
|
}
|
|
1138
|
+
lastModel = turnModel
|
|
1044
1139
|
const t0 = Date.now()
|
|
1045
1140
|
const beforeP = usage.promptTokens, beforeC = usage.completionTokens
|
|
1046
1141
|
let content = ""
|
|
@@ -1049,7 +1144,7 @@ export class Client {
|
|
|
1049
1144
|
const res = await this.llmFetch(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
1050
1145
|
method: "POST",
|
|
1051
1146
|
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...this.opts.headers },
|
|
1052
|
-
body: JSON.stringify(this.openaiBody(messages, tools, true)),
|
|
1147
|
+
body: JSON.stringify(this.openaiBody(messages, tools, true, turnModel)),
|
|
1053
1148
|
}, signal)
|
|
1054
1149
|
if (!res.ok || !res.body) throw await llmHttpError(res)
|
|
1055
1150
|
for await (const line of sseLines(res.body)) {
|
|
@@ -1070,17 +1165,17 @@ export class Client {
|
|
|
1070
1165
|
}
|
|
1071
1166
|
}
|
|
1072
1167
|
} catch (e) {
|
|
1073
|
-
this.emit({ event: "llm", model:
|
|
1168
|
+
this.emit({ event: "llm", model: turnModel, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 })
|
|
1074
1169
|
throw e
|
|
1075
1170
|
}
|
|
1076
|
-
this.emit({ event: "llm", model:
|
|
1077
|
-
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model:
|
|
1171
|
+
this.emit({ event: "llm", model: turnModel, status: "ok", ms: Date.now() - t0, promptTokens: usage.promptTokens - beforeP, completionTokens: usage.completionTokens - beforeC })
|
|
1172
|
+
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model: turnModel, turn })
|
|
1078
1173
|
|
|
1079
1174
|
const calls = [...acc.values()]
|
|
1080
1175
|
if (calls.length === 0) {
|
|
1081
1176
|
messages.push({ role: "assistant", content })
|
|
1082
1177
|
yield { type: "usage", usage }
|
|
1083
|
-
yield { type: "done", result: this.endRun(runStart, content, messages, toolCalls, turns, usage) }
|
|
1178
|
+
yield { type: "done", result: this.endRun(runStart, lastModel, content, messages, toolCalls, turns, usage) }
|
|
1084
1179
|
return
|
|
1085
1180
|
}
|
|
1086
1181
|
messages.push({ role: "assistant", content: content || null, tool_calls: calls.map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: c.args } })) })
|
|
@@ -1097,7 +1192,7 @@ export class Client {
|
|
|
1097
1192
|
if (r.halted) {
|
|
1098
1193
|
toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata })
|
|
1099
1194
|
messages.push(toolMessage(c.id, c.name, result))
|
|
1100
|
-
yield { type: "done", result: this.pendingRun(runStart, r.halted, messages, toolCalls, turns, usage) }
|
|
1195
|
+
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) }
|
|
1101
1196
|
return
|
|
1102
1197
|
}
|
|
1103
1198
|
}
|
|
@@ -1106,17 +1201,16 @@ export class Client {
|
|
|
1106
1201
|
yield { type: "tool_result", id: c.id, name: c.name, output: result.output, isError: result.isError }
|
|
1107
1202
|
}
|
|
1108
1203
|
}
|
|
1109
|
-
yield { type: "done", result: this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage, true) }
|
|
1204
|
+
yield { type: "done", result: this.endRun(runStart, lastModel, lastText(messages), messages, toolCalls, turns, usage, true) }
|
|
1110
1205
|
} catch (e) {
|
|
1111
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
1206
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e)
|
|
1112
1207
|
throw e
|
|
1113
1208
|
}
|
|
1114
1209
|
}
|
|
1115
1210
|
|
|
1116
1211
|
// ---- Streaming: Anthropic-style ----
|
|
1117
|
-
private async *
|
|
1212
|
+
private async *streamAnthropicWithSignal(signal: AbortSignal, prompt: PromptInput, toolkit: Toolkit | undefined, history?: any[]): AsyncGenerator<StreamEvent, void, unknown> {
|
|
1118
1213
|
const key = resolveKey(this.opts)
|
|
1119
|
-
const signal = this.makeSignal(external)
|
|
1120
1214
|
const runStart = Date.now()
|
|
1121
1215
|
const base = this.opts.baseUrl.replace(/\/$/, "")
|
|
1122
1216
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`
|
|
@@ -1126,15 +1220,20 @@ export class Client {
|
|
|
1126
1220
|
const toolCalls: ToolCallRecord[] = []
|
|
1127
1221
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1128
1222
|
let turns = 0
|
|
1223
|
+
// The model of the last call this run made (a beforeLLM override when one applied) — §8.
|
|
1224
|
+
let lastModel = this.opts.model
|
|
1129
1225
|
|
|
1130
1226
|
try {
|
|
1131
1227
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
1132
1228
|
turns++
|
|
1229
|
+
let turnModel = this.opts.model
|
|
1133
1230
|
if (this.opts.hooks?.beforeLLM) {
|
|
1134
1231
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn })
|
|
1135
1232
|
if (ov?.messages) messages = ov.messages
|
|
1136
1233
|
if (ov?.tools) tools = ov.tools
|
|
1234
|
+
if (ov?.model) turnModel = ov.model
|
|
1137
1235
|
}
|
|
1236
|
+
lastModel = turnModel
|
|
1138
1237
|
const t0 = Date.now()
|
|
1139
1238
|
const beforeP = usage.promptTokens, beforeC = usage.completionTokens
|
|
1140
1239
|
const blocks = new Map<number, { type: string; text?: string; id?: string; name?: string; json?: string }>()
|
|
@@ -1143,7 +1242,7 @@ export class Client {
|
|
|
1143
1242
|
const res = await this.llmFetch(endpoint, {
|
|
1144
1243
|
method: "POST",
|
|
1145
1244
|
headers: { "x-api-key": key, "anthropic-version": "2023-06-01", "Content-Type": "application/json", ...this.opts.headers },
|
|
1146
|
-
body: JSON.stringify(this.anthropicBody(system, messages, tools, true)),
|
|
1245
|
+
body: JSON.stringify(this.anthropicBody(system, messages, tools, true, turnModel)),
|
|
1147
1246
|
}, signal)
|
|
1148
1247
|
if (!res.ok || !res.body) throw await llmHttpError(res)
|
|
1149
1248
|
for await (const line of sseLines(res.body)) {
|
|
@@ -1159,11 +1258,11 @@ export class Client {
|
|
|
1159
1258
|
} else if (j.type === "message_delta") { stopReason = j.delta?.stop_reason ?? stopReason; addUsage(usage, j.usage, "anthropic") }
|
|
1160
1259
|
}
|
|
1161
1260
|
} catch (e) {
|
|
1162
|
-
this.emit({ event: "llm", model:
|
|
1261
|
+
this.emit({ event: "llm", model: turnModel, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 })
|
|
1163
1262
|
throw e
|
|
1164
1263
|
}
|
|
1165
|
-
this.emit({ event: "llm", model:
|
|
1166
|
-
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model:
|
|
1264
|
+
this.emit({ event: "llm", model: turnModel, status: "ok", ms: Date.now() - t0, promptTokens: usage.promptTokens - beforeP, completionTokens: usage.completionTokens - beforeC })
|
|
1265
|
+
if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model: turnModel, turn })
|
|
1167
1266
|
|
|
1168
1267
|
const content = [...blocks.values()].map((b) => b.type === "tool_use" ? { type: "tool_use", id: b.id, name: b.name, input: safeJson(b.json || "{}") } : { type: "text", text: b.text })
|
|
1169
1268
|
messages.push({ role: "assistant", content })
|
|
@@ -1171,7 +1270,7 @@ export class Client {
|
|
|
1171
1270
|
if (stopReason !== "tool_use" || uses.length === 0) {
|
|
1172
1271
|
const text = content.filter((b: any) => b.type === "text").map((b: any) => b.text).join("")
|
|
1173
1272
|
yield { type: "usage", usage }
|
|
1174
|
-
yield { type: "done", result: this.endRun(runStart, text, messages, toolCalls, turns, usage) }
|
|
1273
|
+
yield { type: "done", result: this.endRun(runStart, lastModel, text, messages, toolCalls, turns, usage) }
|
|
1175
1274
|
return
|
|
1176
1275
|
}
|
|
1177
1276
|
for (const u of uses as any[]) yield { type: "tool_call", id: u.id, name: u.name, args: u.input }
|
|
@@ -1189,7 +1288,7 @@ export class Client {
|
|
|
1189
1288
|
toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata })
|
|
1190
1289
|
results.push(toolResultBlock(u.id, result))
|
|
1191
1290
|
messages.push({ role: "user", content: results })
|
|
1192
|
-
yield { type: "done", result: this.pendingRun(runStart, r.halted, messages, toolCalls, turns, usage) }
|
|
1291
|
+
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) }
|
|
1193
1292
|
return
|
|
1194
1293
|
}
|
|
1195
1294
|
}
|
|
@@ -1199,9 +1298,9 @@ export class Client {
|
|
|
1199
1298
|
}
|
|
1200
1299
|
messages.push({ role: "user", content: results })
|
|
1201
1300
|
}
|
|
1202
|
-
yield { type: "done", result: this.endRun(runStart, "", messages, toolCalls, turns, usage, true) }
|
|
1301
|
+
yield { type: "done", result: this.endRun(runStart, lastModel, "", messages, toolCalls, turns, usage, true) }
|
|
1203
1302
|
} catch (e) {
|
|
1204
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
1303
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e)
|
|
1205
1304
|
throw e
|
|
1206
1305
|
}
|
|
1207
1306
|
}
|
package/src/index.ts
CHANGED
|
@@ -16,3 +16,29 @@ export * from "./toolkit.js"
|
|
|
16
16
|
export * from "./client.js"
|
|
17
17
|
export * from "./classifier.js"
|
|
18
18
|
export * as agents from "./agents/index.js"
|
|
19
|
+
// Simple judgments (§8B, add-judge-adapters). `judge.noul/choice/score` are the NAMED builders
|
|
20
|
+
// (the bare `noul/choice/score` above stay the §8B wire builders); the rest is also top-level.
|
|
21
|
+
export * as judge from "./judge.js"
|
|
22
|
+
// Judge batteries (§8B "Batteries", add-judge-batteries): eight `*Classifier` values + latestUserText.
|
|
23
|
+
export * from "./batteries.js"
|
|
24
|
+
export {
|
|
25
|
+
State,
|
|
26
|
+
context,
|
|
27
|
+
questionMap,
|
|
28
|
+
ask,
|
|
29
|
+
gate,
|
|
30
|
+
decide,
|
|
31
|
+
applyGate,
|
|
32
|
+
applyPolicy,
|
|
33
|
+
answersOf,
|
|
34
|
+
staticClassifier,
|
|
35
|
+
Tape,
|
|
36
|
+
DEFAULT_BANDS,
|
|
37
|
+
type NamedQuestion,
|
|
38
|
+
type Band,
|
|
39
|
+
type Bands,
|
|
40
|
+
type Answers,
|
|
41
|
+
type Rule,
|
|
42
|
+
type GateOutcome,
|
|
43
|
+
type Policy,
|
|
44
|
+
} from "./judge.js"
|