@zanii/blackbox 0.0.0-stage → 0.2.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 (135) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +109 -2
  3. package/dist/agents/index.d.ts +34 -0
  4. package/dist/agents/index.js +73 -0
  5. package/dist/analysis/detectors.d.ts +36 -0
  6. package/dist/analysis/detectors.js +339 -0
  7. package/dist/analysis/faults.d.ts +9 -0
  8. package/dist/analysis/faults.js +250 -0
  9. package/dist/analysis/index.d.ts +68 -0
  10. package/dist/analysis/index.js +388 -0
  11. package/dist/analysis/landing.d.ts +25 -0
  12. package/dist/analysis/landing.js +225 -0
  13. package/dist/analysis/memory.d.ts +13 -0
  14. package/dist/analysis/memory.js +33 -0
  15. package/dist/analysis/waste.d.ts +29 -0
  16. package/dist/analysis/waste.js +79 -0
  17. package/dist/approvals/index.d.ts +11 -0
  18. package/dist/approvals/index.js +27 -0
  19. package/dist/approvals/warnings.d.ts +2 -0
  20. package/dist/approvals/warnings.js +28 -0
  21. package/dist/attest/index.d.ts +17 -0
  22. package/dist/attest/index.js +106 -0
  23. package/dist/authority/index.d.ts +24 -0
  24. package/dist/authority/index.js +77 -0
  25. package/dist/billing/index.d.ts +99 -0
  26. package/dist/billing/index.js +174 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +1057 -0
  29. package/dist/client/index.d.ts +146 -0
  30. package/dist/client/index.js +210 -0
  31. package/dist/compliance/index.d.ts +41 -0
  32. package/dist/compliance/index.js +96 -0
  33. package/dist/cost/index.d.ts +133 -0
  34. package/dist/cost/index.js +293 -0
  35. package/dist/data/index.d.ts +191 -0
  36. package/dist/data/index.js +762 -0
  37. package/dist/directives/index.d.ts +35 -0
  38. package/dist/directives/index.js +80 -0
  39. package/dist/drills/index.d.ts +43 -0
  40. package/dist/drills/index.js +101 -0
  41. package/dist/duty/index.d.ts +21 -0
  42. package/dist/duty/index.js +68 -0
  43. package/dist/fleet/index.d.ts +141 -0
  44. package/dist/fleet/index.js +454 -0
  45. package/dist/hooks/ai-sdk.d.ts +42 -0
  46. package/dist/hooks/ai-sdk.js +62 -0
  47. package/dist/hooks/claude-agent-sdk.d.ts +14 -0
  48. package/dist/hooks/claude-agent-sdk.js +70 -0
  49. package/dist/hooks/index.d.ts +7 -0
  50. package/dist/hooks/index.js +10 -0
  51. package/dist/hooks/langchain-agent.d.ts +69 -0
  52. package/dist/hooks/langchain-agent.js +163 -0
  53. package/dist/hooks/langchain.d.ts +41 -0
  54. package/dist/hooks/langchain.js +216 -0
  55. package/dist/hooks/langgraph-checkpoint.d.ts +12 -0
  56. package/dist/hooks/langgraph-checkpoint.js +73 -0
  57. package/dist/hooks/memory.d.ts +17 -0
  58. package/dist/hooks/memory.js +64 -0
  59. package/dist/hooks/openai-agents.d.ts +6 -0
  60. package/dist/hooks/openai-agents.js +40 -0
  61. package/dist/hooks/protect.d.ts +7 -0
  62. package/dist/hooks/protect.js +39 -0
  63. package/dist/hooks/providers.d.ts +16 -0
  64. package/dist/hooks/providers.js +149 -0
  65. package/dist/hooks/shared.d.ts +11 -0
  66. package/dist/hooks/shared.js +39 -0
  67. package/dist/index.d.ts +46 -0
  68. package/dist/index.js +48 -0
  69. package/dist/investigate/index.d.ts +66 -0
  70. package/dist/investigate/index.js +119 -0
  71. package/dist/mcp-server/index.d.ts +85 -0
  72. package/dist/mcp-server/index.js +216 -0
  73. package/dist/mcp-wrap/index.d.ts +17 -0
  74. package/dist/mcp-wrap/index.js +170 -0
  75. package/dist/money/index.d.ts +114 -0
  76. package/dist/money/index.js +622 -0
  77. package/dist/occurrence/index.d.ts +108 -0
  78. package/dist/occurrence/index.js +168 -0
  79. package/dist/ocsf/index.d.ts +22 -0
  80. package/dist/ocsf/index.js +168 -0
  81. package/dist/otlp/index.d.ts +24 -0
  82. package/dist/otlp/index.js +143 -0
  83. package/dist/packs/index.d.ts +48 -0
  84. package/dist/packs/index.js +343 -0
  85. package/dist/policy/delta.d.ts +11 -0
  86. package/dist/policy/delta.js +39 -0
  87. package/dist/policy/drafts.d.ts +34 -0
  88. package/dist/policy/drafts.js +129 -0
  89. package/dist/policy/index.d.ts +47 -0
  90. package/dist/policy/index.js +154 -0
  91. package/dist/precog/index.d.ts +96 -0
  92. package/dist/precog/index.js +167 -0
  93. package/dist/precog/intervention.d.ts +22 -0
  94. package/dist/precog/intervention.js +44 -0
  95. package/dist/precog/normal.d.ts +31 -0
  96. package/dist/precog/normal.js +89 -0
  97. package/dist/preflight/index.d.ts +11 -0
  98. package/dist/preflight/index.js +19 -0
  99. package/dist/ratings/index.d.ts +21 -0
  100. package/dist/ratings/index.js +48 -0
  101. package/dist/reconcile/claude-code.d.ts +19 -0
  102. package/dist/reconcile/claude-code.js +220 -0
  103. package/dist/reconcile/codex.d.ts +5 -0
  104. package/dist/reconcile/codex.js +191 -0
  105. package/dist/reconcile/index.d.ts +19 -0
  106. package/dist/reconcile/index.js +50 -0
  107. package/dist/reconcile/record.d.ts +49 -0
  108. package/dist/reconcile/record.js +225 -0
  109. package/dist/reconcile/shared.d.ts +65 -0
  110. package/dist/reconcile/shared.js +113 -0
  111. package/dist/replay/index.d.ts +11 -0
  112. package/dist/replay/index.js +64 -0
  113. package/dist/replay/repair.d.ts +10 -0
  114. package/dist/replay/repair.js +62 -0
  115. package/dist/session/drain.d.ts +13 -0
  116. package/dist/session/drain.js +35 -0
  117. package/dist/session/index.d.ts +275 -0
  118. package/dist/session/index.js +681 -0
  119. package/dist/undo/index.d.ts +45 -0
  120. package/dist/undo/index.js +212 -0
  121. package/dist/verify/anchor.d.ts +54 -0
  122. package/dist/verify/anchor.js +77 -0
  123. package/dist/verify/chain.d.ts +27 -0
  124. package/dist/verify/chain.js +105 -0
  125. package/dist/verify/envelope.d.ts +28 -0
  126. package/dist/verify/envelope.js +55 -0
  127. package/dist/verify/index.d.ts +3 -0
  128. package/dist/verify/index.js +3 -0
  129. package/dist/version.d.ts +1 -0
  130. package/dist/version.js +2 -0
  131. package/dist/weather/index.d.ts +24 -0
  132. package/dist/weather/index.js +45 -0
  133. package/dist/workspace-receipt/index.d.ts +15 -0
  134. package/dist/workspace-receipt/index.js +121 -0
  135. package/package.json +56 -3
