toolnexus 0.20.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/batteries.d.ts +223 -0
- package/dist/batteries.js +488 -0
- package/dist/classifier.d.ts +14 -0
- package/dist/classifier.js +34 -1
- package/dist/client.d.ts +17 -13
- package/dist/client.js +85 -55
- 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/batteries.ts +614 -0
- package/src/classifier.ts +36 -1
- package/src/client.ts +98 -55
- 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,13 +580,15 @@ 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
|
|
@@ -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,7 +620,7 @@ 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
|
}
|
|
606
626
|
} finally {
|
|
@@ -622,14 +642,16 @@ export class Client {
|
|
|
622
642
|
...openAIToolsToAnthropic(req.tools),
|
|
623
643
|
]
|
|
624
644
|
|
|
645
|
+
let turnModel = this.opts.model
|
|
625
646
|
if (this.opts.hooks?.beforeLLM) {
|
|
626
647
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn: 0 })
|
|
627
648
|
if (ov?.messages) messages = ov.messages
|
|
628
649
|
if (ov?.tools) tools = ov.tools
|
|
650
|
+
if (ov?.model) turnModel = ov.model
|
|
629
651
|
}
|
|
630
652
|
|
|
631
653
|
const body: Record<string, any> = {
|
|
632
|
-
model:
|
|
654
|
+
model: turnModel,
|
|
633
655
|
max_tokens: req.maxTokens && req.maxTokens > 0 ? req.maxTokens : 4096,
|
|
634
656
|
messages,
|
|
635
657
|
}
|
|
@@ -654,8 +676,9 @@ export class Client {
|
|
|
654
676
|
},
|
|
655
677
|
signal,
|
|
656
678
|
"anthropic",
|
|
679
|
+
turnModel,
|
|
657
680
|
)
|
|
658
|
-
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 })
|
|
659
682
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
660
683
|
addUsage(usage, data.usage, "anthropic")
|
|
661
684
|
const text: string[] = []
|
|
@@ -675,7 +698,7 @@ export class Client {
|
|
|
675
698
|
toolCalls,
|
|
676
699
|
finishReason: finishReasonFor(toolCalls.length > 0, data.stop_reason),
|
|
677
700
|
usage,
|
|
678
|
-
model:
|
|
701
|
+
model: turnModel,
|
|
679
702
|
raw: data,
|
|
680
703
|
}
|
|
681
704
|
} finally {
|
|
@@ -848,17 +871,17 @@ export class Client {
|
|
|
848
871
|
}
|
|
849
872
|
|
|
850
873
|
/** One non-streaming LLM call, with an `llm` metric event (ok/error + per-call tokens + ms). */
|
|
851
|
-
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> {
|
|
852
875
|
const t0 = Date.now()
|
|
853
876
|
try {
|
|
854
877
|
const res = await this.llmFetch(url, init, signal)
|
|
855
878
|
if (!res.ok) throw await llmHttpError(res)
|
|
856
879
|
const data: any = await res.json()
|
|
857
880
|
const tok = perCall(data.usage, style)
|
|
858
|
-
this.emit({ event: "llm", model
|
|
881
|
+
this.emit({ event: "llm", model, status: "ok", ms: Date.now() - t0, promptTokens: tok.prompt, completionTokens: tok.completion })
|
|
859
882
|
return data
|
|
860
883
|
} catch (e) {
|
|
861
|
-
this.emit({ event: "llm", model
|
|
884
|
+
this.emit({ event: "llm", model, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 })
|
|
862
885
|
throw e
|
|
863
886
|
}
|
|
864
887
|
}
|
|
@@ -915,26 +938,31 @@ export class Client {
|
|
|
915
938
|
const toolCalls: ToolCallRecord[] = []
|
|
916
939
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
917
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
|
|
918
943
|
|
|
919
944
|
try {
|
|
920
945
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
921
946
|
turns++
|
|
947
|
+
let turnModel = this.opts.model
|
|
922
948
|
if (this.opts.hooks?.beforeLLM) {
|
|
923
949
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn })
|
|
924
950
|
if (ov?.messages) messages = ov.messages
|
|
925
951
|
if (ov?.tools) tools = ov.tools
|
|
952
|
+
if (ov?.model) turnModel = ov.model
|
|
926
953
|
}
|
|
954
|
+
lastModel = turnModel
|
|
927
955
|
const data: any = await this.llmCallJson(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
928
956
|
method: "POST",
|
|
929
957
|
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...this.opts.headers },
|
|
930
|
-
body: JSON.stringify(this.openaiBody(messages, tools, false)),
|
|
931
|
-
}, signal, "openai")
|
|
958
|
+
body: JSON.stringify(this.openaiBody(messages, tools, false, turnModel)),
|
|
959
|
+
}, signal, "openai", turnModel)
|
|
932
960
|
addUsage(usage, data.usage, "openai")
|
|
933
|
-
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 })
|
|
934
962
|
const msg = data.choices[0].message
|
|
935
963
|
messages.push(msg)
|
|
936
964
|
const calls = msg.tool_calls ?? []
|
|
937
|
-
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)
|
|
938
966
|
// execute all tool calls in this turn concurrently (true parallel tool calling)
|
|
939
967
|
const settled = await Promise.all(
|
|
940
968
|
calls.map(async (call: any) => {
|
|
@@ -955,27 +983,27 @@ export class Client {
|
|
|
955
983
|
for (const s of settled) {
|
|
956
984
|
toolCalls.push({ name: s.call.function.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata })
|
|
957
985
|
messages.push(toolMessage(s.call.id, s.call.function.name, s.result))
|
|
958
|
-
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)
|
|
959
987
|
}
|
|
960
988
|
}
|
|
961
|
-
return this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage, true)
|
|
989
|
+
return this.endRun(runStart, lastModel, lastText(messages), messages, toolCalls, turns, usage, true)
|
|
962
990
|
} catch (e) {
|
|
963
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
991
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e)
|
|
964
992
|
throw e
|
|
965
993
|
}
|
|
966
994
|
}
|
|
967
995
|
|
|
968
|
-
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 {
|
|
969
997
|
const incomplete = exhausted && text === ""
|
|
970
|
-
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" }
|
|
971
999
|
if (incomplete) r.limit = "maxTurns"
|
|
972
1000
|
return r
|
|
973
1001
|
}
|
|
974
1002
|
|
|
975
1003
|
/** §10: a run halted because a tool suspended and no `waitFor` was configured. */
|
|
976
|
-
private pendingRun(runStart: number, request: Request, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage): RunResult {
|
|
977
|
-
this.emit({ event: "run", model
|
|
978
|
-
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 }
|
|
979
1007
|
}
|
|
980
1008
|
|
|
981
1009
|
/**
|
|
@@ -1014,15 +1042,20 @@ export class Client {
|
|
|
1014
1042
|
const toolCalls: ToolCallRecord[] = []
|
|
1015
1043
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1016
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
|
|
1017
1047
|
|
|
1018
1048
|
try {
|
|
1019
1049
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
1020
1050
|
turns++
|
|
1051
|
+
let turnModel = this.opts.model
|
|
1021
1052
|
if (this.opts.hooks?.beforeLLM) {
|
|
1022
1053
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn })
|
|
1023
1054
|
if (ov?.messages) messages = ov.messages
|
|
1024
1055
|
if (ov?.tools) tools = ov.tools
|
|
1056
|
+
if (ov?.model) turnModel = ov.model
|
|
1025
1057
|
}
|
|
1058
|
+
lastModel = turnModel
|
|
1026
1059
|
const data: any = await this.llmCallJson(endpoint, {
|
|
1027
1060
|
method: "POST",
|
|
1028
1061
|
headers: {
|
|
@@ -1031,15 +1064,15 @@ export class Client {
|
|
|
1031
1064
|
"Content-Type": "application/json",
|
|
1032
1065
|
...this.opts.headers,
|
|
1033
1066
|
},
|
|
1034
|
-
body: JSON.stringify(this.anthropicBody(system, messages, tools, false)),
|
|
1035
|
-
}, signal, "anthropic")
|
|
1067
|
+
body: JSON.stringify(this.anthropicBody(system, messages, tools, false, turnModel)),
|
|
1068
|
+
}, signal, "anthropic", turnModel)
|
|
1036
1069
|
addUsage(usage, data.usage, "anthropic")
|
|
1037
|
-
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 })
|
|
1038
1071
|
messages.push({ role: "assistant", content: data.content })
|
|
1039
1072
|
const uses = (data.content ?? []).filter((b: any) => b.type === "tool_use")
|
|
1040
1073
|
if (uses.length === 0) {
|
|
1041
1074
|
const text = (data.content ?? []).filter((b: any) => b.type === "text").map((b: any) => b.text).join("")
|
|
1042
|
-
return this.endRun(runStart, text, messages, toolCalls, turns, usage)
|
|
1075
|
+
return this.endRun(runStart, lastModel, text, messages, toolCalls, turns, usage)
|
|
1043
1076
|
}
|
|
1044
1077
|
// execute all tool_use blocks in this turn concurrently (true parallel tool calling)
|
|
1045
1078
|
const settled = await Promise.all(
|
|
@@ -1066,11 +1099,11 @@ export class Client {
|
|
|
1066
1099
|
if (s.halted) { halted = s.halted; break }
|
|
1067
1100
|
}
|
|
1068
1101
|
messages.push({ role: "user", content })
|
|
1069
|
-
if (halted) return this.pendingRun(runStart, halted, messages, toolCalls, turns, usage)
|
|
1102
|
+
if (halted) return this.pendingRun(runStart, lastModel, halted, messages, toolCalls, turns, usage)
|
|
1070
1103
|
}
|
|
1071
|
-
return this.endRun(runStart, "", messages, toolCalls, turns, usage, true)
|
|
1104
|
+
return this.endRun(runStart, lastModel, "", messages, toolCalls, turns, usage, true)
|
|
1072
1105
|
} catch (e) {
|
|
1073
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
1106
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e)
|
|
1074
1107
|
throw e
|
|
1075
1108
|
}
|
|
1076
1109
|
}
|
|
@@ -1089,15 +1122,20 @@ export class Client {
|
|
|
1089
1122
|
const toolCalls: ToolCallRecord[] = []
|
|
1090
1123
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1091
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
|
|
1092
1127
|
|
|
1093
1128
|
try {
|
|
1094
1129
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
1095
1130
|
turns++
|
|
1131
|
+
let turnModel = this.opts.model
|
|
1096
1132
|
if (this.opts.hooks?.beforeLLM) {
|
|
1097
1133
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn })
|
|
1098
1134
|
if (ov?.messages) messages = ov.messages
|
|
1099
1135
|
if (ov?.tools) tools = ov.tools
|
|
1136
|
+
if (ov?.model) turnModel = ov.model
|
|
1100
1137
|
}
|
|
1138
|
+
lastModel = turnModel
|
|
1101
1139
|
const t0 = Date.now()
|
|
1102
1140
|
const beforeP = usage.promptTokens, beforeC = usage.completionTokens
|
|
1103
1141
|
let content = ""
|
|
@@ -1106,7 +1144,7 @@ export class Client {
|
|
|
1106
1144
|
const res = await this.llmFetch(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
1107
1145
|
method: "POST",
|
|
1108
1146
|
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...this.opts.headers },
|
|
1109
|
-
body: JSON.stringify(this.openaiBody(messages, tools, true)),
|
|
1147
|
+
body: JSON.stringify(this.openaiBody(messages, tools, true, turnModel)),
|
|
1110
1148
|
}, signal)
|
|
1111
1149
|
if (!res.ok || !res.body) throw await llmHttpError(res)
|
|
1112
1150
|
for await (const line of sseLines(res.body)) {
|
|
@@ -1127,17 +1165,17 @@ export class Client {
|
|
|
1127
1165
|
}
|
|
1128
1166
|
}
|
|
1129
1167
|
} catch (e) {
|
|
1130
|
-
this.emit({ event: "llm", model:
|
|
1168
|
+
this.emit({ event: "llm", model: turnModel, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 })
|
|
1131
1169
|
throw e
|
|
1132
1170
|
}
|
|
1133
|
-
this.emit({ event: "llm", model:
|
|
1134
|
-
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 })
|
|
1135
1173
|
|
|
1136
1174
|
const calls = [...acc.values()]
|
|
1137
1175
|
if (calls.length === 0) {
|
|
1138
1176
|
messages.push({ role: "assistant", content })
|
|
1139
1177
|
yield { type: "usage", usage }
|
|
1140
|
-
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) }
|
|
1141
1179
|
return
|
|
1142
1180
|
}
|
|
1143
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 } })) })
|
|
@@ -1154,7 +1192,7 @@ export class Client {
|
|
|
1154
1192
|
if (r.halted) {
|
|
1155
1193
|
toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata })
|
|
1156
1194
|
messages.push(toolMessage(c.id, c.name, result))
|
|
1157
|
-
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) }
|
|
1158
1196
|
return
|
|
1159
1197
|
}
|
|
1160
1198
|
}
|
|
@@ -1163,9 +1201,9 @@ export class Client {
|
|
|
1163
1201
|
yield { type: "tool_result", id: c.id, name: c.name, output: result.output, isError: result.isError }
|
|
1164
1202
|
}
|
|
1165
1203
|
}
|
|
1166
|
-
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) }
|
|
1167
1205
|
} catch (e) {
|
|
1168
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
1206
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e)
|
|
1169
1207
|
throw e
|
|
1170
1208
|
}
|
|
1171
1209
|
}
|
|
@@ -1182,15 +1220,20 @@ export class Client {
|
|
|
1182
1220
|
const toolCalls: ToolCallRecord[] = []
|
|
1183
1221
|
const usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
|
|
1184
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
|
|
1185
1225
|
|
|
1186
1226
|
try {
|
|
1187
1227
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
1188
1228
|
turns++
|
|
1229
|
+
let turnModel = this.opts.model
|
|
1189
1230
|
if (this.opts.hooks?.beforeLLM) {
|
|
1190
1231
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn })
|
|
1191
1232
|
if (ov?.messages) messages = ov.messages
|
|
1192
1233
|
if (ov?.tools) tools = ov.tools
|
|
1234
|
+
if (ov?.model) turnModel = ov.model
|
|
1193
1235
|
}
|
|
1236
|
+
lastModel = turnModel
|
|
1194
1237
|
const t0 = Date.now()
|
|
1195
1238
|
const beforeP = usage.promptTokens, beforeC = usage.completionTokens
|
|
1196
1239
|
const blocks = new Map<number, { type: string; text?: string; id?: string; name?: string; json?: string }>()
|
|
@@ -1199,7 +1242,7 @@ export class Client {
|
|
|
1199
1242
|
const res = await this.llmFetch(endpoint, {
|
|
1200
1243
|
method: "POST",
|
|
1201
1244
|
headers: { "x-api-key": key, "anthropic-version": "2023-06-01", "Content-Type": "application/json", ...this.opts.headers },
|
|
1202
|
-
body: JSON.stringify(this.anthropicBody(system, messages, tools, true)),
|
|
1245
|
+
body: JSON.stringify(this.anthropicBody(system, messages, tools, true, turnModel)),
|
|
1203
1246
|
}, signal)
|
|
1204
1247
|
if (!res.ok || !res.body) throw await llmHttpError(res)
|
|
1205
1248
|
for await (const line of sseLines(res.body)) {
|
|
@@ -1215,11 +1258,11 @@ export class Client {
|
|
|
1215
1258
|
} else if (j.type === "message_delta") { stopReason = j.delta?.stop_reason ?? stopReason; addUsage(usage, j.usage, "anthropic") }
|
|
1216
1259
|
}
|
|
1217
1260
|
} catch (e) {
|
|
1218
|
-
this.emit({ event: "llm", model:
|
|
1261
|
+
this.emit({ event: "llm", model: turnModel, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 })
|
|
1219
1262
|
throw e
|
|
1220
1263
|
}
|
|
1221
|
-
this.emit({ event: "llm", model:
|
|
1222
|
-
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 })
|
|
1223
1266
|
|
|
1224
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 })
|
|
1225
1268
|
messages.push({ role: "assistant", content })
|
|
@@ -1227,7 +1270,7 @@ export class Client {
|
|
|
1227
1270
|
if (stopReason !== "tool_use" || uses.length === 0) {
|
|
1228
1271
|
const text = content.filter((b: any) => b.type === "text").map((b: any) => b.text).join("")
|
|
1229
1272
|
yield { type: "usage", usage }
|
|
1230
|
-
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) }
|
|
1231
1274
|
return
|
|
1232
1275
|
}
|
|
1233
1276
|
for (const u of uses as any[]) yield { type: "tool_call", id: u.id, name: u.name, args: u.input }
|
|
@@ -1245,7 +1288,7 @@ export class Client {
|
|
|
1245
1288
|
toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata })
|
|
1246
1289
|
results.push(toolResultBlock(u.id, result))
|
|
1247
1290
|
messages.push({ role: "user", content: results })
|
|
1248
|
-
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) }
|
|
1249
1292
|
return
|
|
1250
1293
|
}
|
|
1251
1294
|
}
|
|
@@ -1255,9 +1298,9 @@ export class Client {
|
|
|
1255
1298
|
}
|
|
1256
1299
|
messages.push({ role: "user", content: results })
|
|
1257
1300
|
}
|
|
1258
|
-
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) }
|
|
1259
1302
|
} catch (e) {
|
|
1260
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e)
|
|
1303
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e)
|
|
1261
1304
|
throw e
|
|
1262
1305
|
}
|
|
1263
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"
|