toolnexus 0.20.0 → 0.22.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.
Files changed (49) hide show
  1. package/README.md +40 -0
  2. package/dist/agents/agent.d.ts +12 -1
  3. package/dist/agents/agent.js +43 -7
  4. package/dist/agents/compaction.d.ts +19 -0
  5. package/dist/agents/compaction.js +26 -4
  6. package/dist/agents/runtime.d.ts +5 -0
  7. package/dist/agents/runtime.js +13 -2
  8. package/dist/batteries.d.ts +223 -0
  9. package/dist/batteries.js +488 -0
  10. package/dist/builtin.d.ts +16 -0
  11. package/dist/builtin.js +315 -17
  12. package/dist/builtin_extras.d.ts +80 -0
  13. package/dist/builtin_extras.js +248 -0
  14. package/dist/classifier.d.ts +14 -0
  15. package/dist/classifier.js +34 -1
  16. package/dist/client.d.ts +91 -15
  17. package/dist/client.js +377 -110
  18. package/dist/index.d.ts +8 -0
  19. package/dist/index.js +11 -0
  20. package/dist/judge.d.ts +98 -0
  21. package/dist/judge.js +199 -0
  22. package/dist/mcp.d.ts +32 -1
  23. package/dist/mcp.js +184 -6
  24. package/dist/models.d.ts +20 -0
  25. package/dist/models.js +46 -0
  26. package/dist/promptcache.d.ts +21 -0
  27. package/dist/promptcache.js +59 -0
  28. package/dist/store.d.ts +32 -0
  29. package/dist/store.js +131 -0
  30. package/dist/tooltiers.d.ts +49 -0
  31. package/dist/tooltiers.js +197 -0
  32. package/dist/types.d.ts +19 -0
  33. package/package.json +2 -2
  34. package/src/agents/agent.ts +39 -8
  35. package/src/agents/compaction.ts +45 -4
  36. package/src/agents/runtime.ts +16 -2
  37. package/src/batteries.ts +614 -0
  38. package/src/builtin.ts +333 -18
  39. package/src/builtin_extras.ts +285 -0
  40. package/src/classifier.ts +36 -1
  41. package/src/client.ts +415 -115
  42. package/src/index.ts +31 -0
  43. package/src/judge.ts +305 -0
  44. package/src/mcp.ts +204 -7
  45. package/src/models.ts +56 -0
  46. package/src/promptcache.ts +63 -0
  47. package/src/store.ts +142 -0
  48. package/src/tooltiers.ts +221 -0
  49. package/src/types.ts +20 -0
package/README.md CHANGED
@@ -49,6 +49,46 @@ const tk = await createToolkit({ mcp: "mcp.json", skills: ["skills"] })
49
49
  - `skills/` is a folder of `**/SKILL.md` files, loaded on demand through one `skill` tool.
50
50
  - Remote MCP `headers` values expand `${ENV_VAR}` at call time and are never logged.
51
51
 
