@zanii/blackbox 0.4.0 → 0.6.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 +10 -0
- package/dist/analysis/hallucination.js +16 -1
- package/dist/index.d.ts +1 -1
- package/dist/session/index.d.ts +17 -0
- package/dist/session/index.js +41 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,16 @@ verifyAnswerCredential(credential, lines, bodies, packs); // { ok, problems }: t
|
|
|
83
83
|
|
|
84
84
|
The same checks run offline as pure functions: `toolHallucinations`, `groundingFindings`, `factsIn`, `loadReferencePacks`, `answerRisk`, `detectorAccuracy`.
|
|
85
85
|
|
|
86
|
+
## New in 0.6.0
|
|
87
|
+
|
|
88
|
+
- Serverless: a session joined with its id and token in a fresh function (Vercel, Lambda, Cloud Run) numbers its events from the server's `sdk_next`, so none are lost. Join in each invocation and flush before returning.
|
|
89
|
+
- Fewer false alarms: an ISO date-time (`2026-10-03T09:30:00+04:00`) in a tool's arguments is a date, not an invented number, and an MCP call carrying out the model's own tool call isn't flagged a second time for the same value.
|
|
90
|
+
|
|
91
|
+
## New in 0.5.0
|
|
92
|
+
|
|
93
|
+
- Held streams: open a session with `holdStreams: true` (or set `BLACKBOX_HOLD_STREAMS=all` on the gateway) and a risky streamed answer waits for a person, as a non-streamed one does. The client gets keep-alives, then the whole stream, or one error in the provider's own format.
|
|
94
|
+
- `session.checkAnswer(seq?)`: what in an answer would hold it (`{seq, risks, hold}`), for an app that streams by itself.
|
|
95
|
+
|
|
86
96
|
## New in 0.4.0
|
|
87
97
|
|
|
88
98
|
- Hallucination controls: the checks above, reference packs, answer credentials (`answerClaims`, `answerCredential`, `verifyAnswerCredential`), `session.retrieved()`, accuracy from people's labels (`detectorAccuracy`, Wilson bounds), the grounding and judge contracts, `answerRisk` for held answers.
|
|
@@ -114,6 +114,7 @@ export function normaliseValue(s) {
|
|
|
114
114
|
.replace(/[\s\-_./()+:,]/g, "");
|
|
115
115
|
}
|
|
116
116
|
const DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$|^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
|
|
117
|
+
const DATE_TIME = /(?<![0-9])[0-9]{4}-[0-9]{2}-[0-9]{2}(?:[T ][0-9]{2}:[0-9]{2}(?::[0-9]{2}(?:\.[0-9]+)?)?(?:Z|[+-][0-9]{2}:?[0-9]{2})?)?(?![0-9])/g;
|
|
117
118
|
const VALUE_KINDS = [
|
|
118
119
|
["email", /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g],
|
|
119
120
|
["iban", /\b[A-Z]{2}[0-9]{2}(?:[ ]?[A-Z0-9]){11,30}\b/g],
|
|
@@ -124,6 +125,12 @@ const VALUE_KINDS = [
|
|
|
124
125
|
/** The identifying values in a string (Western digits) and where they are, skipping `taken` spans. */
|
|
125
126
|
export function identifierSpans(s, taken = []) {
|
|
126
127
|
const out = [];
|
|
128
|
+
// a date or an ISO 8601 date-time (2026-10-03T00:00:00+04:00) is a date, not part of a number
|
|
129
|
+
for (const m of s.matchAll(DATE_TIME)) {
|
|
130
|
+
const at = m.index ?? 0;
|
|
131
|
+
if (!taken.some(([a, b]) => at < b && at + m[0].length > a))
|
|
132
|
+
taken.push([at, at + m[0].length]);
|
|
133
|
+
}
|
|
127
134
|
for (const [kind, re] of VALUE_KINDS)
|
|
128
135
|
for (const m of s.matchAll(re)) {
|
|
129
136
|
const at = m.index ?? 0;
|
|
@@ -245,9 +252,14 @@ export function toolHallucinations(lines, bodies) {
|
|
|
245
252
|
let request = "";
|
|
246
253
|
let toolText = "";
|
|
247
254
|
let sawModel = false;
|
|
248
|
-
for
|
|
255
|
+
// §8: what the model asked for is where an agent's MCP call got its values; the model's call is
|
|
256
|
+
// checked above, so the MCP call it led to isn't flagged a second time for the same act
|
|
257
|
+
const modelArgs = new Map();
|
|
258
|
+
for (const t of modelCalls) {
|
|
249
259
|
if (!called.has(t.tool))
|
|
250
260
|
called.set(t.tool, t.seq);
|
|
261
|
+
modelArgs.set(t.seq, `${modelArgs.get(t.seq) ?? ""}\n${normaliseValue(canonical(t.args ?? {}))}`);
|
|
262
|
+
}
|
|
251
263
|
for (const e of events) {
|
|
252
264
|
const m = e.meta;
|
|
253
265
|
const text = () => {
|
|
@@ -258,6 +270,9 @@ export function toolHallucinations(lines, bodies) {
|
|
|
258
270
|
sawModel = true;
|
|
259
271
|
request = normaliseValue(text());
|
|
260
272
|
}
|
|
273
|
+
else if (e.kind === "llm.response" && modelArgs.has(e.seq)) {
|
|
274
|
+
toolText += modelArgs.get(e.seq);
|
|
275
|
+
}
|
|
261
276
|
else if (e.kind === "tool.result" && typeof m.server === "string" && isObj(m.tool_defs)) {
|
|
262
277
|
const tools = rpcOf(bodies(e.body_hash))?.result;
|
|
263
278
|
const schemas = new Map();
|
package/dist/index.d.ts
CHANGED
|
@@ -61,7 +61,7 @@ export { cassette, requestKey, type Take } from "./replay/index.ts";
|
|
|
61
61
|
export { ORPHAN_TEXT, repairToolCalls } from "./replay/repair.ts";
|
|
62
62
|
export { checkSearch, type SearchMatch, type SearchQuery, searchEvents } from "./search/index.ts";
|
|
63
63
|
export { type DrainResult, drainSpools } from "./session/drain.ts";
|
|
64
|
-
export { BlackboxSession, checkEgress, type EgressCheck, type FlightPlan, type LlmCallIds, type SessionError, type SessionOptions, type SessionState, type SessionStats, session, stableEventId, } from "./session/index.ts";
|
|
64
|
+
export { type AnswerCheck, BlackboxSession, checkEgress, type EgressCheck, type FlightPlan, type LlmCallIds, type SessionError, type SessionOptions, type SessionState, type SessionStats, session, stableEventId, } from "./session/index.ts";
|
|
65
65
|
export { assembleSla, assessCompliance, buildSlaBody, signSla, slaHash, verifySla, } from "./sla/index.ts";
|
|
66
66
|
export { assembleSuccession, buildSuccessionBody, type Succession, type SuccessionBody, type SuccessionReason, signSuccession, successionHash, verifyLineage, verifySuccession, } from "./succession/index.ts";
|
|
67
67
|
export { type TimestampReport, timestampRequest, verifyTimestamp, } from "./timestamp/index.ts";
|
package/dist/session/index.d.ts
CHANGED
|
@@ -36,6 +36,9 @@ export interface SessionOptions {
|
|
|
36
36
|
authority?: "agent" | "supervised";
|
|
37
37
|
/** spec/api.md: where it runs (`production`, `staging`, …); policy rules can match it. */
|
|
38
38
|
environment?: string;
|
|
39
|
+
/** spec/findings.md §13.1: hold this session's streamed answers until they're checked (`true`),
|
|
40
|
+
* or never (`false`); unset follows the server's BLACKBOX_HOLD_STREAMS. */
|
|
41
|
+
holdStreams?: boolean;
|
|
39
42
|
/** spec/preflight.md: the equipment the run needs. A no-go returns a disabled session (and
|
|
40
43
|
* `stats.lastError` says why); optional items that are down show in `state().degraded`. */
|
|
41
44
|
preflight?: {
|
|
@@ -98,6 +101,16 @@ export interface LandingResult {
|
|
|
98
101
|
/** How many more tries `maxAttempts` allows; at 0, stop and report. */
|
|
99
102
|
attemptsLeft: number;
|
|
100
103
|
}
|
|
104
|
+
/** spec/findings.md §13.1: what would hold an answer, from `checkAnswer()`. */
|
|
105
|
+
export interface AnswerCheck {
|
|
106
|
+
seq: number;
|
|
107
|
+
risks: Array<{
|
|
108
|
+
index: number;
|
|
109
|
+
kind: string;
|
|
110
|
+
status: "contradicted" | "ungrounded";
|
|
111
|
+
}>;
|
|
112
|
+
hold: boolean;
|
|
113
|
+
}
|
|
101
114
|
export interface SessionState {
|
|
102
115
|
session_id: string;
|
|
103
116
|
closed: boolean;
|
|
@@ -333,6 +346,10 @@ export declare class BlackboxSession {
|
|
|
333
346
|
/** Audit S17: the gateway's view of this session (blocked? findings?), or null if it can't be
|
|
334
347
|
* read. Lets an agent react to its own block before its next model call. Never throws. */
|
|
335
348
|
state(): Promise<SessionState | null>;
|
|
349
|
+
/** spec/findings.md §13.1: what in an answer (the latest, or the one at `seq`) would hold it,
|
|
350
|
+
* for an app that streams the answer by itself: `{seq, risks, hold}`. `hold` says whether the
|
|
351
|
+
* server would hold it. `null` when there's no answer yet or the gateway can't be asked. */
|
|
352
|
+
checkAnswer(seq?: number): Promise<AnswerCheck | null>;
|
|
336
353
|
/** spec/approvals.md §2: asks a second person before running one of the agent's own risky tools,
|
|
337
354
|
* and waits (polling) for the answer. Never throws. Only `"approved"` means go ahead; anything
|
|
338
355
|
* else (`rejected`, `timeout`, or `error` when the gateway can't be asked) means don't. */
|
package/dist/session/index.js
CHANGED
|
@@ -58,6 +58,25 @@ export function imageType(image) {
|
|
|
58
58
|
}
|
|
59
59
|
const LOCK_WAIT_MS = 2_000;
|
|
60
60
|
const LOCK_STALE_MS = 10_000;
|
|
61
|
+
const spoolDirOf = (options) => options.spoolDir ?? process.env.BLACKBOX_SPOOL_DIR ?? join(tmpdir(), "zanii-blackbox-spool");
|
|
62
|
+
/** spec/sdk.md §1: a session joined without its spool (a new process, a serverless function)
|
|
63
|
+
* numbers its events from the server's `next`; from 0, they'd be skipped as already recorded. */
|
|
64
|
+
async function startAtServerNext(options, id, token) {
|
|
65
|
+
const dir = spoolDirOf(options);
|
|
66
|
+
if (existsSync(join(dir, `${id}.jsonl`)) || existsSync(join(dir, `${id}.ack`)))
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
const r = await send("GET", options.url, `/v1/sessions/${id}/state`, token);
|
|
70
|
+
const n = r.status === 200 ? r.json.sdk_next : undefined;
|
|
71
|
+
if (typeof n === "number" && Number.isInteger(n) && n > 0) {
|
|
72
|
+
mkdirSync(dir, { recursive: true });
|
|
73
|
+
writeFileSync(join(dir, `${id}.ack`), JSON.stringify({ next: n, offset: 0, gen: 0 }));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// offline: the spool keeps the events, numbered from 0
|
|
78
|
+
}
|
|
79
|
+
}
|
|
61
80
|
/** Opens (or joins) a session. Never throws: on failure it returns a disabled session and logs why. */
|
|
62
81
|
export async function session(options) {
|
|
63
82
|
const log = options.logger ?? (() => { });
|
|
@@ -84,6 +103,7 @@ export async function session(options) {
|
|
|
84
103
|
...(o.tenant ? { tenant: o.tenant } : {}),
|
|
85
104
|
...(o.authority ? { authority: o.authority } : {}),
|
|
86
105
|
...(o.environment ? { environment: o.environment } : {}),
|
|
106
|
+
...(o.holdStreams !== undefined ? { hold_streams: o.holdStreams } : {}),
|
|
87
107
|
...(o.preflight ? { preflight: await withEgress(o.preflight) } : {}),
|
|
88
108
|
...(o.drill ? { drill: o.drill } : {}),
|
|
89
109
|
sdk: true, // this SDK will report the session's model calls (L2.3.2)
|
|
@@ -94,6 +114,7 @@ export async function session(options) {
|
|
|
94
114
|
token = String(r.json.token);
|
|
95
115
|
return new BlackboxSession(options, id, token, true, true);
|
|
96
116
|
}
|
|
117
|
+
await startAtServerNext(options, id, token);
|
|
97
118
|
return new BlackboxSession(options, id, token);
|
|
98
119
|
}
|
|
99
120
|
catch (error) {
|
|
@@ -142,7 +163,7 @@ export class BlackboxSession {
|
|
|
142
163
|
this.id = id;
|
|
143
164
|
this.token = token;
|
|
144
165
|
this.enabled = enabled;
|
|
145
|
-
const dir = options
|
|
166
|
+
const dir = spoolDirOf(options);
|
|
146
167
|
this.spool = join(dir, `${id}.jsonl`);
|
|
147
168
|
this.ackFile = join(dir, `${id}.ack`);
|
|
148
169
|
if (!enabled)
|
|
@@ -521,6 +542,25 @@ export class BlackboxSession {
|
|
|
521
542
|
}
|
|
522
543
|
return null;
|
|
523
544
|
}
|
|
545
|
+
/** spec/findings.md §13.1: what in an answer (the latest, or the one at `seq`) would hold it,
|
|
546
|
+
* for an app that streams the answer by itself: `{seq, risks, hold}`. `hold` says whether the
|
|
547
|
+
* server would hold it. `null` when there's no answer yet or the gateway can't be asked. */
|
|
548
|
+
async checkAnswer(seq) {
|
|
549
|
+
if (!this.enabled)
|
|
550
|
+
return null;
|
|
551
|
+
const at = seq === undefined ? "latest" : String(seq);
|
|
552
|
+
try {
|
|
553
|
+
const r = await send("GET", this.options.url, `/v1/sessions/${this.id}/answers/${at}/risk`, this.token);
|
|
554
|
+
if (r.status === 200)
|
|
555
|
+
return r.json;
|
|
556
|
+
if (r.status !== 404)
|
|
557
|
+
this.fail("rejected", `checking the answer: the gateway answered ${r.status}`);
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
this.fail("network", `checking the answer: ${message(error)}`);
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
524
564
|
/** spec/approvals.md §2: asks a second person before running one of the agent's own risky tools,
|
|
525
565
|
* and waits (polling) for the answer. Never throws. Only `"approved"` means go ahead; anything
|
|
526
566
|
* else (`rejected`, `timeout`, or `error` when the gateway can't be asked) means don't. */
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.6.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// The SDK version, on its own so the CLI can print it without loading the whole SDK.
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.6.0";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zanii/blackbox",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "The flight recorder for AI agents: sessions, a zero-loss spool, framework hooks, approvals, and offline verification of the gateway's hash-chained record.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai-agents",
|