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/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:
|
package/dist/agents/home.js
CHANGED
|
@@ -188,9 +188,11 @@ export function startAgent(a, run, hb) {
|
|
|
188
188
|
});
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
|
|
191
|
+
// keepAlive:false — a heartbeat is housekeeping and must never be the reason
|
|
192
|
+
// a host process stays up.
|
|
193
|
+
cancel = runtime.clock.setTimeout(beat, hb.everyMs, { keepAlive: false }); // self-reschedule → one live timer
|
|
192
194
|
};
|
|
193
|
-
cancel = runtime.clock.setTimeout(beat, hb.everyMs);
|
|
195
|
+
cancel = runtime.clock.setTimeout(beat, hb.everyMs, { keepAlive: false });
|
|
194
196
|
return {
|
|
195
197
|
runtime,
|
|
196
198
|
beats,
|
package/dist/agents/runtime.d.ts
CHANGED
|
@@ -22,10 +22,19 @@ import type { Answer, Request, Tool } from "../types.js";
|
|
|
22
22
|
export interface Clock {
|
|
23
23
|
/** Current time in ms (epoch or virtual). */
|
|
24
24
|
now(): number;
|
|
25
|
-
/** Schedule `fn` after `ms`; returns a cancel function.
|
|
26
|
-
|
|
25
|
+
/** Schedule `fn` after `ms`; returns a cancel function.
|
|
26
|
+
*
|
|
27
|
+
* `keepAlive` defaults to TRUE, and the default is the important half: a timer
|
|
28
|
+
* that cannot hold the event loop open never fires when it is the only thing
|
|
29
|
+
* left to do — which is precisely a deadline whose promise nothing else will
|
|
30
|
+
* settle. Background housekeeping that should never hold a process open (a
|
|
31
|
+
* heartbeat) passes `false` explicitly. A host's own Clock may ignore the
|
|
32
|
+
* option; it then behaves as that host's timers always have. */
|
|
33
|
+
setTimeout(fn: () => void, ms: number, opts?: {
|
|
34
|
+
keepAlive?: boolean;
|
|
35
|
+
}): () => void;
|
|
27
36
|
}
|
|
28
|
-
/** The real clock (default).
|
|
37
|
+
/** The real clock (default). See `Clock.setTimeout` for why `keepAlive` defaults to true. */
|
|
29
38
|
export declare const systemClock: Clock;
|
|
30
39
|
/** §7D hierarchical budget. Carved at spawn (`min(own, parent remaining)`) and
|
|
31
40
|
* live-enforced against the whole ancestor chain before each turn and spawn.
|
package/dist/agents/runtime.js
CHANGED
|
@@ -2,12 +2,13 @@ import { createClient, createInProcessFetch, InMemoryConversationStore, } from "
|
|
|
2
2
|
import { runGated } from "./loop.js";
|
|
3
3
|
import { createToolkit } from "../toolkit.js";
|
|
4
4
|
import { defineTool } from "../native.js";
|
|
5
|
-
/** The real clock (default).
|
|
5
|
+
/** The real clock (default). See `Clock.setTimeout` for why `keepAlive` defaults to true. */
|
|
6
6
|
export const systemClock = {
|
|
7
7
|
now: () => Date.now(),
|
|
8
|
-
setTimeout(fn, ms) {
|
|
8
|
+
setTimeout(fn, ms, opts) {
|
|
9
9
|
const t = setTimeout(fn, ms);
|
|
10
|
-
|
|
10
|
+
if (opts?.keepAlive === false)
|
|
11
|
+
t.unref?.();
|
|
11
12
|
return () => clearTimeout(t);
|
|
12
13
|
},
|
|
13
14
|
};
|
|
@@ -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 {};
|