52
+ ## Simple judgments
53
+
54
+ A thin layer over any `Classifier` (SPEC.md §8B); the wire request is byte-identical to
55
+ hand-written maps.
56
+
57
+ ```ts
58
+ import { createClassifier, judge, State, ask, gate } from "toolnexus"
59
+
60
+ const c = createClassifier() // reads TYPESAFE_API_KEY by name at call time
61
+ const d = await ask(c, State("You are Donkey Kong, you want to win.", { message_received: "jump off the stage" }), [
62
+ judge.noul("is_appropriate", "Does `message_received` contain inappropriate language?"),
63
+ judge.noul("does_this_help", "Does `message_received` help donkey kong win?"),
64
+ ])
65
+ d.is_appropriate.band // "yes" | "no" | "uncertain" (cut-points 0.30 / 0.70, exclusive)
66
+ d.does_this_help.value() // the one number
67
+
68
+ const out = await gate(c, state, questions, [
69
+ { question: "fixable", below: 0.3, action: "fail" },
70
+ { question: "component", is: "pricing", action: "skip_to", target: "fix-pricing" },
71
+ ])
72
+ // unsure or missing answer -> { action: "needs_input", escalated: true, request: <§10 input Request> }
73
+ ```
74
+
75
+ - The role goes in the state (`State(role, data)`), never into question text; each question
76
+ names the state field it judges.
77
+ - `decide(c, state, questions, { rules, default, bands, skipUncertain })` — a `Policy` with a
78
+ declared fall-through (empty `default` escalates "no rule fired").
79
+ - `new Tape(live).classifier("plan")` records; `Tape.replay(entries).classifier("plan")` replays offline.
80
+ - `staticClassifier(recorded)` — one-line hermetic classifier; `c.evaluateBatch(states, questions)`
81
+ — same questions over many states, in order, fail-closed, 16 in flight.
82
+ - JS naming: the named builders live under `judge.` (bare `noul/choice/score` stay the §8B wire
83
+ builders); a choice answer keeps its `choice` string field, so the picked-option method is `pick()`.
84
+ - Batteries (§8B *Batteries*): `ToolGuardClassifier`, `ToolRelevanceClassifier`,
85
+ `SkillRelevanceClassifier`, `ToolResultFilterClassifier`, `IsCompleteClassifier`,
86
+ `ContentGuardClassifier` (all take a required `onError: "open" | "closed"`),
87
+ `AgentRouterClassifier` and the opt-in `ModelRouterClassifier(c, [{ id, description }])`.
88
+ Methods `check` / `select` / `filter` / `pick`; `asHook(next?)` plugs the guard, relevance,
89
+ filter, content and model batteries into `hooks`. A `beforeLLM` hook may return `{ model }` to
90
+ send another model for that turn only.
91
+
52
92
  ## Documentation
53
93
 
54
94
  Everything else — the full surface, with runnable examples — lives on the docs site:
@@ -8,7 +8,7 @@
8
8
  * Everything compiles down to the six host verbs in runtime.ts.
9
9
  */
10
10
  import type { PromptInput } from "../content.js";
11
- import type { Answer, Request, Tool } from "../types.js";
11
+ import type { Answer, Request, Tool, ToolContext } from "../types.js";
12
12
  import { AgentRuntime, type AgentDef, type Budget, type RuntimeOptions, type TaskResult } from "./runtime.js";
13
13
  import { Loop, type Completion, type Guardrail } from "./loop.js";
14
14
  import type { ClientOptions } from "../client.js";
@@ -93,6 +93,8 @@ export declare class Agent {
93
93
  * case the tree stays parked and `result.runtime.resume(answer)` continues it.
94
94
  */
95
95
  run(prompt: PromptInput, opts?: AgentRunOptions): Promise<AgentRunResult>;
96
+ /** `run` that interrupts the child when `signal` aborts. */
97
+ private runSignal;
96
98
  /**
97
99
  * The bridge: an Agent IS a Tool. Drop the result into the classic API's
98
100
  * `extraTools`; the agent's own loop runs in isolation and the classic run
@@ -100,6 +102,15 @@ export declare class Agent {
100
102
  * `{agent, turns, totalTokens}`.
101
103
  */
102
104
  asTool(opts?: AgentRunOptions): Tool;
105
+ /**
106
+ * `asTool` with options built per call from the calling tool's context (SPEC §4C), so the
107
+ * child runs under the parent turn's gates: set `hooks` (the same beforeTool permission gate —
108
+ * it also sees the child's tool calls), `waitFor` (the same ask bridge) and `onMetric`. A
109
+ * `hooks` on the agent's own spec replaces `opts.hooks` for that agent, so leave it unset when
110
+ * the gate must hold. When the calling turn is cancelled (`ctx.signal`), the child is
111
+ * interrupted and the tool returns a `cancelled:` error result.
112
+ */
113
+ asToolWith(per: (ctx?: ToolContext) => AgentRunOptions): Tool;
103
114
  }
104
115
  /** The one new noun: define an agent. */
105
116
  export declare function agent(name: string, spec: AgentSpec): Agent;
@@ -73,6 +73,32 @@ export class Agent {
73
73
  await runtime.close(runtime.root);
74
74
  return { ...r, runtime };
75
75
  }
