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/dist/client.d.ts
CHANGED
|
@@ -207,20 +207,24 @@ export declare class InMemoryConversationStore implements ConversationStore {
|
|
|
207
207
|
* Lifecycle hooks around the agent loop. Each may observe, and (where noted)
|
|
208
208
|
* mutate or short-circuit. All may be async.
|
|
209
209
|
*/
|
|
210
|
+
/** The `beforeLLM` hook event. */
|
|
211
|
+
export interface BeforeLLMEvent {
|
|
212
|
+
messages: any[];
|
|
213
|
+
tools: any[];
|
|
214
|
+
model: string;
|
|
215
|
+
turn: number;
|
|
216
|
+
}
|
|
217
|
+
/** What a `beforeLLM` hook may return. `model` (non-empty) applies to that turn only. */
|
|
218
|
+
export interface LLMOverride {
|
|
219
|
+
messages?: any[];
|
|
220
|
+
tools?: any[];
|
|
221
|
+
model?: string | null;
|
|
222
|
+
}
|
|
210
223
|
export interface Hooks {
|
|
211
|
-
/** Before each model call. Return { messages?, tools? } to replace them
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
model: string;
|
|
216
|
-
turn: number;
|
|
217
|
-
}): void | {
|
|
218
|
-
messages?: any[];
|
|
219
|
-
tools?: any[];
|
|
220
|
-
} | Promise<void | {
|
|
221
|
-
messages?: any[];
|
|
222
|
-
tools?: any[];
|
|
223
|
-
}>;
|
|
224
|
+
/** Before each model call. Return { messages?, tools? } to replace them, and { model } to send a
|
|
225
|
+
* different model for THIS turn only (request body + afterLLM event); absent/null/empty keeps the
|
|
226
|
+
* configured model verbatim (SPEC §8, e.g. a ModelRouterClassifier). */
|
|
227
|
+
beforeLLM?(ev: BeforeLLMEvent): void | LLMOverride | Promise<void | LLMOverride>;
|
|
224
228
|
/** After each model call (observe: logging, cost, tracing). */
|
|
225
229
|
afterLLM?(ev: {
|
|
226
230
|
response: any;
|
|
@@ -412,7 +416,17 @@ export declare class Client {
|
|
|
412
416
|
/** §0.10 system message. The toolkit is OPTIONAL — with none there is no skills prompt and
|
|
413
417
|
* the system message is the system prompt alone. This deref was issue #86 (ADR 0023). */
|
|
414
418
|
private system;
|
|
415
|
-
/**
|
|
419
|
+
/** Run-scoped timeout timers, cleared by `releaseSignal` when the run settles. */
|
|
420
|
+
private readonly runTimers;
|
|
421
|
+
/** Drop a run's timeout timer. Idempotent, and safe on a signal that never had one. */
|
|
422
|
+
private releaseSignal;
|
|
423
|
+
/** Build a run-scoped abort signal from the optional run-level timeout + an external signal.
|
|
424
|
+
*
|
|
425
|
+
* The timer is deliberately NOT `unref()`d. An unref'd timer cannot keep the event loop
|
|
426
|
+
* alive, so a run whose only pending work is a request that never settles — an injected
|
|
427
|
+
* `fetch`, an in-process transport, a stalled stream — drains the loop and the timeout never
|
|
428
|
+
* fires at all, which is the one case `timeoutMs` exists for. It is RELEASED instead: the
|
|
429
|
+
* caller clears it however the run leaves, so a finished run holds nothing open. */
|
|
416
430
|
private makeSignal;
|
|
417
431
|
/** fetch with retry + exponential backoff on the default retryable set (429/500/502/503/504/529)
|
|
418
432
|
* plus network errors and anything `retryableStatuses` added, honoring Retry-After; aborts via signal.
|
|
@@ -425,6 +439,10 @@ export declare class Client {
|
|
|
425
439
|
/** One non-streaming LLM call, with an `llm` metric event (ok/error + per-call tokens + ms). */
|
|
426
440
|
private llmCallJson;
|
|
427
441
|
private runOpenAI;
|
|
442
|
+
private runAnthropic;
|
|
443
|
+
private streamOpenAI;
|
|
444
|
+
private streamAnthropic;
|
|
445
|
+
private runOpenAIWithSignal;
|
|
428
446
|
private result;
|
|
429
447
|
/** §10: a run halted because a tool suspended and no `waitFor` was configured. */
|
|
430
448
|
private pendingRun;
|
|
@@ -434,9 +452,9 @@ export declare class Client {
|
|
|
434
452
|
* no `waitFor` is configured (the run should stop and surface the request).
|
|
435
453
|
*/
|
|
436
454
|
private resolvePending;
|
|
437
|
-
private
|
|
438
|
-
private
|
|
439
|
-
private
|
|
455
|
+
private runAnthropicWithSignal;
|
|
456
|
+
private streamOpenAIWithSignal;
|
|
457
|
+
private streamAnthropicWithSignal;
|
|
440
458
|
}
|
|
441
459
|
export declare function createClient(opts: ClientOptions): Client;
|
|
442
460
|
/** One tool call an in-process model asks for. Flat on purpose: the nested
|
package/dist/client.js
CHANGED
|
@@ -254,8 +254,8 @@ export class Client {
|
|
|
254
254
|
}
|
|
255
255
|
/** §8 Gap 5. OpenAI-style body; omits tools/tool_choice when the tool list is empty. Key order
|
|
256
256
|
* matches the pre-change body so a no-options call is byte-identical. */
|
|
257
|
-
openaiBody(messages, tools, stream) {
|
|
258
|
-
const b = { model
|
|
257
|
+
openaiBody(messages, tools, stream, model = this.opts.model) {
|
|
258
|
+
const b = { model, messages: toOpenAIWire(messages, this.wireOpts()) };
|
|
259
259
|
if (tools.length) {
|
|
260
260
|
b.tools = tools;
|
|
261
261
|
b.tool_choice = "auto";
|
|
@@ -267,8 +267,8 @@ export class Client {
|
|
|
267
267
|
return this.finalizeBody(b);
|
|
268
268
|
}
|
|
269
269
|
/** §8 Gap 5. Anthropic-style body; omits tools when the tool list is empty. */
|
|
270
|
-
anthropicBody(system, messages, tools, stream) {
|
|
271
|
-
const b = { model
|
|
270
|
+
anthropicBody(system, messages, tools, stream, model = this.opts.model) {
|
|
271
|
+
const b = { model, max_tokens: 4096, system, messages: toAnthropicWire(messages, this.wireOpts()) };
|
|
272
272
|
if (tools.length)
|
|
273
273
|
b.tools = tools;
|
|
274
274
|
if (stream)
|
|
@@ -290,13 +290,13 @@ export class Client {
|
|
|
290
290
|
}
|
|
291
291
|
/** Emit the terminal `run` metric event and build the RunResult. `exhausted` marks a
|
|
292
292
|
* maxTurns loop exit — with no final text that is a LOUD "incomplete", never "done" (§7D). */
|
|
293
|
-
endRun(runStart, text, messages, toolCalls, turns, usage, exhausted = false) {
|
|
294
|
-
this.emit({ event: "run", model
|
|
295
|
-
return this.result(text, messages, toolCalls, turns, usage, exhausted);
|
|
293
|
+
endRun(runStart, model, text, messages, toolCalls, turns, usage, exhausted = false) {
|
|
294
|
+
this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart });
|
|
295
|
+
return this.result(model, text, messages, toolCalls, turns, usage, exhausted);
|
|
296
296
|
}
|
|
297
297
|
/** Emit a `run` error metric event (once, on a thrown run). */
|
|
298
|
-
emitRunError(runStart, toolCalls, turns, usage, e) {
|
|
299
|
-
this.emit({ event: "run", model
|
|
298
|
+
emitRunError(runStart, model, toolCalls, turns, usage, e) {
|
|
299
|
+
this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart, error: e instanceof Error ? e.message : String(e) });
|
|
300
300
|
}
|
|
301
301
|
/**
|
|
302
302
|
* Run the loop. `ctx` — and `ctx.toolkit` inside it — are OPTIONAL: an absent toolkit is a
|
|
@@ -334,14 +334,17 @@ export class Client {
|
|
|
334
334
|
if (sys && !hasSystemMessage(messages))
|
|
335
335
|
messages = [{ role: "system", content: sys }, ...messages];
|
|
336
336
|
let tools = declared;
|
|
337
|
+
let turnModel = this.opts.model;
|
|
337
338
|
if (this.opts.hooks?.beforeLLM) {
|
|
338
339
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn: 0 });
|
|
339
340
|
if (ov?.messages)
|
|
340
341
|
messages = ov.messages;
|
|
341
342
|
if (ov?.tools)
|
|
342
343
|
tools = ov.tools;
|
|
344
|
+
if (ov?.model)
|
|
345
|
+
turnModel = ov.model;
|
|
343
346
|
}
|
|
344
|
-
const body = { model:
|
|
347
|
+
const body = { model: turnModel, messages };
|
|
345
348
|
if (tools.length)
|
|
346
349
|
body.tools = tools;
|
|
347
350
|
if (req.toolChoice !== undefined)
|
|
@@ -349,14 +352,14 @@ export class Client {
|
|
|
349
352
|
if (req.maxTokens)
|
|
350
353
|
body.max_tokens = req.maxTokens;
|
|
351
354
|
const signal = this.makeSignal(req.signal);
|
|
352
|
-
{
|
|
355
|
+
try {
|
|
353
356
|
const data = await this.llmCallJson(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
354
357
|
method: "POST",
|
|
355
358
|
headers: { "content-type": "application/json", authorization: `Bearer ${key}`, ...this.opts.headers },
|
|
356
359
|
body: JSON.stringify(this.finalizeBody(body)),
|
|
357
|
-
}, signal, "openai");
|
|
360
|
+
}, signal, "openai", turnModel);
|
|
358
361
|
if (this.opts.hooks?.afterLLM)
|
|
359
|
-
await this.opts.hooks.afterLLM({ response: data, model:
|
|
362
|
+
await this.opts.hooks.afterLLM({ response: data, model: turnModel, turn: 0 });
|
|
360
363
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
361
364
|
addUsage(usage, data.usage, "openai");
|
|
362
365
|
const choice = data.choices?.[0];
|
|
@@ -370,10 +373,13 @@ export class Client {
|
|
|
370
373
|
toolCalls,
|
|
371
374
|
finishReason: choice?.finish_reason ?? finishReasonFor(toolCalls.length > 0),
|
|
372
375
|
usage,
|
|
373
|
-
model:
|
|
376
|
+
model: turnModel,
|
|
374
377
|
raw: data,
|
|
375
378
|
};
|
|
376
379
|
}
|
|
380
|
+
finally {
|
|
381
|
+
this.releaseSignal(signal);
|
|
382
|
+
}
|
|
377
383
|
}
|
|
378
384
|
/** Anthropic-style upstream: the real translation. */
|
|
379
385
|
async translateAnthropic(req) {
|
|
@@ -387,15 +393,18 @@ export class Client {
|
|
|
387
393
|
...(req.toolkit ? req.toolkit.toAnthropic() : []),
|
|
388
394
|
...openAIToolsToAnthropic(req.tools),
|
|
389
395
|
];
|
|
396
|
+
let turnModel = this.opts.model;
|
|
390
397
|
if (this.opts.hooks?.beforeLLM) {
|
|
391
398
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn: 0 });
|
|
392
399
|
if (ov?.messages)
|
|
393
400
|
messages = ov.messages;
|
|
394
401
|
if (ov?.tools)
|
|
395
402
|
tools = ov.tools;
|
|
403
|
+
if (ov?.model)
|
|
404
|
+
turnModel = ov.model;
|
|
396
405
|
}
|
|
397
406
|
const body = {
|
|
398
|
-
model:
|
|
407
|
+
model: turnModel,
|
|
399
408
|
max_tokens: req.maxTokens && req.maxTokens > 0 ? req.maxTokens : 4096,
|
|
400
409
|
messages,
|
|
401
410
|
};
|
|
@@ -407,7 +416,7 @@ export class Client {
|
|
|
407
416
|
if (tc)
|
|
408
417
|
body.tool_choice = tc;
|
|
409
418
|
const signal = this.makeSignal(req.signal);
|
|
410
|
-
{
|
|
419
|
+
try {
|
|
411
420
|
const data = await this.llmCallJson(endpoint, {
|
|
412
421
|
method: "POST",
|
|
413
422
|
headers: {
|
|
@@ -417,9 +426,9 @@ export class Client {
|
|
|
417
426
|
...this.opts.headers,
|
|
418
427
|
},
|
|
419
428
|
body: JSON.stringify(this.finalizeBody(body)),
|
|
420
|
-
}, signal, "anthropic");
|
|
429
|
+
}, signal, "anthropic", turnModel);
|
|
421
430
|
if (this.opts.hooks?.afterLLM)
|
|
422
|
-
await this.opts.hooks.afterLLM({ response: data, model:
|
|
431
|
+
await this.opts.hooks.afterLLM({ response: data, model: turnModel, turn: 0 });
|
|
423
432
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
424
433
|
addUsage(usage, data.usage, "anthropic");
|
|
425
434
|
const text = [];
|
|
@@ -440,10 +449,13 @@ export class Client {
|
|
|
440
449
|
toolCalls,
|
|
441
450
|
finishReason: finishReasonFor(toolCalls.length > 0, data.stop_reason),
|
|
442
451
|
usage,
|
|
443
|
-
model:
|
|
452
|
+
model: turnModel,
|
|
444
453
|
raw: data,
|
|
445
454
|
};
|
|
446
455
|
}
|
|
456
|
+
finally {
|
|
457
|
+
this.releaseSignal(signal);
|
|
458
|
+
}
|
|
447
459
|
}
|
|
448
460
|
/**
|
|
449
461
|
* Renders a translate result's tool calls as an OpenAI `tool_calls` array, ready to put
|
|
@@ -508,12 +520,28 @@ export class Client {
|
|
|
508
520
|
system(toolkit) {
|
|
509
521
|
return [this.opts.systemPrompt ?? "", toolkit?.skillsPrompt() ?? ""].filter(Boolean).join("\n\n");
|
|
510
522
|
}
|
|
511
|
-
/**
|
|
523
|
+
/** Run-scoped timeout timers, cleared by `releaseSignal` when the run settles. */
|
|
524
|
+
runTimers = new WeakMap();
|
|
525
|
+
/** Drop a run's timeout timer. Idempotent, and safe on a signal that never had one. */
|
|
526
|
+
releaseSignal(signal) {
|
|
527
|
+
const t = this.runTimers.get(signal);
|
|
528
|
+
if (t !== undefined) {
|
|
529
|
+
clearTimeout(t);
|
|
530
|
+
this.runTimers.delete(signal);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/** Build a run-scoped abort signal from the optional run-level timeout + an external signal.
|
|
534
|
+
*
|
|
535
|
+
* The timer is deliberately NOT `unref()`d. An unref'd timer cannot keep the event loop
|
|
536
|
+
* alive, so a run whose only pending work is a request that never settles — an injected
|
|
537
|
+
* `fetch`, an in-process transport, a stalled stream — drains the loop and the timeout never
|
|
538
|
+
* fires at all, which is the one case `timeoutMs` exists for. It is RELEASED instead: the
|
|
539
|
+
* caller clears it however the run leaves, so a finished run holds nothing open. */
|
|
512
540
|
makeSignal(external) {
|
|
513
541
|
const ctrl = new AbortController();
|
|
514
542
|
if (this.opts.timeoutMs) {
|
|
515
543
|
const t = setTimeout(() => ctrl.abort(new Error(`run timeout after ${this.opts.timeoutMs}ms`)), this.opts.timeoutMs);
|
|
516
|
-
|
|
544
|
+
this.runTimers.set(ctrl.signal, t);
|
|
517
545
|
}
|
|
518
546
|
if (external) {
|
|
519
547
|
if (external.aborted)
|
|
@@ -596,7 +624,7 @@ export class Client {
|
|
|
596
624
|
this.emit({ event: "tool", tool: name, source, isError: req ? false : result.isError, ms: Date.now() - t0, ...(req ? { pending: true } : {}) });
|
|
597
625
|
}
|
|
598
626
|
/** One non-streaming LLM call, with an `llm` metric event (ok/error + per-call tokens + ms). */
|
|
599
|
-
async llmCallJson(url, init, signal, style) {
|
|
627
|
+
async llmCallJson(url, init, signal, style, model) {
|
|
600
628
|
const t0 = Date.now();
|
|
601
629
|
try {
|
|
602
630
|
const res = await this.llmFetch(url, init, signal);
|
|
@@ -604,18 +632,55 @@ export class Client {
|
|
|
604
632
|
throw await llmHttpError(res);
|
|
605
633
|
const data = await res.json();
|
|
606
634
|
const tok = perCall(data.usage, style);
|
|
607
|
-
this.emit({ event: "llm", model
|
|
635
|
+
this.emit({ event: "llm", model, status: "ok", ms: Date.now() - t0, promptTokens: tok.prompt, completionTokens: tok.completion });
|
|
608
636
|
return data;
|
|
609
637
|
}
|
|
610
638
|
catch (e) {
|
|
611
|
-
this.emit({ event: "llm", model
|
|
639
|
+
this.emit({ event: "llm", model, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 });
|
|
612
640
|
throw e;
|
|
613
641
|
}
|
|
614
642
|
}
|
|
615
|
-
//
|
|
643
|
+
// The run-scoped timeout timer is owned by these four wrappers, so it is cleared however the
|
|
644
|
+
// run leaves: return, throw, or a consumer abandoning the stream half-way.
|
|
616
645
|
async runOpenAI(prompt, toolkit, external, history) {
|
|
617
|
-
const key = resolveKey(this.opts);
|
|
618
646
|
const signal = this.makeSignal(external);
|
|
647
|
+
try {
|
|
648
|
+
return await this.runOpenAIWithSignal(signal, prompt, toolkit, history);
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
this.releaseSignal(signal);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async runAnthropic(prompt, toolkit, external, history) {
|
|
655
|
+
const signal = this.makeSignal(external);
|
|
656
|
+
try {
|
|
657
|
+
return await this.runAnthropicWithSignal(signal, prompt, toolkit, history);
|
|
658
|
+
}
|
|
659
|
+
finally {
|
|
660
|
+
this.releaseSignal(signal);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async *streamOpenAI(prompt, toolkit, external, history) {
|
|
664
|
+
const signal = this.makeSignal(external);
|
|
665
|
+
try {
|
|
666
|
+
yield* this.streamOpenAIWithSignal(signal, prompt, toolkit, history);
|
|
667
|
+
}
|
|
668
|
+
finally {
|
|
669
|
+
this.releaseSignal(signal);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async *streamAnthropic(prompt, toolkit, external, history) {
|
|
673
|
+
const signal = this.makeSignal(external);
|
|
674
|
+
try {
|
|
675
|
+
yield* this.streamAnthropicWithSignal(signal, prompt, toolkit, history);
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
this.releaseSignal(signal);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// ---- OpenAI-style: POST {baseUrl}/chat/completions ----
|
|
682
|
+
async runOpenAIWithSignal(signal, prompt, toolkit, history) {
|
|
683
|
+
const key = resolveKey(this.opts);
|
|
619
684
|
const runStart = Date.now();
|
|
620
685
|
let messages = history && history.length ? [...history] : [];
|
|
621
686
|
if (!messages.length) {
|
|
@@ -628,29 +693,35 @@ export class Client {
|
|
|
628
693
|
const toolCalls = [];
|
|
629
694
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
630
695
|
let turns = 0;
|
|
696
|
+
// The model of the last call this run made (a beforeLLM override when one applied) — §8.
|
|
697
|
+
let lastModel = this.opts.model;
|
|
631
698
|
try {
|
|
632
699
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
633
700
|
turns++;
|
|
701
|
+
let turnModel = this.opts.model;
|
|
634
702
|
if (this.opts.hooks?.beforeLLM) {
|
|
635
703
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn });
|
|
636
704
|
if (ov?.messages)
|
|
637
705
|
messages = ov.messages;
|
|
638
706
|
if (ov?.tools)
|
|
639
707
|
tools = ov.tools;
|
|
708
|
+
if (ov?.model)
|
|
709
|
+
turnModel = ov.model;
|
|
640
710
|
}
|
|
711
|
+
lastModel = turnModel;
|
|
641
712
|
const data = await this.llmCallJson(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
642
713
|
method: "POST",
|
|
643
714
|
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...this.opts.headers },
|
|
644
|
-
body: JSON.stringify(this.openaiBody(messages, tools, false)),
|
|
645
|
-
}, signal, "openai");
|
|
715
|
+
body: JSON.stringify(this.openaiBody(messages, tools, false, turnModel)),
|
|
716
|
+
}, signal, "openai", turnModel);
|
|
646
717
|
addUsage(usage, data.usage, "openai");
|
|
647
718
|
if (this.opts.hooks?.afterLLM)
|
|
648
|
-
await this.opts.hooks.afterLLM({ response: data, model:
|
|
719
|
+
await this.opts.hooks.afterLLM({ response: data, model: turnModel, turn });
|
|
649
720
|
const msg = data.choices[0].message;
|
|
650
721
|
messages.push(msg);
|
|
651
722
|
const calls = msg.tool_calls ?? [];
|
|
652
723
|
if (calls.length === 0)
|
|
653
|
-
return this.endRun(runStart, msg.content ?? "", messages, toolCalls, turns, usage);
|
|
724
|
+
return this.endRun(runStart, lastModel, msg.content ?? "", messages, toolCalls, turns, usage);
|
|
654
725
|
// execute all tool calls in this turn concurrently (true parallel tool calling)
|
|
655
726
|
const settled = await Promise.all(calls.map(async (call) => {
|
|
656
727
|
let { args, result } = await this.runTool(toolkit, call.function.name, safeJson(call.function.arguments), call.id, turn);
|
|
@@ -670,27 +741,27 @@ export class Client {
|
|
|
670
741
|
toolCalls.push({ name: s.call.function.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata });
|
|
671
742
|
messages.push(toolMessage(s.call.id, s.call.function.name, s.result));
|
|
672
743
|
if (s.halted)
|
|
673
|
-
return this.pendingRun(runStart, s.halted, messages, toolCalls, turns, usage);
|
|
744
|
+
return this.pendingRun(runStart, lastModel, s.halted, messages, toolCalls, turns, usage);
|
|
674
745
|
}
|
|
675
746
|
}
|
|
676
|
-
return this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage, true);
|
|
747
|
+
return this.endRun(runStart, lastModel, lastText(messages), messages, toolCalls, turns, usage, true);
|
|
677
748
|
}
|
|
678
749
|
catch (e) {
|
|
679
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e);
|
|
750
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e);
|
|
680
751
|
throw e;
|
|
681
752
|
}
|
|
682
753
|
}
|
|
683
|
-
result(text, messages, toolCalls, turns, usage, exhausted = false) {
|
|
754
|
+
result(model, text, messages, toolCalls, turns, usage, exhausted = false) {
|
|
684
755
|
const incomplete = exhausted && text === "";
|
|
685
|
-
const r = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model
|
|
756
|
+
const r = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" };
|
|
686
757
|
if (incomplete)
|
|
687
758
|
r.limit = "maxTurns";
|
|
688
759
|
return r;
|
|
689
760
|
}
|
|
690
761
|
/** §10: a run halted because a tool suspended and no `waitFor` was configured. */
|
|
691
|
-
pendingRun(runStart, request, messages, toolCalls, turns, usage) {
|
|
692
|
-
this.emit({ event: "run", model
|
|
693
|
-
return { text: request.prompt, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model
|
|
762
|
+
pendingRun(runStart, model, request, messages, toolCalls, turns, usage) {
|
|
763
|
+
this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart });
|
|
764
|
+
return { text: request.prompt, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request };
|
|
694
765
|
}
|
|
695
766
|
/**
|
|
696
767
|
* §10: resolve a suspended tool result. Calls `waitFor(request)`, and on `ok` re-executes the
|
|
@@ -718,9 +789,8 @@ export class Client {
|
|
|
718
789
|
return { result };
|
|
719
790
|
}
|
|
720
791
|
// ---- Anthropic-style: POST {baseUrl}/messages ----
|
|
721
|
-
async
|
|
792
|
+
async runAnthropicWithSignal(signal, prompt, toolkit, history) {
|
|
722
793
|
const key = resolveKey(this.opts);
|
|
723
|
-
const signal = this.makeSignal(external);
|
|
724
794
|
const base = this.opts.baseUrl.replace(/\/$/, "");
|
|
725
795
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
|
726
796
|
const system = this.system(toolkit);
|
|
@@ -730,16 +800,22 @@ export class Client {
|
|
|
730
800
|
const toolCalls = [];
|
|
731
801
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
732
802
|
let turns = 0;
|
|
803
|
+
// The model of the last call this run made (a beforeLLM override when one applied) — §8.
|
|
804
|
+
let lastModel = this.opts.model;
|
|
733
805
|
try {
|
|
734
806
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
735
807
|
turns++;
|
|
808
|
+
let turnModel = this.opts.model;
|
|
736
809
|
if (this.opts.hooks?.beforeLLM) {
|
|
737
810
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn });
|
|
738
811
|
if (ov?.messages)
|
|
739
812
|
messages = ov.messages;
|
|
740
813
|
if (ov?.tools)
|
|
741
814
|
tools = ov.tools;
|
|
815
|
+
if (ov?.model)
|
|
816
|
+
turnModel = ov.model;
|
|
742
817
|
}
|
|
818
|
+
lastModel = turnModel;
|
|
743
819
|
const data = await this.llmCallJson(endpoint, {
|
|
744
820
|
method: "POST",
|
|
745
821
|
headers: {
|
|
@@ -748,16 +824,16 @@ export class Client {
|
|
|
748
824
|
"Content-Type": "application/json",
|
|
749
825
|
...this.opts.headers,
|
|
750
826
|
},
|
|
751
|
-
body: JSON.stringify(this.anthropicBody(system, messages, tools, false)),
|
|
752
|
-
}, signal, "anthropic");
|
|
827
|
+
body: JSON.stringify(this.anthropicBody(system, messages, tools, false, turnModel)),
|
|
828
|
+
}, signal, "anthropic", turnModel);
|
|
753
829
|
addUsage(usage, data.usage, "anthropic");
|
|
754
830
|
if (this.opts.hooks?.afterLLM)
|
|
755
|
-
await this.opts.hooks.afterLLM({ response: data, model:
|
|
831
|
+
await this.opts.hooks.afterLLM({ response: data, model: turnModel, turn });
|
|
756
832
|
messages.push({ role: "assistant", content: data.content });
|
|
757
833
|
const uses = (data.content ?? []).filter((b) => b.type === "tool_use");
|
|
758
834
|
if (uses.length === 0) {
|
|
759
835
|
const text = (data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
760
|
-
return this.endRun(runStart, text, messages, toolCalls, turns, usage);
|
|
836
|
+
return this.endRun(runStart, lastModel, text, messages, toolCalls, turns, usage);
|
|
761
837
|
}
|
|
762
838
|
// execute all tool_use blocks in this turn concurrently (true parallel tool calling)
|
|
763
839
|
const settled = await Promise.all(uses.map(async (use) => {
|
|
@@ -786,19 +862,18 @@ export class Client {
|
|
|
786
862
|
}
|
|
787
863
|
messages.push({ role: "user", content });
|
|
788
864
|
if (halted)
|
|
789
|
-
return this.pendingRun(runStart, halted, messages, toolCalls, turns, usage);
|
|
865
|
+
return this.pendingRun(runStart, lastModel, halted, messages, toolCalls, turns, usage);
|
|
790
866
|
}
|
|
791
|
-
return this.endRun(runStart, "", messages, toolCalls, turns, usage, true);
|
|
867
|
+
return this.endRun(runStart, lastModel, "", messages, toolCalls, turns, usage, true);
|
|
792
868
|
}
|
|
793
869
|
catch (e) {
|
|
794
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e);
|
|
870
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e);
|
|
795
871
|
throw e;
|
|
796
872
|
}
|
|
797
873
|
}
|
|
798
874
|
// ---- Streaming: OpenAI-style ----
|
|
799
|
-
async *
|
|
875
|
+
async *streamOpenAIWithSignal(signal, prompt, toolkit, history) {
|
|
800
876
|
const key = resolveKey(this.opts);
|
|
801
|
-
const signal = this.makeSignal(external);
|
|
802
877
|
const runStart = Date.now();
|
|
803
878
|
let messages = history && history.length ? [...history] : [];
|
|
804
879
|
if (!messages.length) {
|
|
@@ -811,16 +886,22 @@ export class Client {
|
|
|
811
886
|
const toolCalls = [];
|
|
812
887
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
813
888
|
let turns = 0;
|
|
889
|
+
// The model of the last call this run made (a beforeLLM override when one applied) — §8.
|
|
890
|
+
let lastModel = this.opts.model;
|
|
814
891
|
try {
|
|
815
892
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
816
893
|
turns++;
|
|
894
|
+
let turnModel = this.opts.model;
|
|
817
895
|
if (this.opts.hooks?.beforeLLM) {
|
|
818
896
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn });
|
|
819
897
|
if (ov?.messages)
|
|
820
898
|
messages = ov.messages;
|
|
821
899
|
if (ov?.tools)
|
|
822
900
|
tools = ov.tools;
|
|
901
|
+
if (ov?.model)
|
|
902
|
+
turnModel = ov.model;
|
|
823
903
|
}
|
|
904
|
+
lastModel = turnModel;
|
|
824
905
|
const t0 = Date.now();
|
|
825
906
|
const beforeP = usage.promptTokens, beforeC = usage.completionTokens;
|
|
826
907
|
let content = "";
|
|
@@ -829,7 +910,7 @@ export class Client {
|
|
|
829
910
|
const res = await this.llmFetch(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
830
911
|
method: "POST",
|
|
831
912
|
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", ...this.opts.headers },
|
|
832
|
-
body: JSON.stringify(this.openaiBody(messages, tools, true)),
|
|
913
|
+
body: JSON.stringify(this.openaiBody(messages, tools, true, turnModel)),
|
|
833
914
|
}, signal);
|
|
834
915
|
if (!res.ok || !res.body)
|
|
835
916
|
throw await llmHttpError(res);
|
|
@@ -861,17 +942,17 @@ export class Client {
|
|
|
861
942
|
}
|
|
862
943
|
}
|
|
863
944
|
catch (e) {
|
|
864
|
-
this.emit({ event: "llm", model:
|
|
945
|
+
this.emit({ event: "llm", model: turnModel, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 });
|
|
865
946
|
throw e;
|
|
866
947
|
}
|
|
867
|
-
this.emit({ event: "llm", model:
|
|
948
|
+
this.emit({ event: "llm", model: turnModel, status: "ok", ms: Date.now() - t0, promptTokens: usage.promptTokens - beforeP, completionTokens: usage.completionTokens - beforeC });
|
|
868
949
|
if (this.opts.hooks?.afterLLM)
|
|
869
|
-
await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model:
|
|
950
|
+
await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model: turnModel, turn });
|
|
870
951
|
const calls = [...acc.values()];
|
|
871
952
|
if (calls.length === 0) {
|
|
872
953
|
messages.push({ role: "assistant", content });
|
|
873
954
|
yield { type: "usage", usage };
|
|
874
|
-
yield { type: "done", result: this.endRun(runStart, content, messages, toolCalls, turns, usage) };
|
|
955
|
+
yield { type: "done", result: this.endRun(runStart, lastModel, content, messages, toolCalls, turns, usage) };
|
|
875
956
|
return;
|
|
876
957
|
}
|
|
877
958
|
messages.push({ role: "assistant", content: content || null, tool_calls: calls.map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: c.args } })) });
|
|
@@ -889,7 +970,7 @@ export class Client {
|
|
|
889
970
|
if (r.halted) {
|
|
890
971
|
toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
891
972
|
messages.push(toolMessage(c.id, c.name, result));
|
|
892
|
-
yield { type: "done", result: this.pendingRun(runStart, r.halted, messages, toolCalls, turns, usage) };
|
|
973
|
+
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
|
|
893
974
|
return;
|
|
894
975
|
}
|
|
895
976
|
}
|
|
@@ -898,17 +979,16 @@ export class Client {
|
|
|
898
979
|
yield { type: "tool_result", id: c.id, name: c.name, output: result.output, isError: result.isError };
|
|
899
980
|
}
|
|
900
981
|
}
|
|
901
|
-
yield { type: "done", result: this.endRun(runStart, lastText(messages), messages, toolCalls, turns, usage, true) };
|
|
982
|
+
yield { type: "done", result: this.endRun(runStart, lastModel, lastText(messages), messages, toolCalls, turns, usage, true) };
|
|
902
983
|
}
|
|
903
984
|
catch (e) {
|
|
904
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e);
|
|
985
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e);
|
|
905
986
|
throw e;
|
|
906
987
|
}
|
|
907
988
|
}
|
|
908
989
|
// ---- Streaming: Anthropic-style ----
|
|
909
|
-
async *
|
|
990
|
+
async *streamAnthropicWithSignal(signal, prompt, toolkit, history) {
|
|
910
991
|
const key = resolveKey(this.opts);
|
|
911
|
-
const signal = this.makeSignal(external);
|
|
912
992
|
const runStart = Date.now();
|
|
913
993
|
const base = this.opts.baseUrl.replace(/\/$/, "");
|
|
914
994
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
|
@@ -918,16 +998,22 @@ export class Client {
|
|
|
918
998
|
const toolCalls = [];
|
|
919
999
|
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
920
1000
|
let turns = 0;
|
|
1001
|
+
// The model of the last call this run made (a beforeLLM override when one applied) — §8.
|
|
1002
|
+
let lastModel = this.opts.model;
|
|
921
1003
|
try {
|
|
922
1004
|
for (let turn = 0; turn < (this.opts.maxTurns ?? 10); turn++) {
|
|
923
1005
|
turns++;
|
|
1006
|
+
let turnModel = this.opts.model;
|
|
924
1007
|
if (this.opts.hooks?.beforeLLM) {
|
|
925
1008
|
const ov = await this.opts.hooks.beforeLLM({ messages, tools, model: this.opts.model, turn });
|
|
926
1009
|
if (ov?.messages)
|
|
927
1010
|
messages = ov.messages;
|
|
928
1011
|
if (ov?.tools)
|
|
929
1012
|
tools = ov.tools;
|
|
1013
|
+
if (ov?.model)
|
|
1014
|
+
turnModel = ov.model;
|
|
930
1015
|
}
|
|
1016
|
+
lastModel = turnModel;
|
|
931
1017
|
const t0 = Date.now();
|
|
932
1018
|
const beforeP = usage.promptTokens, beforeC = usage.completionTokens;
|
|
933
1019
|
const blocks = new Map();
|
|
@@ -936,7 +1022,7 @@ export class Client {
|
|
|
936
1022
|
const res = await this.llmFetch(endpoint, {
|
|
937
1023
|
method: "POST",
|
|
938
1024
|
headers: { "x-api-key": key, "anthropic-version": "2023-06-01", "Content-Type": "application/json", ...this.opts.headers },
|
|
939
|
-
body: JSON.stringify(this.anthropicBody(system, messages, tools, true)),
|
|
1025
|
+
body: JSON.stringify(this.anthropicBody(system, messages, tools, true, turnModel)),
|
|
940
1026
|
}, signal);
|
|
941
1027
|
if (!res.ok || !res.body)
|
|
942
1028
|
throw await llmHttpError(res);
|
|
@@ -966,19 +1052,19 @@ export class Client {
|
|
|
966
1052
|
}
|
|
967
1053
|
}
|
|
968
1054
|
catch (e) {
|
|
969
|
-
this.emit({ event: "llm", model:
|
|
1055
|
+
this.emit({ event: "llm", model: turnModel, status: "error", ms: Date.now() - t0, promptTokens: 0, completionTokens: 0 });
|
|
970
1056
|
throw e;
|
|
971
1057
|
}
|
|
972
|
-
this.emit({ event: "llm", model:
|
|
1058
|
+
this.emit({ event: "llm", model: turnModel, status: "ok", ms: Date.now() - t0, promptTokens: usage.promptTokens - beforeP, completionTokens: usage.completionTokens - beforeC });
|
|
973
1059
|
if (this.opts.hooks?.afterLLM)
|
|
974
|
-
await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model:
|
|
1060
|
+
await this.opts.hooks.afterLLM({ response: { streamed: true, usage }, model: turnModel, turn });
|
|
975
1061
|
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 });
|
|
976
1062
|
messages.push({ role: "assistant", content });
|
|
977
1063
|
const uses = content.filter((b) => b.type === "tool_use");
|
|
978
1064
|
if (stopReason !== "tool_use" || uses.length === 0) {
|
|
979
1065
|
const text = content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
980
1066
|
yield { type: "usage", usage };
|
|
981
|
-
yield { type: "done", result: this.endRun(runStart, text, messages, toolCalls, turns, usage) };
|
|
1067
|
+
yield { type: "done", result: this.endRun(runStart, lastModel, text, messages, toolCalls, turns, usage) };
|
|
982
1068
|
return;
|
|
983
1069
|
}
|
|
984
1070
|
for (const u of uses)
|
|
@@ -997,7 +1083,7 @@ export class Client {
|
|
|
997
1083
|
toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
|
|
998
1084
|
results.push(toolResultBlock(u.id, result));
|
|
999
1085
|
messages.push({ role: "user", content: results });
|
|
1000
|
-
yield { type: "done", result: this.pendingRun(runStart, r.halted, messages, toolCalls, turns, usage) };
|
|
1086
|
+
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
|
|
1001
1087
|
return;
|
|
1002
1088
|
}
|
|
1003
1089
|
}
|
|
@@ -1007,10 +1093,10 @@ export class Client {
|
|
|
1007
1093
|
}
|
|
1008
1094
|
messages.push({ role: "user", content: results });
|
|
1009
1095
|
}
|
|
1010
|
-
yield { type: "done", result: this.endRun(runStart, "", messages, toolCalls, turns, usage, true) };
|
|
1096
|
+
yield { type: "done", result: this.endRun(runStart, lastModel, "", messages, toolCalls, turns, usage, true) };
|
|
1011
1097
|
}
|
|
1012
1098
|
catch (e) {
|
|
1013
|
-
this.emitRunError(runStart, toolCalls, turns, usage, e);
|
|
1099
|
+
this.emitRunError(runStart, lastModel, toolCalls, turns, usage, e);
|
|
1014
1100
|
throw e;
|
|
1015
1101
|
}
|
|
1016
1102
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,3 +16,6 @@ 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
|
+
export * as judge from "./judge.js";
|
|
20
|
+
export * from "./batteries.js";
|
|
21
|
+
export { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, type NamedQuestion, type Band, type Bands, type Answers, type Rule, type GateOutcome, type Policy, } from "./judge.js";
|
package/dist/index.js
CHANGED
|
@@ -16,3 +16,9 @@ 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 { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, } from "./judge.js";
|