@zanii/blackbox 0.21.2 → 0.23.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 +1 -0
- package/dist/actions/index.d.ts +29 -0
- package/dist/actions/index.js +114 -0
- package/dist/admission/index.d.ts +30 -0
- package/dist/admission/index.js +106 -0
- package/dist/analysis/accepted.d.ts +26 -0
- package/dist/analysis/accepted.js +61 -0
- package/dist/analysis/detectors.js +12 -9
- package/dist/analysis/faults.js +71 -0
- package/dist/analysis/grounding.js +5 -9
- package/dist/analysis/guard.d.ts +72 -0
- package/dist/analysis/guard.js +240 -0
- package/dist/analysis/hallucination.js +2 -2
- package/dist/analysis/index.d.ts +21 -1
- package/dist/analysis/index.js +101 -19
- package/dist/analysis/injection.d.ts +2 -1
- package/dist/analysis/injection.js +16 -16
- package/dist/analysis/landing.js +60 -14
- package/dist/analysis/plan-checks.d.ts +55 -0
- package/dist/analysis/plan-checks.js +748 -0
- package/dist/analysis/taxonomy.js +1 -0
- package/dist/analysis/waste.js +2 -3
- package/dist/approvals/index.d.ts +12 -3
- package/dist/approvals/index.js +36 -10
- package/dist/authority/index.js +3 -3
- package/dist/aviation/index.d.ts +422 -0
- package/dist/aviation/index.js +1014 -0
- package/dist/aviation/qrh.d.ts +4 -0
- package/dist/aviation/qrh.js +171 -0
- package/dist/budgets/index.d.ts +63 -0
- package/dist/budgets/index.js +161 -0
- package/dist/cli.js +199 -24
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.js +26 -2
- package/dist/cost/batch.d.ts +26 -0
- package/dist/cost/batch.js +173 -0
- package/dist/cost/index.d.ts +21 -0
- package/dist/cost/index.js +115 -30
- package/dist/cv/index.js +1 -1
- package/dist/duty/index.js +2 -1
- package/dist/evals/index.d.ts +233 -0
- package/dist/evals/index.js +488 -0
- package/dist/evidence/index.d.ts +9 -0
- package/dist/evidence/index.js +50 -0
- package/dist/federation/index.d.ts +9 -0
- package/dist/federation/index.js +8 -1
- package/dist/finance/index.js +3 -4
- package/dist/fleet/index.d.ts +25 -0
- package/dist/fleet/index.js +57 -17
- package/dist/fleet/insights.js +4 -1
- package/dist/health/index.js +2 -2
- package/dist/hooks/ai-sdk.d.ts +17 -19
- package/dist/hooks/ai-sdk.js +13 -7
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/mastra.d.ts +26 -0
- package/dist/hooks/mastra.js +63 -0
- package/dist/index.d.ts +24 -9
- package/dist/index.js +25 -9
- package/dist/mcp-requests/index.d.ts +26 -0
- package/dist/mcp-requests/index.js +154 -0
- package/dist/mcp-server/index.d.ts +12 -0
- package/dist/mcp-server/index.js +84 -2
- package/dist/mcp-wrap/index.js +8 -4
- package/dist/otlp/index.d.ts +155 -0
- package/dist/otlp/index.js +468 -0
- package/dist/otlp/protobuf.d.ts +7 -0
- package/dist/otlp/protobuf.js +314 -0
- package/dist/packs/index.d.ts +2 -0
- package/dist/packs/index.js +5 -1
- package/dist/pdf/arabic.d.ts +45 -0
- package/dist/pdf/arabic.js +373 -0
- package/dist/pdf/index.d.ts +77 -0
- package/dist/pdf/index.js +673 -0
- package/dist/policy/index.d.ts +0 -4
- package/dist/policy/index.js +13 -5
- package/dist/realtime/index.d.ts +33 -0
- package/dist/realtime/index.js +204 -0
- package/dist/reconcile/record.d.ts +14 -0
- package/dist/reconcile/record.js +161 -66
- package/dist/session/index.d.ts +43 -0
- package/dist/session/index.js +83 -0
- package/dist/undo/index.js +3 -3
- package/dist/verify/anchor.d.ts +5 -1
- package/dist/verify/anchor.js +13 -4
- package/dist/verify/chain.d.ts +4 -0
- package/dist/verify/chain.js +4 -3
- package/dist/verify/envelope.d.ts +1 -1
- package/dist/verify/envelope.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.js +2 -2
- package/dist/warehouse/index.d.ts +62 -0
- package/dist/warehouse/index.js +142 -0
- package/package.json +6 -3
package/dist/policy/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Tool policy (spec/policy.md): rules checked in order, the first match decides. Mirrors policy.py.
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { hasEvidence } from "../data/index.js";
|
|
4
|
-
import { callsOf } from "../reconcile/record.js";
|
|
4
|
+
import { callsOf, envelopesOf } from "../reconcile/record.js";
|
|
5
5
|
import { canonical } from "../reconcile/shared.js";
|
|
6
6
|
import { checkCompensations } from "../undo/index.js";
|
|
7
7
|
/** spec/api.md: a session's environment, e.g. `production`, `staging`, `dev`. */
|
|
@@ -276,8 +276,7 @@ function unproven(lines, bodies, compiled) {
|
|
|
276
276
|
ref: { seq, rule: rule.id, tool },
|
|
277
277
|
detail: `${tool} ran under policy ${rule.id}, and its result carries no witness${rule.field ? ` or ${rule.field}` : ""}.`,
|
|
278
278
|
});
|
|
279
|
-
for (const
|
|
280
|
-
const e = JSON.parse(line);
|
|
279
|
+
for (const e of envelopesOf(lines)) {
|
|
281
280
|
const m = e.meta;
|
|
282
281
|
if (e.kind === "tool.call" && typeof m.tool === "string" && m.via !== "broker") {
|
|
283
282
|
if (m.policy?.action === "deny")
|
|
@@ -325,10 +324,10 @@ function unproven(lines, bodies, compiled) {
|
|
|
325
324
|
* spec/policy.md §2 as findings: POLICY_DENIED for MCP calls the gateway refused (tool.call meta.policy),
|
|
326
325
|
* POLICY_VIOLATION for tools a model response asked for that a deny rule matches.
|
|
327
326
|
*/
|
|
327
|
+
const SERVER_REQUEST_KINDS = new Set(["tool.sampling", "tool.elicitation", "tool.roots"]);
|
|
328
328
|
export function policyFindings(lines, bodies, compiled) {
|
|
329
329
|
const out = [];
|
|
330
|
-
for (const
|
|
331
|
-
const e = JSON.parse(line);
|
|
330
|
+
for (const e of envelopesOf(lines)) {
|
|
332
331
|
const p = e.meta.policy;
|
|
333
332
|
if (e.kind === "tool.call" && p?.action === "deny" && typeof p.rule === "string")
|
|
334
333
|
out.push({
|
|
@@ -338,6 +337,15 @@ export function policyFindings(lines, bodies, compiled) {
|
|
|
338
337
|
ref: { rule: p.rule, seq: e.seq },
|
|
339
338
|
detail: `The gateway refused ${String(e.meta.tool ?? "a tool call")} (policy ${p.rule}).`,
|
|
340
339
|
});
|
|
340
|
+
// spec/mcp.md §6: a server's request (sampling, elicitation, roots) the gateway refused
|
|
341
|
+
if (SERVER_REQUEST_KINDS.has(e.kind) && p?.action === "deny" && typeof p.rule === "string")
|
|
342
|
+
out.push({
|
|
343
|
+
code: "POLICY_DENIED",
|
|
344
|
+
source: "policy",
|
|
345
|
+
severity: "advisory",
|
|
346
|
+
ref: { rule: p.rule, seq: e.seq },
|
|
347
|
+
detail: `The gateway refused the MCP server's ${String(e.meta.method ?? "request")} (policy ${p.rule}).`,
|
|
348
|
+
});
|
|
341
349
|
const hits = e.kind === "tool.call" ? e.meta.policy_audit : undefined;
|
|
342
350
|
for (const a of Array.isArray(hits) ? hits : [])
|
|
343
351
|
if (typeof a.rule === "string" && a.would !== "allow")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Json } from "../verify/envelope.ts";
|
|
2
|
+
type Meta = {
|
|
3
|
+
[key: string]: Json;
|
|
4
|
+
};
|
|
5
|
+
type Obj = Record<string, unknown>;
|
|
6
|
+
/** The realtime protocols: OpenAI Realtime (and Azure's), Gemini Live (and Vertex's), and Bedrock's
|
|
7
|
+
* bidirectional stream (Nova Sonic, spec/providers.md §8). */
|
|
8
|
+
export type RealtimeFormat = "openai" | "gemini" | "bedrock";
|
|
9
|
+
export interface RealtimeFrame {
|
|
10
|
+
/** The event's `meta` (spec/providers.md §7), without `request_seq`. */
|
|
11
|
+
meta: Meta;
|
|
12
|
+
/** Whether the frame's bytes are kept as the event's body. A frame carrying audio, an image or
|
|
13
|
+
* other non-JSON bytes is recorded by size and hash only. */
|
|
14
|
+
keep: boolean;
|
|
15
|
+
/** The session's model after this frame. */
|
|
16
|
+
model: string | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One WebSocket frame of a realtime session (spec/providers.md §7). `binary` says the frame was a
|
|
20
|
+
* binary one (Gemini Live sends its JSON that way); `model` is the session's model so far.
|
|
21
|
+
*/
|
|
22
|
+
export declare function realtimeFrame(format: RealtimeFormat, frame: Uint8Array, binary: boolean, model: string | null): RealtimeFrame;
|
|
23
|
+
/** The model a realtime connection names in its URL: OpenAI's `?model=`, Azure's `?deployment=`. */
|
|
24
|
+
export declare function realtimeModelFromPath(path: string): string | null;
|
|
25
|
+
/** A realtime session's usage reports (spec/providers.md §7), in record order, each with the
|
|
26
|
+
* configured provider its connection went to: what the cost report and the spend check price. */
|
|
27
|
+
export declare function realtimeUsage(lines: readonly string[]): Array<{
|
|
28
|
+
seq: number;
|
|
29
|
+
model: string | null;
|
|
30
|
+
usage: Obj;
|
|
31
|
+
provider: string | null;
|
|
32
|
+
}>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Realtime and voice sessions over WebSocket (spec/providers.md §7): what each frame is recorded
|
|
2
|
+
// with. Pure functions, pinned by spec/vectors/realtime.json. Mirrors
|
|
3
|
+
// sdks/python/src/zanii_blackbox/realtime.py.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { envelopesOf } from "../reconcile/record.js";
|
|
6
|
+
// The top-level keys of a Gemini Live message, client and server, in the order they name a type.
|
|
7
|
+
const GEMINI_TYPES = [
|
|
8
|
+
"setup",
|
|
9
|
+
"clientContent",
|
|
10
|
+
"realtimeInput",
|
|
11
|
+
"toolResponse",
|
|
12
|
+
"setupComplete",
|
|
13
|
+
"serverContent",
|
|
14
|
+
"toolCall",
|
|
15
|
+
"toolCallCancellation",
|
|
16
|
+
"goAway",
|
|
17
|
+
"sessionResumptionUpdate",
|
|
18
|
+
"usageMetadata",
|
|
19
|
+
];
|
|
20
|
+
const B64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
21
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
22
|
+
const str = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max ? v : undefined;
|
|
23
|
+
/** The bytes a base64 media string stands for: decoded when it is valid base64, else its text. */
|
|
24
|
+
function mediaBytes(s) {
|
|
25
|
+
return s.length % 4 === 0 && B64.test(s) ? Buffer.from(s, "base64") : Buffer.from(s, "utf8");
|
|
26
|
+
}
|
|
27
|
+
/** Every media string in a message, in document order: OpenAI's `audio` fields and the `delta` of
|
|
28
|
+
* an `*audio.delta` event; Gemini's blobs (an object with string `data` and `mimeType`). */
|
|
29
|
+
function mediaOf(format, message) {
|
|
30
|
+
const found = [];
|
|
31
|
+
if (format === "bedrock") {
|
|
32
|
+
// spec/providers.md §8: Nova Sonic's audioInput / audioOutput carry base64 `content`
|
|
33
|
+
if (isObj(message.event))
|
|
34
|
+
for (const [name, body] of Object.entries(message.event))
|
|
35
|
+
if (name.startsWith("audio") && isObj(body) && typeof body.content === "string")
|
|
36
|
+
found.push(body.content);
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
const walk = (v, depth) => {
|
|
40
|
+
if (depth > 8)
|
|
41
|
+
return;
|
|
42
|
+
if (Array.isArray(v)) {
|
|
43
|
+
for (const item of v)
|
|
44
|
+
walk(item, depth + 1);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!isObj(v))
|
|
48
|
+
return;
|
|
49
|
+
if (format === "gemini" && typeof v.data === "string" && typeof v.mimeType === "string") {
|
|
50
|
+
found.push(v.data);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const [k, x] of Object.entries(v)) {
|
|
54
|
+
if (format === "openai" && k === "audio" && typeof x === "string")
|
|
55
|
+
found.push(x);
|
|
56
|
+
else
|
|
57
|
+
walk(x, depth + 1);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
if (format === "openai" &&
|
|
61
|
+
typeof message.type === "string" &&
|
|
62
|
+
message.type.endsWith("audio.delta") &&
|
|
63
|
+
typeof message.delta === "string")
|
|
64
|
+
found.push(message.delta);
|
|
65
|
+
walk(message, 0);
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
/** Flattened integer usage (spec/api.md §3); Gemini's per-modality lists become `<name>_AUDIO`. */
|
|
69
|
+
function flatUsage(usage) {
|
|
70
|
+
if (!isObj(usage))
|
|
71
|
+
return undefined;
|
|
72
|
+
const out = {};
|
|
73
|
+
const walk = (o, prefix) => {
|
|
74
|
+
for (const [k, v] of Object.entries(o)) {
|
|
75
|
+
if (typeof v === "number" && Number.isSafeInteger(v))
|
|
76
|
+
out[prefix + k] = v;
|
|
77
|
+
else if (isObj(v))
|
|
78
|
+
walk(v, `${prefix}${k}_`);
|
|
79
|
+
else if (Array.isArray(v))
|
|
80
|
+
for (const item of v)
|
|
81
|
+
if (isObj(item) &&
|
|
82
|
+
typeof item.modality === "string" &&
|
|
83
|
+
/^[A-Z_]{1,32}$/.test(item.modality) &&
|
|
84
|
+
Number.isSafeInteger(item.tokenCount))
|
|
85
|
+
out[`${prefix}${k}_${item.modality}`] = item.tokenCount;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
walk(usage, "");
|
|
89
|
+
return Object.keys(out).length ? out : undefined;
|
|
90
|
+
}
|
|
91
|
+
/** A model name as priced: Gemini's `models/<id>` (or Vertex's full resource name) as `<id>`. */
|
|
92
|
+
function modelName(v) {
|
|
93
|
+
const s = str(v, 256);
|
|
94
|
+
if (s === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
const i = s.lastIndexOf("models/");
|
|
97
|
+
return i === -1 ? s : str(s.slice(i + "models/".length), 256);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* One WebSocket frame of a realtime session (spec/providers.md §7). `binary` says the frame was a
|
|
101
|
+
* binary one (Gemini Live sends its JSON that way); `model` is the session's model so far.
|
|
102
|
+
*/
|
|
103
|
+
export function realtimeFrame(format, frame, binary, model) {
|
|
104
|
+
const bytes = Buffer.from(frame);
|
|
105
|
+
const meta = { bytes: bytes.length };
|
|
106
|
+
if (binary)
|
|
107
|
+
meta.binary = true;
|
|
108
|
+
let message;
|
|
109
|
+
try {
|
|
110
|
+
message = JSON.parse(bytes.toString("utf8"));
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
if (!isObj(message)) {
|
|
114
|
+
if (!binary)
|
|
115
|
+
return { meta, keep: true, model };
|
|
116
|
+
meta.media = {
|
|
117
|
+
bytes: bytes.length,
|
|
118
|
+
sha256: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
|
119
|
+
};
|
|
120
|
+
return { meta, keep: false, model };
|
|
121
|
+
}
|
|
122
|
+
let next = model;
|
|
123
|
+
if (format === "openai") {
|
|
124
|
+
const type = str(message.type, 64);
|
|
125
|
+
if (type)
|
|
126
|
+
meta.type = type;
|
|
127
|
+
if (type === "session.created" || type === "session.updated")
|
|
128
|
+
next = modelName(isObj(message.session) ? message.session.model : undefined) ?? next;
|
|
129
|
+
if (type === "response.done" && isObj(message.response)) {
|
|
130
|
+
const r = message.response;
|
|
131
|
+
const id = str(r.id, 256);
|
|
132
|
+
if (id)
|
|
133
|
+
meta.response_id = id;
|
|
134
|
+
const status = str(r.status, 64);
|
|
135
|
+
if (status)
|
|
136
|
+
meta.response_status = status;
|
|
137
|
+
const usage = flatUsage(r.usage);
|
|
138
|
+
if (usage)
|
|
139
|
+
meta.usage = usage;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (format === "bedrock") {
|
|
143
|
+
// spec/providers.md §8: {"event": {"<name>": {...}}}; usageEvent's delta is this report's
|
|
144
|
+
if (isObj(message.event)) {
|
|
145
|
+
const type = str(Object.keys(message.event)[0], 64);
|
|
146
|
+
if (type)
|
|
147
|
+
meta.type = type;
|
|
148
|
+
const u = message.event.usageEvent;
|
|
149
|
+
const details = isObj(u) ? u.details : undefined;
|
|
150
|
+
const usage = flatUsage(isObj(details) ? details.delta : undefined);
|
|
151
|
+
if (usage)
|
|
152
|
+
meta.usage = usage;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const type = GEMINI_TYPES.find((k) => k in message);
|
|
157
|
+
if (type)
|
|
158
|
+
meta.type = type;
|
|
159
|
+
if (isObj(message.setup))
|
|
160
|
+
next = modelName(message.setup.model) ?? next;
|
|
161
|
+
const usage = flatUsage(message.usageMetadata);
|
|
162
|
+
if (usage)
|
|
163
|
+
meta.usage = usage;
|
|
164
|
+
}
|
|
165
|
+
if (meta.usage !== undefined && next !== null)
|
|
166
|
+
meta.model = next;
|
|
167
|
+
const media = mediaOf(format, message);
|
|
168
|
+
if (media.length === 0)
|
|
169
|
+
return { meta, keep: true, model: next };
|
|
170
|
+
const hash = createHash("sha256");
|
|
171
|
+
let size = 0;
|
|
172
|
+
for (const m of media) {
|
|
173
|
+
const b = mediaBytes(m);
|
|
174
|
+
size += b.length;
|
|
175
|
+
hash.update(b);
|
|
176
|
+
}
|
|
177
|
+
meta.media = { bytes: size, sha256: `sha256:${hash.digest("hex")}` };
|
|
178
|
+
return { meta, keep: false, model: next };
|
|
179
|
+
}
|
|
180
|
+
/** The model a realtime connection names in its URL: OpenAI's `?model=`, Azure's `?deployment=`. */
|
|
181
|
+
export function realtimeModelFromPath(path) {
|
|
182
|
+
const q = new URLSearchParams(path.split("?")[1] ?? "");
|
|
183
|
+
const m = q.get("model") ?? q.get("deployment");
|
|
184
|
+
return m !== null && m.length > 0 && m.length <= 256 ? m : null;
|
|
185
|
+
}
|
|
186
|
+
/** A realtime session's usage reports (spec/providers.md §7), in record order, each with the
|
|
187
|
+
* configured provider its connection went to: what the cost report and the spend check price. */
|
|
188
|
+
export function realtimeUsage(lines) {
|
|
189
|
+
const providers = new Map();
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const e of envelopesOf(lines)) {
|
|
192
|
+
if (e.kind === "llm.request" && typeof e.meta.provider === "string")
|
|
193
|
+
providers.set(e.seq, e.meta.provider);
|
|
194
|
+
if (e.kind !== "llm.realtime.server" || !isObj(e.meta.usage))
|
|
195
|
+
continue;
|
|
196
|
+
out.push({
|
|
197
|
+
seq: e.seq,
|
|
198
|
+
model: typeof e.meta.model === "string" ? e.meta.model : null,
|
|
199
|
+
usage: e.meta.usage,
|
|
200
|
+
provider: providers.get(e.meta.request_seq) ?? null,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Envelope } from "../verify/index.ts";
|
|
1
2
|
export type Obj = Record<string, unknown>;
|
|
2
3
|
export interface Block {
|
|
3
4
|
type: string;
|
|
@@ -35,9 +36,22 @@ export interface Call {
|
|
|
35
36
|
blocks: Map<number, Block>;
|
|
36
37
|
/** OpenAI Responses output items, in order. */
|
|
37
38
|
items: Obj[];
|
|
39
|
+
/** A Chat Completions message's own reasoning (`reasoning_content` or `reasoning`), when given. */
|
|
40
|
+
reasoning?: string;
|
|
38
41
|
}
|
|
39
42
|
export type Bodies = (hash: string) => Uint8Array | undefined;
|
|
40
43
|
export declare function parseJson(text: string): unknown;
|
|
44
|
+
/** Marks an append-only array of lines as one record that only grows (an analysis checkpoint). */
|
|
45
|
+
export declare function trackRecord(lines: readonly string[]): void;
|
|
46
|
+
/** The record's events, parsed: once per line for a tracked record (frozen), else now. */
|
|
47
|
+
export declare function envelopesOf<T = Envelope>(lines: readonly string[]): readonly T[];
|
|
48
|
+
/** A named fold's state on a tracked record (created by `init`); undefined for an untracked one. */
|
|
49
|
+
export declare function foldState<T>(lines: readonly string[], name: string, init: () => T): T | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Findings that each depend on one event alone (and the bodies): on a tracked record each event is
|
|
52
|
+
* judged once, and the findings so far are kept; otherwise every event is judged now.
|
|
53
|
+
*/
|
|
54
|
+
export declare function perEvent<T>(lines: readonly string[], name: string, bodies: Bodies, judge: (e: Envelope) => T[]): T[];
|
|
41
55
|
/** The calls in a verified record, in request order. */
|
|
42
56
|
export declare function callsOf(lines: readonly string[], bodies: Bodies): Call[];
|
|
43
57
|
export interface Receipt {
|
package/dist/reconcile/record.js
CHANGED
|
@@ -10,82 +10,131 @@ export function parseJson(text) {
|
|
|
10
10
|
return undefined;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
const folds = new WeakMap();
|
|
14
|
+
/** Marks an append-only array of lines as one record that only grows (an analysis checkpoint). */
|
|
15
|
+
export function trackRecord(lines) {
|
|
16
|
+
if (!folds.has(lines))
|
|
17
|
+
folds.set(lines, { events: [], more: new Map() });
|
|
18
|
+
}
|
|
19
|
+
function deepFreeze(value) {
|
|
20
|
+
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
21
|
+
Object.freeze(value);
|
|
22
|
+
for (const v of Object.values(value))
|
|
23
|
+
deepFreeze(v);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
/** The record's events, parsed: once per line for a tracked record (frozen), else now. */
|
|
28
|
+
export function envelopesOf(lines) {
|
|
29
|
+
const f = folds.get(lines);
|
|
30
|
+
if (!f)
|
|
31
|
+
return lines.map((l) => JSON.parse(l));
|
|
32
|
+
while (f.events.length < lines.length)
|
|
33
|
+
f.events.push(deepFreeze(JSON.parse(lines[f.events.length])));
|
|
34
|
+
return f.events;
|
|
35
|
+
}
|
|
36
|
+
/** A named fold's state on a tracked record (created by `init`); undefined for an untracked one. */
|
|
37
|
+
export function foldState(lines, name, init) {
|
|
38
|
+
const f = folds.get(lines);
|
|
39
|
+
if (!f)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (!f.more.has(name))
|
|
42
|
+
f.more.set(name, init());
|
|
43
|
+
return f.more.get(name);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Findings that each depend on one event alone (and the bodies): on a tracked record each event is
|
|
47
|
+
* judged once, and the findings so far are kept; otherwise every event is judged now.
|
|
48
|
+
*/
|
|
49
|
+
export function perEvent(lines, name, bodies, judge) {
|
|
50
|
+
const events = envelopesOf(lines);
|
|
51
|
+
const s = foldState(lines, name, () => ({ bodies, done: 0, out: [] }));
|
|
52
|
+
if (!s || s.bodies !== bodies)
|
|
53
|
+
return events.flatMap(judge);
|
|
54
|
+
for (; s.done < events.length; s.done++)
|
|
55
|
+
s.out.push(...judge(events[s.done]));
|
|
56
|
+
return [...s.out];
|
|
57
|
+
}
|
|
13
58
|
/** The calls in a verified record, in request order. */
|
|
14
59
|
export function callsOf(lines, bodies) {
|
|
15
|
-
const events = lines
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
60
|
+
const events = envelopesOf(lines);
|
|
61
|
+
const f = folds.get(lines);
|
|
62
|
+
let s = f?.calls?.bodies === bodies ? f.calls.state : undefined;
|
|
63
|
+
if (!s) {
|
|
64
|
+
s = { done: 0, calls: new Map(), chunks: new Map(), latest: null };
|
|
65
|
+
if (f)
|
|
66
|
+
f.calls = { bodies, state: s };
|
|
67
|
+
}
|
|
68
|
+
for (; s.done < events.length; s.done++)
|
|
69
|
+
stepCalls(s, events[s.done], bodies);
|
|
70
|
+
return [...s.calls.values()];
|
|
71
|
+
}
|
|
72
|
+
function stepCalls(s, e, bodies) {
|
|
73
|
+
const { calls, chunks } = s;
|
|
21
74
|
const text = (hash) => {
|
|
22
75
|
const b = bodies(hash);
|
|
23
76
|
return b === undefined ? "" : decoder.decode(b);
|
|
24
77
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (e.kind !== "llm.chunk" && e.kind !== "llm.response" && e.kind !== "llm.incomplete")
|
|
54
|
-
continue;
|
|
55
|
-
const owner = typeof meta.request_seq === "number" ? meta.request_seq : latest;
|
|
56
|
-
const call = owner === null ? undefined : calls.get(owner);
|
|
57
|
-
if (!call)
|
|
58
|
-
continue;
|
|
59
|
-
if (meta.redacted !== undefined)
|
|
60
|
-
call.redacted = true;
|
|
61
|
-
if (e.kind === "llm.chunk") {
|
|
62
|
-
chunks.get(call.requestSeq)?.push(text(e.body_hash));
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
call.endSeq = e.seq;
|
|
66
|
-
call.complete = e.kind === "llm.response";
|
|
67
|
-
call.status = typeof meta.status === "number" ? meta.status : null;
|
|
68
|
-
if (typeof meta.message_id === "string")
|
|
69
|
-
call.messageId = meta.message_id;
|
|
70
|
-
if (typeof meta.response_id === "string")
|
|
71
|
-
call.responseId = meta.response_id;
|
|
72
|
-
if (typeof meta.model === "string")
|
|
73
|
-
call.model = meta.model;
|
|
74
|
-
if (isObj(meta.usage))
|
|
75
|
-
call.usage = meta.usage;
|
|
76
|
-
if (typeof meta.service_tier === "string")
|
|
77
|
-
call.serviceTier = meta.service_tier;
|
|
78
|
-
if (Number.isSafeInteger(meta.reported_cost_micro_usd))
|
|
79
|
-
call.reportedMicroUsd = meta.reported_cost_micro_usd;
|
|
80
|
-
rebuild(call, (chunks.get(call.requestSeq) ?? []).join(""));
|
|
78
|
+
const meta = e.meta;
|
|
79
|
+
if (e.kind === "llm.request") {
|
|
80
|
+
s.latest = e.seq;
|
|
81
|
+
const request = parseJson(text(e.body_hash));
|
|
82
|
+
calls.set(e.seq, {
|
|
83
|
+
requestSeq: e.seq,
|
|
84
|
+
endSeq: e.seq,
|
|
85
|
+
path: typeof meta.path === "string" ? meta.path : "",
|
|
86
|
+
headers: isObj(meta.headers) ? meta.headers : {},
|
|
87
|
+
request: isObj(request) ? request : null,
|
|
88
|
+
complete: false,
|
|
89
|
+
status: null,
|
|
90
|
+
redacted: meta.redacted !== undefined,
|
|
91
|
+
requestRedacted: meta.redacted !== undefined,
|
|
92
|
+
messageId: null,
|
|
93
|
+
responseId: null,
|
|
94
|
+
model: null,
|
|
95
|
+
usage: null,
|
|
96
|
+
provider: typeof meta.provider === "string" ? meta.provider : null,
|
|
97
|
+
serviceTier: null,
|
|
98
|
+
reportedMicroUsd: null,
|
|
99
|
+
blocks: new Map(),
|
|
100
|
+
items: [],
|
|
101
|
+
});
|
|
102
|
+
chunks.set(e.seq, []);
|
|
103
|
+
return;
|
|
81
104
|
}
|
|
82
|
-
|
|
105
|
+
if (e.kind !== "llm.chunk" && e.kind !== "llm.response" && e.kind !== "llm.incomplete")
|
|
106
|
+
return;
|
|
107
|
+
const owner = typeof meta.request_seq === "number" ? meta.request_seq : s.latest;
|
|
108
|
+
const call = owner === null ? undefined : calls.get(owner);
|
|
109
|
+
if (!call)
|
|
110
|
+
return;
|
|
111
|
+
if (meta.redacted !== undefined)
|
|
112
|
+
call.redacted = true;
|
|
113
|
+
if (e.kind === "llm.chunk") {
|
|
114
|
+
chunks.get(call.requestSeq)?.push(text(e.body_hash));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
call.endSeq = e.seq;
|
|
118
|
+
call.complete = e.kind === "llm.response";
|
|
119
|
+
call.status = typeof meta.status === "number" ? meta.status : null;
|
|
120
|
+
if (typeof meta.message_id === "string")
|
|
121
|
+
call.messageId = meta.message_id;
|
|
122
|
+
if (typeof meta.response_id === "string")
|
|
123
|
+
call.responseId = meta.response_id;
|
|
124
|
+
if (typeof meta.model === "string")
|
|
125
|
+
call.model = meta.model;
|
|
126
|
+
if (isObj(meta.usage))
|
|
127
|
+
call.usage = meta.usage;
|
|
128
|
+
if (typeof meta.service_tier === "string")
|
|
129
|
+
call.serviceTier = meta.service_tier;
|
|
130
|
+
if (Number.isSafeInteger(meta.reported_cost_micro_usd))
|
|
131
|
+
call.reportedMicroUsd = meta.reported_cost_micro_usd;
|
|
132
|
+
rebuild(call, (chunks.get(call.requestSeq) ?? []).join(""));
|
|
83
133
|
}
|
|
84
134
|
/** The workspace receipts in a record (spec/reconcile.md §7), in seq order. */
|
|
85
135
|
export function receiptsOf(lines) {
|
|
86
136
|
const out = [];
|
|
87
|
-
for (const
|
|
88
|
-
const e = JSON.parse(line);
|
|
137
|
+
for (const e of envelopesOf(lines)) {
|
|
89
138
|
if (e.kind !== "workspace.receipt")
|
|
90
139
|
continue;
|
|
91
140
|
const m = e.meta;
|
|
@@ -125,9 +174,13 @@ function rebuild(call, body) {
|
|
|
125
174
|
if (typeof json.id === "string")
|
|
126
175
|
call.messageId ??= json.id;
|
|
127
176
|
const message = json.choices.find(isObj)?.message;
|
|
128
|
-
if (isObj(message))
|
|
177
|
+
if (isObj(message)) {
|
|
129
178
|
chatBlocks(call, message.content, message.tool_calls);
|
|
179
|
+
chatReasoning(call, message);
|
|
180
|
+
}
|
|
130
181
|
}
|
|
182
|
+
if (Array.isArray(json.candidates))
|
|
183
|
+
geminiParts(call, json);
|
|
131
184
|
return;
|
|
132
185
|
}
|
|
133
186
|
const partial = new Map();
|
|
@@ -141,6 +194,10 @@ function rebuild(call, body) {
|
|
|
141
194
|
if (!isObj(event))
|
|
142
195
|
continue;
|
|
143
196
|
const type = event.type;
|
|
197
|
+
if (Array.isArray(event.candidates)) {
|
|
198
|
+
geminiParts(call, event);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
144
201
|
if (Array.isArray(event.choices)) {
|
|
145
202
|
if (typeof event.id === "string")
|
|
146
203
|
call.messageId ??= event.id;
|
|
@@ -149,6 +206,7 @@ function rebuild(call, body) {
|
|
|
149
206
|
continue;
|
|
150
207
|
if (typeof delta.content === "string")
|
|
151
208
|
chatText = (chatText ?? "") + delta.content;
|
|
209
|
+
chatReasoning(call, delta);
|
|
152
210
|
for (const tc of Array.isArray(delta.tool_calls) ? delta.tool_calls.filter(isObj) : []) {
|
|
153
211
|
const k = typeof tc.index === "number" ? tc.index : 0;
|
|
154
212
|
const acc = chatCalls.get(k) ?? { arguments: "" };
|
|
@@ -210,6 +268,43 @@ function rebuild(call, body) {
|
|
|
210
268
|
* its text and tool calls: a text block (when there's text), then one tool_use per tool call, its
|
|
211
269
|
* arguments parsed from their JSON string.
|
|
212
270
|
*/
|
|
271
|
+
/** spec/reconcile.md §2: a Chat Completions message's (or a stream delta's) own reasoning. */
|
|
272
|
+
function chatReasoning(call, m) {
|
|
273
|
+
for (const key of ["reasoning_content", "reasoning"]) {
|
|
274
|
+
const v = m[key];
|
|
275
|
+
if (typeof v === "string" && v) {
|
|
276
|
+
call.reasoning = (call.reasoning ?? "") + v;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** spec/reconcile.md §2: Gemini's parts (a whole answer, or one streamed chunk) as blocks: its
|
|
282
|
+
* thoughts as thinking, its text as text, each functionCall as a tool_use. */
|
|
283
|
+
function geminiParts(call, event) {
|
|
284
|
+
if (typeof event.responseId === "string")
|
|
285
|
+
call.messageId ??= event.responseId;
|
|
286
|
+
const first = event.candidates.find(isObj) ?? {};
|
|
287
|
+
const content = first.content;
|
|
288
|
+
const parts = isObj(content) && Array.isArray(content.parts) ? content.parts.filter(isObj) : [];
|
|
289
|
+
for (const p of parts) {
|
|
290
|
+
const fc = p.functionCall;
|
|
291
|
+
if (isObj(fc) && typeof fc.name === "string") {
|
|
292
|
+
const block = { type: "tool_use", name: fc.name, input: fc.args ?? {} };
|
|
293
|
+
if (typeof fc.id === "string")
|
|
294
|
+
block.id = fc.id;
|
|
295
|
+
call.blocks.set(call.blocks.size, block);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (typeof p.text !== "string")
|
|
299
|
+
continue;
|
|
300
|
+
const kind = p.thought === true ? "thinking" : "text";
|
|
301
|
+
const last = call.blocks.get(call.blocks.size - 1);
|
|
302
|
+
if (last && last.type === kind)
|
|
303
|
+
last[kind] = (last[kind] ?? "") + p.text; // a streamed chunk
|
|
304
|
+
else
|
|
305
|
+
call.blocks.set(call.blocks.size, { type: kind, [kind]: p.text });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
213
308
|
function chatBlocks(call, content, toolCalls) {
|
|
214
309
|
let i = 0;
|
|
215
310
|
if (typeof content === "string" && content !== "")
|
package/dist/session/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Lessons } from "../analysis/learning.ts";
|
|
2
2
|
import { type Attestation } from "../attest/index.ts";
|
|
3
|
+
import { type ClearanceAnswer, type Notam, type Procedure } from "../aviation/index.ts";
|
|
3
4
|
import { buildPayment, type screenCounterparty } from "../finance/index.ts";
|
|
4
5
|
export interface SessionOptions {
|
|
5
6
|
/** The gateway, e.g. http://127.0.0.1:8787 */
|
|
@@ -314,6 +315,27 @@ export declare class BlackboxSession {
|
|
|
314
315
|
}): void;
|
|
315
316
|
/** The agent reports something it almost got wrong (spec/fleet.md §3): a NEAR_MISS finding. */
|
|
316
317
|
nearMiss(description: string): void;
|
|
318
|
+
/** spec/aviation.md §6: a voluntary, non-punitive safety report ("I nearly..."): a
|
|
319
|
+
* SAFETY_REPORT finding that never blocks. `category`: near_miss, hazard, procedure, tooling or
|
|
320
|
+
* other (the default). */
|
|
321
|
+
safetyReport(description: string, options?: {
|
|
322
|
+
category?: string;
|
|
323
|
+
}): void;
|
|
324
|
+
/** spec/aviation.md §10: the agent declares distress (it's stuck, unsure, or about to do harm): a
|
|
325
|
+
* MAYDAY warning, routed to a person at once. Shipped now, not with the next batch. */
|
|
326
|
+
mayday(reason: string, options?: {
|
|
327
|
+
kind?: "stuck" | "unsure" | "harm";
|
|
328
|
+
}): void;
|
|
329
|
+
/** spec/aviation.md §10: urgency short of distress: a PAN_PAN caution, shipped now. */
|
|
330
|
+
panPan(reason: string, options?: {
|
|
331
|
+
kind?: "stuck" | "unsure" | "harm";
|
|
332
|
+
}): void;
|
|
333
|
+
private distress;
|
|
334
|
+
/** spec/aviation.md §8: the agent reports running a QRH procedure (`started`, `completed`). */
|
|
335
|
+
procedure(code: string, status: "started" | "completed"): void;
|
|
336
|
+
/** spec/aviation.md §9: the run passes its commit point (V1): past it, with the gateway's
|
|
337
|
+
* BLACKBOX_COMMIT_POINT on, a non-critical finding doesn't stop it. Only the first counts. */
|
|
338
|
+
commitPoint(note?: string): void;
|
|
317
339
|
/** spec/attestation.md: re-runs a read-only command and records whether the output matches. */
|
|
318
340
|
attest(argv: string[], claimed: string, cwd?: string, claimedExit?: number): Attestation | undefined;
|
|
319
341
|
/**
|
|
@@ -377,6 +399,23 @@ export declare class BlackboxSession {
|
|
|
377
399
|
lessons(options?: {
|
|
378
400
|
limit?: number;
|
|
379
401
|
}): Promise<Lessons | null>;
|
|
402
|
+
/** One of this session's gateway routes: the answer on 200 or 201, else null. Never throws. */
|
|
403
|
+
private route;
|
|
404
|
+
/** spec/aviation.md §1-§2: asks for a clearance on a shared resource before changing it. The
|
|
405
|
+
* answer is `granted` (with its `clearance_id`) or `hold` (wait `retry_after_s`, then ask again).
|
|
406
|
+
* `null` when the gateway can't be asked. */
|
|
407
|
+
clearance(resource: string, options?: {
|
|
408
|
+
mode?: "exclusive" | "shared";
|
|
409
|
+
ttlS?: number;
|
|
410
|
+
}): Promise<ClearanceAnswer | null>;
|
|
411
|
+
/** spec/aviation.md §1: gives a clearance back. True when the gateway released it. */
|
|
412
|
+
releaseClearance(clearanceId: string): Promise<boolean>;
|
|
413
|
+
/** spec/aviation.md §8: the quick reference handbook's procedures for this session's
|
|
414
|
+
* findings. The gateway records that the agent read them. `null` when it can't be asked. */
|
|
415
|
+
qrh(): Promise<Procedure[] | null>;
|
|
416
|
+
/** spec/aviation.md §3: the operator's notices in force now, to read before a run. The gateway
|
|
417
|
+
* records that the agent read them. `null` when it can't be asked. */
|
|
418
|
+
notams(): Promise<Notam[] | null>;
|
|
380
419
|
/**
|
|
381
420
|
* spec/findings.md §1c: the agent changed itself (its prompt, a tool, its policy, its memory, its
|
|
382
421
|
* model). Only hashes of `before` and `after` are sent, never the content. A change that gives
|
|
@@ -393,6 +432,10 @@ export declare class BlackboxSession {
|
|
|
393
432
|
/** A Zanii Evolve certificate's digest behind the change (spec/fleet.md §9). */
|
|
394
433
|
certificate?: string;
|
|
395
434
|
}): void;
|
|
435
|
+
/** spec/evals.md §4: the version of a registered prompt this run uses, for when the gateway
|
|
436
|
+
* can't tell it from the system prompt's hash (the prompt is assembled per run). A bad name or
|
|
437
|
+
* version is dropped. */
|
|
438
|
+
promptVersion(name: string, version: number): void;
|
|
396
439
|
/** spec/findings.md §13.1: what in an answer (the latest, or the one at `seq`) would hold it,
|
|
397
440
|
* for an app that streams the answer by itself: `{seq, risks, hold}`. `hold` says whether the
|
|
398
441
|
* server would hold it. `null` when there's no answer yet or the gateway can't be asked. */
|