76
+ /** `run` that interrupts the child when `signal` aborts. */
77
+ async runSignal(prompt, opts, signal) {
78
+ if (!signal)
79
+ return this.run(prompt, opts);
80
+ const reason = () => (signal.reason instanceof Error ? signal.reason.message : String(signal.reason ?? "aborted"));
81
+ if (signal.aborted)
82
+ return { text: reason(), isError: true, turns: 0, totalTokens: 0, cancelled: true };
83
+ const runtime = this.createRuntime(opts);
84
+ const h = runtime.spawn(runtime.root, this.name);
85
+ if (isVerbError(h))
86
+ return { text: h.error, isError: true, turns: 0, totalTokens: 0 };
87
+ const onAbort = () => void runtime.close(runtime.root, { force: true }).catch(() => { });
88
+ signal.addEventListener("abort", onAbort, { once: true });
89
+ try {
90
+ const woke = runtime.wake(h, prompt);
91
+ const r = await runtime.wait(h);
92
+ if (signal.aborted)
93
+ return { text: reason(), isError: true, turns: r.turns, totalTokens: r.totalTokens, cancelled: true };
94
+ if (woke.ok && r.status !== "pending")
95
+ await runtime.close(runtime.root);
96
+ return r;
97
+ }
98
+ finally {
99
+ signal.removeEventListener("abort", onAbort);
100
+ }
101
+ }
76
102
  /**
77
103
  * The bridge: an Agent IS a Tool. Drop the result into the classic API's
78
104
  * `extraTools`; the agent's own loop runs in isolation and the classic run
@@ -80,6 +106,17 @@ export class Agent {
80
106
  * `{agent, turns, totalTokens}`.
81
107
  */
