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/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
- beforeLLM?(ev: { messages: any[]; tools: any[]; model: string; turn: number }): void | { messages?: any[]; tools?: any[] } | Promise<void | { messages?: any[]; tools?: any[] }>
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: this.opts.model, messages: toOpenAIWire(messages, this.wireOpts()) }
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: this.opts.model, max_tokens: 4096, system, messages: toAnthropicWire(messages, this.wireOpts()) }
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: this.opts.model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart })
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: this.opts.model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart, error: e instanceof Error ? e.message : String(e) })
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: this.opts.model, messages }
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: this.opts.model, turn: 0 })
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: this.opts.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: this.opts.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: this.opts.model, turn: 0 })
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: this.opts.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: this.opts.model, status: "ok", ms: Date.now() - t0, promptTokens: tok.prompt, completionTokens: tok.completion })
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: this.opts.model, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 })
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: this.opts.model, turn })
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: this.opts.model, status: incomplete ? "incomplete" : "done" }
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: this.opts.model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart })
978
- return { text: request.prompt, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model: this.opts.model, status: "pending", pending: request }
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: this.opts.model, turn })
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: this.opts.model, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 })
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: this.opts.model, status: "ok", ms: Date.now() - t0, promptTokens: usage.promptTokens - beforeP, completionTokens: usage.completionTokens - beforeC })
1134
- if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model: this.opts.model, turn })
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: this.opts.model, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 })
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: this.opts.model, status: "ok", ms: Date.now() - t0, promptTokens: usage.promptTokens - beforeP, completionTokens: usage.completionTokens - beforeC })
1222
- if (this.opts.hooks?.afterLLM) await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model: this.opts.model, turn })
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"