@@ -0,0 +1,69 @@
1
+ import { type CompiledPolicy } from "../policy/index.ts";
2
+ import { type EventSink } from "./shared.ts";
3
+ /** A BlackboxSession, or anything with the same methods. */
4
+ export interface AgentSink extends EventSink {
5
+ state?(): Promise<{
6
+ control?: {
7
+ state?: string;
8
+ };
9
+ } | null>;
10
+ requestApproval?(tool: string, options?: {
11
+ args?: Record<string, unknown>;
12
+ reason?: string;
13
+ }): Promise<"approved" | "rejected" | "timeout" | "error">;
14
+ }
15
+ type ToolCall = {
16
+ name: string;
17
+ args?: Record<string, unknown>;
18
+ id?: string;
19
+ };
20
+ /** Thrown by `beforeAgent` when the gateway has stopped this session (spec/control.md). */
21
+ export declare class BlackboxStoppedError extends Error {
22
+ constructor(state: string);
23
+ }
24
+ export declare function blackboxAgentMiddleware(session: AgentSink, options?: {
25
+ /** A tool policy checked before each tool call (spec/policy.md): deny refuses it,
26
+ * require_approval asks a second person through `requestApproval`. */
27
+ policy?: CompiledPolicy;
28
+ /** Before the run, stop if the gateway has blocked or paused the session (default true). */
29
+ checkBlock?: boolean;
30
+ /** Files with a machine-managed block (idea S5): file tools may not change it. */
31
+ protect?: readonly string[];
32
+ }): {
33
+ [x: symbol]: true;
34
+ name: string;
35
+ beforeAgent(): Promise<undefined>;
36
+ wrapModelCall<Q extends {
37
+ messages?: readonly unknown[];
38
+ model?: unknown;
39
+ }, R>(request: Q, handler: (r: Q) => R | Promise<R>): Promise<R>;
40
+ wrapToolCall<Q extends {
41
+ toolCall: ToolCall;
42
+ }, R>(request: Q, handler: (r: Q) => R | Promise<R>): Promise<R>;
43
+ afterAgent(): undefined;
44
+ };
45
+ /** LangChain's human-in-the-loop request (humanInTheLoopMiddleware's interrupt value). */
46
+ export interface HitlRequest {
47
+ actionRequests: Array<{
48
+ name: string;
49
+ args?: Record<string, unknown>;
50
+ description?: string;
51
+ }>;
52
+ reviewConfigs?: Array<{
53
+ actionName: string;
54
+ allowedDecisions: string[];
55
+ }>;
56
+ }
57
+ /** Idea I2: answers a LangChain HITL interrupt with Blackbox approvals, one per action, so a
58
+ * second person decides through Blackbox and the agent resumes with
59
+ * `new Command({ resume: await blackboxHitl(session, interrupt.value) })`. An edit can't be
60
+ * made here: anything but an approval is a reject, with the reason as its message. */
61
+ export declare function blackboxHitl(session: AgentSink, request: HitlRequest): Promise<{
62
+ decisions: Array<{
63
+ type: "approve";
64
+ } | {
65
+ type: "reject";
66
+ message: string;
67
+ }>;
68
+ }>;
69
+ export {};
@@ -0,0 +1,163 @@
1
+ // LangChain.js `createAgent` middleware (spec/sdk.md §5.2, idea I1). Mirrors hooks/langchain_agent.py.
2
+ //
3
+ // const agent = createAgent({ model, tools, middleware: [blackboxAgentMiddleware(session, { policy })] });
4
+ //
5
+ // Put it first in the list, so it's the outermost: it sees every model and tool call, and its
6
+ // gate is the last word before a tool runs. A plain object with LangChain's brand, so the SDK
7
+ // needs no LangChain dependency (ToolMessage is loaded only when a call is refused).
8
+ import { readFileSync } from "node:fs";
9
+ import { resolve } from "node:path";
10
+ import { decide } from "../policy/index.js";
11
+ import { stableEventId } from "../session/index.js";
12
+ import { protectedEdit } from "./protect.js";
13
+ import { llmIds, neverThrow, providerOf } from "./shared.js";
14
+ /** Thrown by `beforeAgent` when the gateway has stopped this session (spec/control.md). */
15
+ export class BlackboxStoppedError extends Error {
16
+ constructor(state) {
17
+ super(`blackbox: the session is ${state}; resume it before the agent runs again`);
18
+ this.name = "BlackboxStoppedError";
19
+ }
20
+ }
21
+ /** The refusal for a file tool call that would change a protected file's managed block. */
22
+ function protectedRefusal(protect, call) {
23
+ const args = call.args ?? {};
24
+ const target = typeof args.file_path === "string"
25
+ ? args.file_path
26
+ : typeof args.path === "string"
27
+ ? args.path
28
+ : undefined;
29
+ if (!protect?.length || target === undefined)
30
+ return null;
31
+ if (!protect.some((p) => resolve(p) === resolve(target)))
32
+ return null;
33
+ let current = null;
34
+ try {
35
+ current = readFileSync(target, "utf8");
36
+ }
37
+ catch { } // it doesn't exist (yet)
38
+ return protectedEdit(current, call.name, args);
39
+ }
40
+ /** LangGraph's control signals (interrupt(), Command) pass through untouched and aren't failures. */
41
+ const isControl = (e) => ["GraphInterrupt", "NodeInterrupt", "ParentCommand"].includes(e?.name ?? "");
42
+ export function blackboxAgentMiddleware(session, options = {}) {
43
+ /** With the tool call's id, a stable event id (idea R6), so a re-sent event counts once. */
44
+ const emit = (type, name, data, key) => neverThrow(() => session.event(type, name, data, key ? { eventId: stableEventId("lc-agent", type, key) } : {}));
45
+ const refusal = async (call, text) => {
46
+ const { ToolMessage } = await import("@langchain/core/messages");
47
+ return new ToolMessage({
48
+ content: `blackbox: ${text}`,
49
+ tool_call_id: call.id ?? "",
50
+ name: call.name,
51
+ status: "error",
52
+ });
53
+ };
54
+ return {
55
+ [Symbol.for("AgentMiddleware")]: true,
56
+ name: "blackbox",
57
+ async beforeAgent() {
58
+ emit("step", "agent.start", {});
59
+ if (options.checkBlock === false || !session.state)
60
+ return undefined;
61
+ const state = (await session.state())?.control?.state;
62
+ if (state === "blocked" || state === "paused")
63
+ throw new BlackboxStoppedError(state);
64
+ return undefined;
65
+ },
66
+ async wrapModelCall(request, handler) {
67
+ const m = request.model;
68
+ const model = m?.model ?? m?.modelName;
69
+ emit("llm.start", undefined, {
70
+ messages: request.messages?.length ?? 0,
71
+ ...(typeof model === "string" ? { params: { model } } : {}),
72
+ });
73
+ try {
74
+ const out = await handler(request);
75
+ neverThrow(() => {
76
+ const m = out;
77
+ const meta = m?.response_metadata ?? {};
78
+ const id = typeof meta.id === "string" ? meta.id : typeof m?.id === "string" ? m.id : undefined;
79
+ const ids = llmIds(id && !/^(lc_)?run-/.test(id) ? id : undefined, typeof meta.request_id === "string" ? meta.request_id : undefined, typeof meta.model_name === "string" ? meta.model_name : undefined);
80
+ if (Object.keys(ids).length)
81
+ emit("llm.call", providerOf(id), ids);
82
+ });
83
+ return out;
84
+ }
85
+ catch (error) {
86
+ if (!isControl(error))
87
+ emit("llm.error", undefined, {
88
+ error: String(error?.message ?? error).slice(0, 500),
89
+ });
90
+ throw error;
91
+ }
92
+ },
93
+ async wrapToolCall(request, handler) {
94
+ const call = request.toolCall;
95
+ // The tool call's id is the run id the detectors pair a call and its result by.
96
+ const key = call.id;
97
+ emit("tool.call", call.name, { args: call.args ?? {}, ...(key ? { run_id: key } : {}) }, key);
98
+ const guarded = protectedRefusal(options.protect, call);
99
+ if (guarded) {
100
+ emit("tool.result", call.name, { ok: false, ...(key ? { run_id: key } : {}), error: "protected region" }, key);
101
+ return (await refusal(call, guarded.replace(/^blackbox: /, "")));
102
+ }
103
+ const d = options.policy ? decide(options.policy, [call.name], call.args ?? {}) : null;
104
+ if (d?.action === "deny") {
105
+ emit("tool.result", call.name, { ok: false, ...(key ? { run_id: key } : {}), error: `denied by ${d.rule}` }, key);
106
+ return (await refusal(call, `denied by policy ${d.rule}${d.reason ? `: ${d.reason}` : ""}`));
107
+ }
108
+ if (d?.action === "require_approval") {
109
+ const answer = session.requestApproval
110
+ ? await session.requestApproval(call.name, {
111
+ args: call.args ?? {},
112
+ reason: `policy ${d.rule}`,
113
+ })
114
+ : "error";
115
+ if (answer !== "approved") {
116
+ emit("tool.result", call.name, { ok: false, ...(key ? { run_id: key } : {}), error: `approval ${answer}` }, key);
117
+ return (await refusal(call, `a second person's approval is needed (${answer})`));
118
+ }
119
+ }
120
+ try {
121
+ const out = await handler(request);
122
+ const status = out?.status;
123
+ emit("tool.result", call.name, { ok: status !== "error", ...(key ? { run_id: key } : {}) }, key);
124
+ return out;
125
+ }
126
+ catch (error) {
127
+ if (isControl(error)) {
128
+ emit("interrupt", call.name, { ...(key ? { run_id: key } : {}) }, key);
129
+ }
130
+ else
131
+ emit("tool.result", call.name, {
132
+ ok: false,
133
+ ...(key ? { run_id: key } : {}),
134
+ error: String(error?.message ?? error).slice(0, 500),
135
+ }, key);
136
+ throw error; // control signals included: LangGraph needs them
137
+ }
138
+ },
139
+ afterAgent() {
140
+ emit("step", "agent.end", {});
141
+ return undefined;
142
+ },
143
+ };
144
+ }
145
+ /** Idea I2: answers a LangChain HITL interrupt with Blackbox approvals, one per action, so a
146
+ * second person decides through Blackbox and the agent resumes with
147
+ * `new Command({ resume: await blackboxHitl(session, interrupt.value) })`. An edit can't be
148
+ * made here: anything but an approval is a reject, with the reason as its message. */
149
+ export async function blackboxHitl(session, request) {
150
+ const decisions = [];
151
+ for (const a of request.actionRequests) {
152
+ const answer = session.requestApproval
153
+ ? await session.requestApproval(a.name, {
154
+ args: a.args ?? {},
155
+ ...(a.description ? { reason: a.description } : {}),
156
+ })
157
+ : "error";
158
+ decisions.push(answer === "approved"
159
+ ? { type: "approve" }
160
+ : { type: "reject", message: `blackbox: not approved (${answer})` });
161
+ }
162
+ return { decisions };
163
+ }
@@ -0,0 +1,41 @@
1
+ import { type EventSink } from "./shared.ts";
2
+ type Meta = Record<string, unknown> | undefined;
3
+ export interface BlackboxCallbacks {
4
+ name: string;
5
+ /** N4 (idea R11): LangChain.js waits for each handler, so the log keeps up with the run. */
6
+ awaitHandlers: boolean;
7
+ /** LangGraph.js's GraphCallbackHandler brand. */
8
+ readonly [brand: symbol]: unknown;
9
+ handleChatModelStart(llm: unknown, messages: readonly (readonly unknown[])[], runId: string, parentRunId?: string, extraParams?: Record<string, unknown>): void;
10
+ handleLLMStart(llm: unknown, prompts: readonly string[], runId: string, parentRunId?: string, extraParams?: Record<string, unknown>): void;
11
+ handleLLMError(error: unknown, runId: string): void;
12
+ handleRetrieverEnd(documents: readonly unknown[], runId: string): void;
13
+ handleCustomEvent(eventName: string, data: unknown, runId: string): void;
14
+ /** N5 (idea R3): LangGraph.js's own lifecycle callbacks (it calls them for handlers with its brand). */
15
+ handleInterrupt(event: GraphEvent): void;
16
+ handleResume(event: GraphEvent): void;
17
+ handleChainStart(chain: unknown, inputs: unknown, runId: string, parentRunId?: string, tags?: string[], metadata?: Meta, runType?: string, runName?: string): void;
18
+ handleToolStart(tool: unknown, input: string, runId: string, parentRunId?: string, tags?: string[], metadata?: Meta, runName?: string, toolCallId?: string, toolInputs?: unknown): void;
19
+ handleChainEnd(outputs: unknown, runId: string): void;
20
+ handleChainError(error: unknown, runId: string): void;
21
+ handleToolEnd(output: unknown, runId: string): void;
22
+ handleToolError(error: unknown, runId: string): void;
23
+ handleLLMEnd(output: {
24
+ generations?: readonly (readonly object[])[];
25
+ llmOutput?: Record<string, unknown>;
26
+ }, runId: string, parentRunId?: string, tags?: string[], extraParams?: Record<string, unknown>): void;
27
+ }
28
+ /** LangGraph.js's GraphInterruptEvent / GraphResumeEvent. */
29
+ type GraphEvent = {
30
+ runId?: string;
31
+ status?: string;
32
+ checkpointId?: string;
33
+ checkpointNs?: readonly string[];
34
+ interrupts?: ReadonlyArray<{
35
+ id?: string;
36
+ }>;
37
+ };
38
+ export declare function blackboxCallbacks(session: EventSink): BlackboxCallbacks;
39
+ /** Audit S6: the Python SDK's name, so docs and examples can be written once. Called, not `new`ed. */
40
+ export declare const BlackboxCallbackHandler: typeof blackboxCallbacks;
41
+ export {};
@@ -0,0 +1,216 @@
1
+ // LangChain.js / LangGraph.js callback handler (spec/sdk.md §5). Mirrors hooks/langchain.py.
2
+ //
3
+ // await graph.invoke(input, { callbacks: [blackboxCallbacks(session)], configurable: { thread_id } })
4
+ //
5
+ // A plain handler object: LangChain.js accepts any object with the handle* methods, so the SDK
6
+ // needs no LangChain dependency.
7
+ import { stableEventId } from "../session/index.js";
8
+ import { llmIds, neverThrow, providerOf } from "./shared.js";
9
+ /** A stable id per callback (N4, idea R6): a re-sent callback is recorded as a duplicate. The
10
+ * extra argument is ignored by sinks that don't take it. */
11
+ const emit = (sink, type, name, data, key) => sink.event(type, name, data, { eventId: stableEventId("langchain", type, key) });
12
+ const PARAMS = ["model", "model_name", "temperature", "max_tokens", "top_p", "stop", "seed"];
13
+ /** N4 (idea R11): usage_metadata, flattened: tokens only, with cache and reasoning detail. */
14
+ function usageOf(u) {
15
+ const out = {};
16
+ for (const k of ["input_tokens", "output_tokens", "total_tokens"])
17
+ if (Number.isSafeInteger(u?.[k]))
18
+ out[k] = u?.[k];
19
+ for (const group of ["input_token_details", "output_token_details"])
20
+ for (const [k, v] of Object.entries(u?.[group] ?? {}))
21
+ if (Number.isSafeInteger(v))
22
+ out[`${group.split("_")[0]}_${k}`] = v;
23
+ return out;
24
+ }
25
+ export function blackboxCallbacks(session) {
26
+ const parents = new Map();
27
+ const threads = new Map(); // a graph run's thread_id, for its interrupts
28
+ const lifecycle = (type, e) => {
29
+ const ns = [...(e.checkpointNs ?? [])].map(String);
30
+ const last = ns.at(-1) ?? "";
31
+ const node = last.includes(":") ? last.slice(0, last.lastIndexOf(":")) : undefined;
32
+ session.event(type, node?.slice(0, 256), {
33
+ source: "langgraph",
34
+ checkpoint_id: String(e.checkpointId ?? ""),
35
+ checkpoint_ns: ns.join("|"),
36
+ status: String(e.status ?? ""),
37
+ ...(e.runId ? { run_id: e.runId } : {}),
38
+ ...(e.runId && threads.has(e.runId) ? { thread_id: threads.get(e.runId) } : {}),
39
+ ...(node ? { node, task_id: last.slice(last.lastIndexOf(":") + 1) } : {}),
40
+ ...(type === "interrupt"
41
+ ? { interrupt_ids: (e.interrupts ?? []).flatMap((i) => (i?.id ? [String(i.id)] : [])) }
42
+ : {}),
43
+ });
44
+ };
45
+ const tools = new Map();
46
+ const nodes = new Map();
47
+ const track = (runId, parentRunId) => {
48
+ parents.set(runId, parentRunId);
49
+ let root = runId;
50
+ const seen = new Set();
51
+ for (let p = parents.get(root); p && !seen.has(root); p = parents.get(root)) {
52
+ seen.add(root);
53
+ root = p;
54
+ }
55
+ return root;
56
+ };
57
+ const graph = (metadata) => {
58
+ const m = metadata ?? {};
59
+ return {
60
+ ...(typeof m.langgraph_node === "string" ? { node: m.langgraph_node } : {}),
61
+ ...(typeof m.langgraph_step === "number" ? { langgraph_step: m.langgraph_step } : {}),
62
+ ...(m.thread_id !== undefined && m.thread_id !== null
63
+ ? { thread_id: String(m.thread_id) }
64
+ : {}),
65
+ // LangGraph's deterministic task id is the suffix after the last ":" of langgraph_checkpoint_ns.
66
+ ...(typeof m.langgraph_checkpoint_ns === "string" && m.langgraph_checkpoint_ns.includes(":")
67
+ ? {
68
+ checkpoint_ns: m.langgraph_checkpoint_ns,
69
+ task_id: m.langgraph_checkpoint_ns.slice(m.langgraph_checkpoint_ns.lastIndexOf(":") + 1),
70
+ }
71
+ : {}),
72
+ };
73
+ };
74
+ const nameOf = (serialized, runName) => {
75
+ const s = serialized;
76
+ const fromId = Array.isArray(s?.id) ? s?.id.at(-1) : undefined;
77
+ return String(runName ?? s?.name ?? fromId ?? "run").slice(0, 256);
78
+ };
79
+ const llmStart = (runId, parentRunId, count, extra) => {
80
+ const root = track(runId, parentRunId);
81
+ const raw = extra?.invocation_params ?? {};
82
+ const params = Object.fromEntries(PARAMS.filter((k) => ["string", "number"].includes(typeof raw[k]) || Array.isArray(raw[k])).map((k) => [k, raw[k]]));
83
+ emit(session, "llm.start", undefined, { run_id: runId, root_run_id: root, messages: count, params }, runId);
84
+ };
85
+ return {
86
+ name: "blackbox",
87
+ awaitHandlers: true,
88
+ [Symbol.for("langgraph.graph_callback_handler")]: true,
89
+ handleInterrupt(event) {
90
+ neverThrow(() => lifecycle("interrupt", event));
91
+ },
92
+ handleResume(event) {
93
+ neverThrow(() => lifecycle("resume", event));
94
+ },
95
+ handleChatModelStart(_llm, messages, runId, parentRunId, extraParams) {
96
+ neverThrow(() => llmStart(runId, parentRunId, messages.reduce((n, m) => n + m.length, 0), extraParams));
97
+ },
98
+ handleLLMStart(_llm, prompts, runId, parentRunId, extraParams) {
99
+ neverThrow(() => llmStart(runId, parentRunId, prompts.length, extraParams));
100
+ },
101
+ handleLLMError(error, runId) {
102
+ neverThrow(() => {
103
+ const text = error instanceof Error ? error.message : String(error);
104
+ const type = error instanceof Error ? error.name : typeof error;
105
+ emit(session, "llm.error", undefined, { run_id: runId, error: text.slice(0, 500), type }, runId);
106
+ });
107
+ },
108
+ handleRetrieverEnd(documents, runId) {
109
+ neverThrow(() => {
110
+ const sources = documents
111
+ .slice(0, 20)
112
+ .map((d) => {
113
+ const doc = d;
114
+ return doc.metadata?.source ?? doc.id;
115
+ })
116
+ .filter((s) => s !== undefined && s !== null)
117
+ .map((s) => String(s).slice(0, 256));
118
+ emit(session, "retrieval", undefined, { run_id: runId, documents: documents.length, sources }, runId);
119
+ });
120
+ },
121
+ handleCustomEvent(eventName, data, runId) {
122
+ neverThrow(() => session.event("custom", String(eventName).slice(0, 256), {
123
+ run_id: runId,
124
+ ...(typeof data === "object" && data !== null && !Array.isArray(data) ? { data } : {}),
125
+ }));
126
+ },
127
+ handleChainStart(chain, _inputs, runId, parentRunId, _tags, metadata, _runType, runName) {
128
+ neverThrow(() => {
129
+ const root = track(runId, parentRunId);
130
+ const g = graph(metadata);
131
+ const name = nameOf(chain, runName);
132
+ if (g.node && name === g.node)
133
+ nodes.set(runId, g);
134
+ if (!parentRunId && g.thread_id)
135
+ threads.set(runId, g.thread_id);
136
+ // A LangGraph node's own run, or a top-level run; not every inner runnable.
137
+ if (!parentRunId || (g.node && name === g.node))
138
+ emit(session, "step", name, { run_id: runId, root_run_id: root, ...g }, runId);
139
+ });
140
+ },
141
+ handleToolStart(tool, input, runId, parentRunId, _tags, metadata, runName, _toolCallId, toolInputs) {
142
+ neverThrow(() => {
143
+ const root = track(runId, parentRunId);
144
+ const name = nameOf(tool, runName);
145
+ tools.set(runId, name);
146
+ let args = toolInputs ?? input;
147
+ if (toolInputs === undefined && typeof input === "string") {
148
+ try {
149
+ args = JSON.parse(input);
150
+ }
151
+ catch { }
152
+ }
153
+ emit(session, "tool.call", name, { args, run_id: runId, root_run_id: root, ...graph(metadata) }, runId);
154
+ });
155
+ },
156
+ handleChainEnd(_outputs, runId) {
157
+ nodes.delete(runId);
158
+ },
159
+ handleChainError(error, runId) {
160
+ neverThrow(() => {
161
+ const g = nodes.get(runId);
162
+ nodes.delete(runId);
163
+ // interrupt() stops a node with GraphInterrupt; resuming re-runs the node from the top.
164
+ const kind = error?.name;
165
+ if (g && (kind === "GraphInterrupt" || kind === "NodeInterrupt"))
166
+ session.event("interrupt", String(g.node).slice(0, 256), { run_id: runId, ...g });
167
+ });
168
+ },
169
+ handleToolEnd(_output, runId) {
170
+ neverThrow(() => {
171
+ emit(session, "tool.result", tools.get(runId) ?? "tool", { ok: true, run_id: runId }, runId);
172
+ tools.delete(runId);
173
+ });
174
+ },
175
+ handleToolError(error, runId) {
176
+ neverThrow(() => {
177
+ const text = error instanceof Error ? error.message : String(error);
178
+ emit(session, "tool.result", tools.get(runId) ?? "tool", { ok: false, run_id: runId, error: text.slice(0, 500) }, runId);
179
+ tools.delete(runId);
180
+ });
181
+ },
182
+ handleLLMEnd(output, runId, _parentRunId, _tags, extraParams) {
183
+ neverThrow(() => {
184
+ if (extraParams?.cached === true)
185
+ return; // a LangChain cache hit: no provider call to witness
186
+ let id;
187
+ let model;
188
+ let requestId;
189
+ let usage = {};
190
+ for (const generations of output.generations ?? [])
191
+ for (const g of generations) {
192
+ if (Object.keys(usage).length === 0)
193
+ usage = usageOf(g.message?.usage_metadata);
194
+ const meta = g.message?.response_metadata ?? {};
195
+ // response_metadata.id, else the message's own id (Chat Completions, Anthropic
196
+ // streaming); LangChain's placeholders ("run-…", "lc_run-…") aren't provider ids.
197
+ const own = [g.message?.id, g.message?.additional_kwargs?.id].find((v) => typeof v === "string" && !/^(lc_)?run-/.test(v));
198
+ id ??= typeof meta.id === "string" ? meta.id : own;
199
+ model ??=
200
+ typeof meta.model_name === "string"
201
+ ? meta.model_name
202
+ : typeof meta.model === "string"
203
+ ? meta.model
204
+ : undefined;
205
+ requestId ??= typeof meta.request_id === "string" ? meta.request_id : undefined;
206
+ }
207
+ id ??= typeof output.llmOutput?.id === "string" ? output.llmOutput.id : undefined;
208
+ const ids = llmIds(id, requestId, model);
209
+ if (Object.keys(ids).length > 0)
210
+ emit(session, "llm.call", providerOf(id), { ...ids, ...(Object.keys(usage).length ? { usage } : {}) }, runId);
211
+ });
212
+ },
213
+ };
214
+ }
215
+ /** Audit S6: the Python SDK's name, so docs and examples can be written once. Called, not `new`ed. */
216
+ export const BlackboxCallbackHandler = blackboxCallbacks;
@@ -0,0 +1,12 @@
1
+ import { type EventSink } from "./shared.ts";
2
+ type Config = {
3
+ configurable?: Record<string, unknown>;
4
+ };
5
+ interface Saver {
6
+ put(config: Config, checkpoint: {
7
+ id?: unknown;
8
+ }, metadata: unknown, versions: unknown): Promise<unknown>;
9
+ putWrites(config: Config, writes: ReadonlyArray<readonly [string, unknown]>, taskId: string): Promise<void>;
10
+ }
11
+ export declare function blackboxCheckpointer<T extends Saver>(inner: T, session: EventSink): T;
12
+ export {};
@@ -0,0 +1,73 @@
1
+ // A LangGraph.js checkpointer that records what it saves (spec/sdk.md §5.3, ideas R1, R2).
2
+ // Mirrors hooks/langgraph_checkpoint.py.
3
+ //
4
+ // const app = graph.compile({ checkpointer: blackboxCheckpointer(new MemorySaver(), session) });
5
+ //
6
+ // Every `put` is a `checkpoint.put` event and every `putWrites` a `checkpoint.writes` event: ids,
7
+ // the step, the source and channel names, never the values. The wrapper is the inner saver with
8
+ // two methods replaced (its prototype is the inner one, so `instanceof BaseCheckpointSaver` still
9
+ // holds): the SDK needs no LangGraph dependency.
10
+ import { stableEventId } from "../session/index.js";
11
+ import { neverThrow } from "./shared.js";
12
+ export function blackboxCheckpointer(inner, session) {
13
+ const emit = (type, name, data, key) => neverThrow(() => session.event(type, name, data, {
14
+ eventId: stableEventId("checkpoint", type, ...key),
15
+ }));
16
+ const where = (config) => {
17
+ const c = config?.configurable ?? {};
18
+ return Object.fromEntries(["thread_id", "checkpoint_ns", "checkpoint_id"]
19
+ .filter((k) => c[k] !== undefined && c[k] !== null)
20
+ .map((k) => [k, String(c[k])]));
21
+ };
22
+ // Every method runs on the inner saver itself, so its own state stays its own.
23
+ const wrapped = Object.create(inner);
24
+ let p = inner;
25
+ while (p && p !== Object.prototype) {
26
+ for (const name of Object.getOwnPropertyNames(p)) {
27
+ const v = inner[name];
28
+ if (name !== "constructor" &&
29
+ typeof v === "function" &&
30
+ !(name in wrapped && Object.hasOwn(wrapped, name)))
31
+ Object.defineProperty(wrapped, name, {
32
+ value: v.bind(inner),
33
+ writable: true,
34
+ configurable: true,
35
+ });
36
+ }
37
+ p = Object.getPrototypeOf(p);
38
+ }
39
+ wrapped.put = async (config, checkpoint, metadata, versions) => {
40
+ const out = await inner.put(config, checkpoint, metadata, versions);
41
+ const w = where(config);
42
+ const m = (metadata ?? {});
43
+ const data = {
44
+ thread_id: w.thread_id ?? "",
45
+ checkpoint_ns: w.checkpoint_ns ?? "",
46
+ checkpoint_id: String(checkpoint?.id ?? ""),
47
+ ...(w.checkpoint_id ? { parent_checkpoint_id: w.checkpoint_id } : {}),
48
+ ...(Number.isSafeInteger(m.step) ? { step: m.step } : {}),
49
+ ...(m.source ? { source: String(m.source) } : {}),
50
+ channels: Object.keys(versions ?? {}).sort(),
51
+ };
52
+ emit("checkpoint.put", data.source, data, [
53
+ data.thread_id,
54
+ data.checkpoint_ns,
55
+ data.checkpoint_id,
56
+ ]);
57
+ return out;
58
+ };
59
+ wrapped.putWrites = async (config, writes, taskId) => {
60
+ await inner.putWrites(config, writes, taskId);
61
+ const w = where(config);
62
+ const channels = writes.map(([c]) => String(c));
63
+ emit("checkpoint.writes", undefined, {
64
+ ...w,
65
+ task_id: taskId,
66
+ channels,
67
+ error: channels.includes("__error__"),
68
+ interrupt: channels.includes("__interrupt__"),
69
+ resume: channels.includes("__resume__"),
70
+ }, [w.thread_id ?? "", w.checkpoint_id ?? "", taskId, ...channels]);
71
+ };
72
+ return wrapped;
73
+ }
@@ -0,0 +1,17 @@
1
+ export interface MemorySink {
2
+ memoryWrite(memoryId: string, summary?: string): void;
3
+ memoryRead(memoryIds: string[], query?: string): void;
4
+ memoryRevoke(memoryId: string, reason?: string): void;
5
+ }
6
+ export interface MemoryMethod {
7
+ /** The store's method name. */
8
+ method: string;
9
+ /** The memory ids a call touched, from its result and arguments. */
10
+ ids?: (result: unknown, args: unknown[]) => string[];
11
+ }
12
+ export interface TrackMemoryOptions {
13
+ write?: MemoryMethod;
14
+ read?: MemoryMethod;
15
+ revoke?: MemoryMethod;
16
+ }
17
+ export declare function trackMemory<T extends object>(store: T, sink: MemorySink, options?: TrackMemoryOptions): T;
@@ -0,0 +1,64 @@
1
+ // Audit S10: a memory store, wrapped so its writes, reads and deletes are recorded for the Memory
2
+ // X-ray (spec/agents.md §1) without wiring memoryWrite / memoryRead / memoryRevoke by hand.
3
+ // Duck-typed; the defaults fit mem0-shaped stores (add / search / delete, `{results: [{id, memory}]}`).
4
+ // Mirrors sdks/python/src/zanii_blackbox/hooks/memory.py.
5
+ //
6
+ // const memory = trackMemory(new Memory(), session);
7
+ // await memory.add(messages, { userId }); // → memory.write, one per memory id
8
+ import { neverThrow } from "./shared.js";
9
+ /** Items with an `id`: an array, `{results: [...]}`, or one item. */
10
+ function itemsOf(result) {
11
+ if (Array.isArray(result))
12
+ return result;
13
+ const r = result;
14
+ if (r && Array.isArray(r.results))
15
+ return r.results;
16
+ if (r && typeof r === "object" && typeof r.id === "string")
17
+ return [r];
18
+ return [];
19
+ }
20
+ const idsOf = (result) => itemsOf(result)
21
+ .map((i) => i.id)
22
+ .filter((id) => typeof id === "string");
23
+ const DEFAULTS = {
24
+ write: { method: "add", ids: idsOf },
25
+ read: { method: "search", ids: idsOf },
26
+ // delete(memoryId): the id is the argument
27
+ revoke: { method: "delete", ids: (_r, args) => (typeof args[0] === "string" ? [args[0]] : []) },
28
+ };
29
+ export function trackMemory(store, sink, options = {}) {
30
+ const kinds = {
31
+ write: { ...DEFAULTS.write, ...options.write },
32
+ read: { ...DEFAULTS.read, ...options.read },
33
+ revoke: { ...DEFAULTS.revoke, ...options.revoke },
34
+ };
35
+ const record = (kind, result, args) => neverThrow(() => {
36
+ const ids = (kinds[kind].ids ?? idsOf)(result, args);
37
+ if (kind === "read") {
38
+ sink.memoryRead(ids, typeof args[0] === "string" ? args[0] : undefined);
39
+ return;
40
+ }
41
+ const summaries = new Map(itemsOf(result).map((i) => [i.id, typeof i.memory === "string" ? i.memory : undefined]));
42
+ for (const id of ids)
43
+ if (kind === "write")
44
+ sink.memoryWrite(id, summaries.get(id)?.slice(0, 200));
45
+ else
46
+ sink.memoryRevoke(id);
47
+ });
48
+ return new Proxy(store, {
49
+ get(obj, prop, receiver) {
50
+ const value = Reflect.get(obj, prop, receiver);
51
+ const kind = Object.keys(kinds).find((k) => kinds[k].method === prop);
52
+ if (!kind || typeof value !== "function")
53
+ return value;
54
+ return (...args) => {
55
+ const result = value.apply(obj, args);
56
+ if (typeof result?.then === "function")
57
+ result.then((r) => record(kind, r, args), () => { });
58
+ else
59
+ record(kind, result, args);
60
+ return result;
61
+ };
62
+ },
63
+ });
64
+ }
@@ -0,0 +1,6 @@
1
+ import { type EventSink } from "./shared.ts";
2
+ interface HookEmitter {
3
+ on(event: any, listener: any): unknown;
4
+ }
5
+ export declare function attachBlackbox(runner: HookEmitter, session: EventSink): void;
6
+ export {};