82
108
  asTool(opts = {}) {
109
+ return this.asToolWith(() => opts);
110
+ }
111
+ /**
112
+ * `asTool` with options built per call from the calling tool's context (SPEC §4C), so the
113
+ * child runs under the parent turn's gates: set `hooks` (the same beforeTool permission gate —
114
+ * it also sees the child's tool calls), `waitFor` (the same ask bridge) and `onMetric`. A
115
+ * `hooks` on the agent's own spec replaces `opts.hooks` for that agent, so leave it unset when
116
+ * the gate must hold. When the calling turn is cancelled (`ctx.signal`), the child is
117
+ * interrupted and the tool returns a `cancelled:` error result.
118
+ */
119
+ asToolWith(per) {
83
120
  return defineTool({
84
121
  name: this.name,
85
122
  description: this.spec.does,
@@ -88,13 +125,12 @@ export class Agent {
88
125
  properties: { prompt: { type: "string", description: "The task for this agent" } },
89
126
  required: ["prompt"],
90
127
  },
91
- run: async (args) => {
92
- const r = await this.run(String(args.prompt), opts);
93
- return {
94
- output: r.text,
95
- isError: r.isError,
96
- metadata: { agent: this.name, turns: r.turns, totalTokens: r.totalTokens },
97
- };
128
+ run: async (args, ctx) => {
129
+ const r = await this.runSignal(String(args.prompt), per(ctx), ctx?.signal);
130
+ const metadata = { agent: this.name, turns: r.turns, totalTokens: r.totalTokens };
131
+ if (r.cancelled)
132
+ return { output: `cancelled: ${r.text}`, isError: true, metadata };
133
+ return { output: r.text, isError: r.isError, metadata };
98
134
  },
99
135
  });
100
136
  }
@@ -32,6 +32,25 @@ export interface CompactorOptions {
32
32
  * default. Composes with the memory builtin.
33
33
  */
34
34
  flushToMemory?: boolean;
35
+ /**
36
+ * Called after each compaction with what changed, so a host can tell its UI or log (SPEC §4C).
37
+ * Never called on a no-op. Awaited before the next model call.
38
+ */
39
+ onCompact?: (info: CompactInfo) => void | Promise<void>;
40
+ }
41
+ /** One compaction, for {@link CompactorOptions.onCompact}. Token counts come from `countTokens`. */
42
+ export interface CompactInfo {
43
+ tokensBefore: number;
44
+ tokensAfter: number;
45
+ messagesBefore: number;
46
+ messagesAfter: number;
47
+ /** How many messages were folded into the summary. */
48
+ summarized: number;
49
+ /** The text `summarize` returned (without the header line). */
50
+ summary: string;
51
+ /** The summarized messages, verbatim, so a host can persist them (the loop saves only the
52
+ * compacted transcript). */
53
+ folded: Message[];
35
54
  }
36
55
  /** The event a `beforeLLM` hook receives (§8). */
37
56
  export interface BeforeLLMEvent {
@@ -89,7 +89,8 @@ export function compactor(opts) {
89
89
  const keepTail = opts.keepTail ?? Math.floor(opts.maxTokens / 2);
90
90
  return async (ev) => {
91
91
  const msgs = ev.messages;
92
- if (count(msgs) <= opts.maxTokens)
92
+ const before = count(msgs);
93
+ if (before <= opts.maxTokens)
93
94
  return undefined; // under budget → no-op
94
95
  // Preserve a leading system prompt verbatim (identity / soul / skills — never summarized).
95
96
  const headEnd = msgs[0]?.role === "system" ? 1 : 0;
@@ -97,16 +98,37 @@ export function compactor(opts) {
97
98
  const split = findTailStart(msgs, headEnd, keepTail, count);
98
99
  if (split <= headEnd)
99
100
  return undefined; // nothing safely compactible
100
- const older = msgs.slice(headEnd, split);
101
+ let older = msgs.slice(headEnd, split);
102
+ const tail = msgs.slice(split);
103
+ // The latest user prompt is never summarized away. When the only clean boundary is the first
104
+ // message after the system prompt (one prompt followed by a long agentic run), keep that
105
+ // prompt verbatim and summarize the work after it: [system, prompt, summary].
106
+ let pinned = [];
107
+ if (split === msgs.length && headEnd < msgs.length && isBoundary(msgs[headEnd])) {
108
+ pinned = msgs.slice(headEnd, headEnd + 1);
109
+ older = msgs.slice(headEnd + 1);
110
+ }
101
111
  if (older.length === 0)
102
112
  return undefined;
103
- const tail = msgs.slice(split);
104
113
  const flushNote = opts.flushToMemory
105
114
  ? [{ role: "system", content: FLUSH_TO_MEMORY_REMINDER }]
106
115
  : [];
107
116
  const summary = await opts.summarize(older);
108
117
  const summaryMsg = { role: "system", content: `${SUMMARY_PREFIX}${summary}` };
109
- return { messages: [...system, summaryMsg, ...flushNote, ...tail] };
118
+ const out = [...system, ...pinned, summaryMsg, ...flushNote, ...tail];
119
+ const after = count(out);
120
+ if (after >= before)
121
+ return undefined; // compaction would not shrink the transcript → no-op
122
+ await opts.onCompact?.({
123
+ tokensBefore: before,
124
+ tokensAfter: after,
125
+ messagesBefore: msgs.length,
126
+ messagesAfter: out.length,
127
+ summarized: older.length,
128
+ summary,
129
+ folded: older,
130
+ });
131
+ return { messages: out };
110
132
  };
111
133
  }
112
134
  /**
@@ -293,6 +293,11 @@ export interface RuntimeOptions {
293
293
  /** §8 observability sink applied to EVERY agent's client, unless that agent's def
294
294
  * sets its own `onMetric`. Resolves independently of `hooks`. Forwarded verbatim. */
295
295
  onMetric?: (ev: MetricEvent) => void;
296
+ /** The runtime-level §10 interpreter (SPEC §4C): answers a suspension when no agent on the
297
+ * handle's chain has its own `waitFor`. Absent ⇒ unchanged (the suspension goes durable). A
298
+ * host running an agent as a tool (`asToolWith`) sets it so the child's asks reach the same
299
+ * bridge as the parent turn. */
300
+ waitFor?: (request: Request) => Promise<Answer>;
296
301
  }
297
302
  /**
298
303
  * The §7D agent runtime: six host verbs over a tree of Handles, three loud
@@ -595,12 +595,23 @@ export class AgentRuntime {
595
595
  * escalation traced (nearest interpreter wins; the Request's `data.path` names
596
596
  * the suspended subtree that is asking). */
597
597
  wrapWaitFor(h) {
598
- const waitFor = h.def.waitFor;
598
+ const own = h.def.waitFor;
599
+ let waitFor = own;
600
+ if (!waitFor && this.opts.waitFor) {
601
+ // The runtime-level bridge answers only when NO agent on the chain has its own waitFor;
602
+ // otherwise the ask escalates to that nearest interpreter, as before.
603
+ let a = h.parent;
604
+ while (a && !a.def.waitFor)
605
+ a = a.parent;
606
+ if (!a)
607
+ waitFor = this.opts.waitFor;
608
+ }
599
609
  if (!waitFor)
600
610
  return undefined;
611
+ const answerer = own ? h.id : this.root.id;
601
612
  return async (req) => {
602
613
  const path = Array.isArray(req.data?.path) ? req.data.path.join("/") : h.id;
603
- this.t(`${path}: escalate → ${h.id} answers ("nearest interpreter")`);
614
+ this.t(`${path}: escalate → ${answerer} answers ("nearest interpreter")`);
604
615
  return waitFor(req);
605
616
  };
606
617
  }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Judge batteries (SPEC.md §8B "Batteries", change `add-judge-batteries`, ADR 0035 D3): eight
3
+ * `*Classifier` values built on the simple-judgment layer (`judge.ts`). Each has a standalone
4
+ * method returning a typed verdict and, where a seam exists, `asHook(next?)`. They are advisory:
5
+ * nothing here is a security control. Default role and question text is contract, pinned
6
+ * byte-for-byte by `examples/judge/batteries/`.
7
+ *
8
+ * A classifier error never propagates from a standalone method: the verdict carries `error` (the
9
+ * message) and `calibrated: false`.
10
+ */
11
+ import type { Classifier } from "./classifier.js";
12
+ import { type Bands } from "./judge.js";
13
+ import type { Hooks } from "./client.js";
14
+ /** The host's required decision for a classifier error. */
15
+ export type OnError = "open" | "closed";
16
+ /** Default role sentences (contract). */
17
+ export declare const ROLE_TOOL_GUARD = "You review one tool call an AI agent is about to make and rate how risky it is to run it without a human approving it.";
18
+ export declare const ROLE_TOOL_RELEVANCE = "You decide which tools an AI agent needs for the user's request, so the tools it does not need can be left out.";
19
+ export declare const ROLE_SKILL_RELEVANCE = "You decide which agent skills are relevant to the user's request, so the skills it does not need can be left out.";
20
+ export declare const ROLE_TOOL_RESULT_FILTER = "You decide which parts of a tool's output are relevant to the query, so the irrelevant parts can be dropped.";
21
+ export declare const ROLE_IS_COMPLETE = "You check whether an AI agent's final answer completes the task it was given.";
22
+ export declare const ROLE_AGENT_ROUTER = "You route a task to the agent best suited to do it.";
23
+ export declare const ROLE_CONTENT_GUARD = "You screen text that is about to enter or leave an AI agent.";
24
+ export declare const ROLE_MODEL_ROUTER = "You pick the cheapest model that can handle the user's request well.";
25
+ type BeforeLLMHook = NonNullable<Hooks["beforeLLM"]>;
26
+ type BeforeToolHook = NonNullable<Hooks["beforeTool"]>;
27
+ type AfterToolHook = NonNullable<Hooks["afterTool"]>;
28
+ /** Common options. `bands` defaults to 0.30 / 0.70; `role` overrides the default role sentence. */
29
+ export interface BatteryOptions {
30
+ bands?: Bands;
31
+ role?: string;
32
+ }
33
+ /** Options for a battery whose error outcome is a policy choice: `onError` is REQUIRED. */
34
+ export interface PolicyBatteryOptions extends BatteryOptions {
35
+ onError: OnError;
36
+ }
37
+ /** What ToolGuard rates. */
38
+ export interface GuardedCall {
39
+ name: string;
40
+ arguments?: Record<string, unknown>;
41
+ description?: string;
42
+ }
43
+ export interface ToolGuardOptions extends PolicyBatteryOptions {
44
+ /** Default 1.5. */
45
+ askAt?: number;
46
+ /** Default 2.5. */
47
+ denyAt?: number;
48
+ }
49
+ export interface ToolGuardVerdict {
50
+ action: "allow" | "ask" | "deny";
51
+ /** `low risk` | `medium risk` | `high risk` | `uncertain` | `missing answer` | `classifier error`. */
52
+ reason: string;
53
+ risk: number | null;
54
+ sure: boolean;
55
+ calibrated: boolean;
56
+ error?: string;
57
+ }
58
+ /** The default 4-level risk rubric (contract). */
59
+ export declare const RISK_RUBRIC: readonly string[];
60
+ /** Rates one tool call's risk: allow | ask | deny. Thresholds are compared in code. */
61
+ export declare class ToolGuardClassifier {
62
+ private readonly classifier;
63
+ private readonly opts;
64
+ private readonly onError;
65
+ private readonly askAt;
66
+ private readonly denyAt;
67
+ constructor(classifier: Classifier, opts: ToolGuardOptions);
68
+ check(call: GuardedCall): Promise<ToolGuardVerdict>;
69
+ /**
70
+ * A `beforeTool` hook: allow delegates to `next`; deny short-circuits with an error result; ask
71
+ * short-circuits with a §10 `approval` Request (path B). `next` is not called on deny or ask.
72
+ */
73
+ asHook(next?: BeforeToolHook): BeforeToolHook;
74
+ }
75
+ /** A named, described candidate (a tool or a skill). */
76
+ export interface Item {
77
+ name: string;
78
+ description?: string;
79
+ }
80
+ export interface RelevanceVerdict {
81
+ selected: string[];
82
+ dropped: string[];
83
+ calibrated: boolean;
84
+ error?: string;
85
+ }
86
+ /** Drops the tools the classifier is confident a request does not need. */
87
+ export declare class ToolRelevanceClassifier {
88
+ private readonly r;
89
+ constructor(classifier: Classifier, opts: PolicyBatteryOptions);
90
+ select(prompt: string, tools: readonly Item[]): Promise<RelevanceVerdict>;
91
+ /**
92
+ * A `beforeLLM` hook judging the latest user text: overrides `tools` (the kept provider entries,
93
+ * original order) only when it dropped one. No text or no tools ⇒ plain delegation.
94
+ */
95
+ asHook(next?: BeforeLLMHook): BeforeLLMHook;
96
+ }
97
+ /**
98
+ * Drops the skills the classifier is confident a request does not need. No hook: feed `selected`
99
+ * into the S2 skill allowlist — and never pass an EMPTY `selected` (empty ⇒ all).
100
+ */
101
+ export declare class SkillRelevanceClassifier {
102
+ private readonly r;
103
+ constructor(classifier: Classifier, opts: PolicyBatteryOptions);
104
+ select(prompt: string, skills: readonly Item[]): Promise<RelevanceVerdict>;
105
+ }
106
+ export interface FilterVerdict {
107
+ kept: number[];
108
+ dropped: number[];
109
+ calibrated: boolean;
110
+ error?: string;
111
+ }
112
+ /** Keeps the chunks of a tool's output not confidently irrelevant to the query. */
113
+ export declare class ToolResultFilterClassifier {
114
+ private readonly classifier;
115
+ private readonly opts;
116
+ private readonly onError;
117
+ constructor(classifier: Classifier, opts: PolicyBatteryOptions);
118
+ /** `query` is a string or an object. Indices in order. */
119
+ filter(query: unknown, chunks: readonly string[]): Promise<FilterVerdict>;
120
+ /**
121
+ * An `afterTool` hook: a non-error text result with >= 2 `"\n\n"` chunks (and no non-text
122
+ * parts) keeps only the relevant chunks. `next` sees the filtered result; its override wins.
123
+ */
124
+ asHook(next?: AfterToolHook): AfterToolHook;
125
+ }
126
+ export interface CompleteVerdict {
127
+ complete: boolean;
128
+ p: number | null;
129
+ band: "yes" | "no" | "uncertain";
130
+ calibrated: boolean;
131
+ error?: string;
132
+ }
133
+ /** Whether an agent's final answer completes its task. No hook (ADR 0035). */
134
+ export declare class IsCompleteClassifier {
135
+ private readonly classifier;
136
+ private readonly opts;
137
+ private readonly onError;
138
+ constructor(classifier: Classifier, opts: PolicyBatteryOptions);
139
+ check(task: string, answer: string): Promise<CompleteVerdict>;
140
+ }
141
+ /** An agent, or a group of agents when `agents` is non-empty. */
142
+ export interface AgentNode {
143
+ name: string;
144
+ description: string;
145
+ agents?: AgentNode[];
146
+ }
147
+ export interface AgentVerdict {
148
+ agent: string;
149
+ path: string[];
150
+ sure: boolean;
151
+ probabilities: Record<string, number> | null;
152
+ calibrated: boolean;
153
+ error?: string;
154
+ }
155
+ /** Routes a task through the host's agent tree, one choice per level. No hook: the host dispatches. */
156
+ export declare class AgentRouterClassifier {
157
+ private readonly classifier;
158
+ private readonly opts;
159
+ constructor(classifier: Classifier, opts?: BatteryOptions);
160
+ /** An unsure, missing or failed level returns `fallback`. */
161
+ pick(task: string, agents: readonly AgentNode[], fallback: string): Promise<AgentVerdict>;
162
+ }
163
+ /** One ContentGuard question. */
164
+ export interface Dimension {
165
+ name: string;
166
+ instructions: string;
167
+ }
168
+ /** The default dimensions (contract). */
169
+ export declare const DEFAULT_DIMENSIONS: readonly Dimension[];
170
+ export interface ContentGuardOptions extends PolicyBatteryOptions {
171
+ dimensions?: Dimension[];
172
+ }
173
+ export interface ContentVerdict {
174
+ action: "allow" | "review" | "block";
175
+ flagged: string[];
176
+ uncertain: string[];
177
+ scores: Record<string, number>;
178
+ calibrated: boolean;
179
+ error?: string;
180
+ }
181
+ /** Screens text: any yes ⇒ block; else any uncertain/missing ⇒ review; else allow. */
182
+ export declare class ContentGuardClassifier {
183
+ private readonly classifier;
184
+ private readonly opts;
185
+ private readonly onError;
186
+ private readonly dimensions;
187
+ constructor(classifier: Classifier, opts: ContentGuardOptions);
188
+ check(text: string): Promise<ContentVerdict>;
189
+ /** A `beforeLLM` hook on the latest user text: block throws; allow and review delegate to `next`. */
190
+ asHook(next?: BeforeLLMHook): BeforeLLMHook;
191
+ }
192
+ /** One user-supplied model: an id and a prose description of what it is good for (ADR 0021). */
193
+ export interface ModelOption {
194
+ id: string;
195
+ description: string;
196
+ }
197
+ export interface ModelVerdict {
198
+ model: string;
199
+ routed: boolean;
200
+ sure: boolean;
201
+ probabilities: Record<string, number> | null;
202
+ calibrated: boolean;
203
+ error?: string;
204
+ }
205
+ /**
206
+ * OPT-IN per-query model routing (SPEC §8 "Right-size routing"). `models` is the user's ordered
207
+ * option list; anything but a sure pick falls back.
208
+ */
209
+ export declare class ModelRouterClassifier {
210
+ private readonly classifier;
211
+ private readonly opts;
212
+ private readonly models;
213
+ constructor(classifier: Classifier, models: readonly ModelOption[], opts?: BatteryOptions);
214
+ pick(prompt: string, fallback: string): Promise<ModelVerdict>;
215
+ /** A `beforeLLM` hook: overrides `model` only when routed to a model other than the turn's configured one. */
216
+ asHook(next?: BeforeLLMHook): BeforeLLMHook;
217
+ }
218
+ /**
219
+ * The text of the last user message that has text: string content, or every `{type:"text"}` part
220
+ * joined with `"\n"`. A tool_result-only user message is skipped. `""` when there is none.
221
+ */
222
+ export declare function latestUserText(messages: readonly any[] | undefined): string;
223
+ export {};