@zanii/blackbox 0.0.0-stage → 0.1.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 (133) 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 +210 -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 +1019 -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/directives/index.d.ts +35 -0
  36. package/dist/directives/index.js +80 -0
  37. package/dist/drills/index.d.ts +43 -0
  38. package/dist/drills/index.js +101 -0
  39. package/dist/duty/index.d.ts +21 -0
  40. package/dist/duty/index.js +68 -0
  41. package/dist/fleet/index.d.ts +141 -0
  42. package/dist/fleet/index.js +454 -0
  43. package/dist/hooks/ai-sdk.d.ts +42 -0
  44. package/dist/hooks/ai-sdk.js +62 -0
  45. package/dist/hooks/claude-agent-sdk.d.ts +14 -0
  46. package/dist/hooks/claude-agent-sdk.js +70 -0
  47. package/dist/hooks/index.d.ts +7 -0
  48. package/dist/hooks/index.js +10 -0
  49. package/dist/hooks/langchain-agent.d.ts +69 -0
  50. package/dist/hooks/langchain-agent.js +163 -0
  51. package/dist/hooks/langchain.d.ts +41 -0
  52. package/dist/hooks/langchain.js +216 -0
  53. package/dist/hooks/langgraph-checkpoint.d.ts +12 -0
  54. package/dist/hooks/langgraph-checkpoint.js +73 -0
  55. package/dist/hooks/memory.d.ts +17 -0
  56. package/dist/hooks/memory.js +64 -0
  57. package/dist/hooks/openai-agents.d.ts +6 -0
  58. package/dist/hooks/openai-agents.js +40 -0
  59. package/dist/hooks/protect.d.ts +7 -0
  60. package/dist/hooks/protect.js +39 -0
  61. package/dist/hooks/providers.d.ts +16 -0
  62. package/dist/hooks/providers.js +149 -0
  63. package/dist/hooks/shared.d.ts +11 -0
  64. package/dist/hooks/shared.js +39 -0
  65. package/dist/index.d.ts +45 -0
  66. package/dist/index.js +47 -0
  67. package/dist/investigate/index.d.ts +66 -0
  68. package/dist/investigate/index.js +119 -0
  69. package/dist/mcp-server/index.d.ts +85 -0
  70. package/dist/mcp-server/index.js +216 -0
  71. package/dist/mcp-wrap/index.d.ts +17 -0
  72. package/dist/mcp-wrap/index.js +170 -0
  73. package/dist/money/index.d.ts +114 -0
  74. package/dist/money/index.js +622 -0
  75. package/dist/occurrence/index.d.ts +108 -0
  76. package/dist/occurrence/index.js +168 -0
  77. package/dist/ocsf/index.d.ts +22 -0
  78. package/dist/ocsf/index.js +168 -0
  79. package/dist/otlp/index.d.ts +24 -0
  80. package/dist/otlp/index.js +143 -0
  81. package/dist/packs/index.d.ts +40 -0
  82. package/dist/packs/index.js +217 -0
  83. package/dist/policy/delta.d.ts +11 -0
  84. package/dist/policy/delta.js +39 -0
  85. package/dist/policy/drafts.d.ts +34 -0
  86. package/dist/policy/drafts.js +129 -0
  87. package/dist/policy/index.d.ts +47 -0
  88. package/dist/policy/index.js +154 -0
  89. package/dist/precog/index.d.ts +96 -0
  90. package/dist/precog/index.js +167 -0
  91. package/dist/precog/intervention.d.ts +22 -0
  92. package/dist/precog/intervention.js +44 -0
  93. package/dist/precog/normal.d.ts +31 -0
  94. package/dist/precog/normal.js +89 -0
  95. package/dist/preflight/index.d.ts +11 -0
  96. package/dist/preflight/index.js +19 -0
  97. package/dist/ratings/index.d.ts +21 -0
  98. package/dist/ratings/index.js +48 -0
  99. package/dist/reconcile/claude-code.d.ts +19 -0
  100. package/dist/reconcile/claude-code.js +220 -0
  101. package/dist/reconcile/codex.d.ts +5 -0
  102. package/dist/reconcile/codex.js +191 -0
  103. package/dist/reconcile/index.d.ts +19 -0
  104. package/dist/reconcile/index.js +50 -0
  105. package/dist/reconcile/record.d.ts +49 -0
  106. package/dist/reconcile/record.js +225 -0
  107. package/dist/reconcile/shared.d.ts +65 -0
  108. package/dist/reconcile/shared.js +113 -0
  109. package/dist/replay/index.d.ts +11 -0
  110. package/dist/replay/index.js +64 -0
  111. package/dist/replay/repair.d.ts +10 -0
  112. package/dist/replay/repair.js +62 -0
  113. package/dist/session/drain.d.ts +13 -0
  114. package/dist/session/drain.js +35 -0
  115. package/dist/session/index.d.ts +256 -0
  116. package/dist/session/index.js +658 -0
  117. package/dist/undo/index.d.ts +45 -0
  118. package/dist/undo/index.js +212 -0
  119. package/dist/verify/anchor.d.ts +54 -0
  120. package/dist/verify/anchor.js +77 -0
  121. package/dist/verify/chain.d.ts +27 -0
  122. package/dist/verify/chain.js +105 -0
  123. package/dist/verify/envelope.d.ts +28 -0
  124. package/dist/verify/envelope.js +55 -0
  125. package/dist/verify/index.d.ts +3 -0
  126. package/dist/verify/index.js +3 -0
  127. package/dist/version.d.ts +1 -0
  128. package/dist/version.js +2 -0
  129. package/dist/weather/index.d.ts +24 -0
  130. package/dist/weather/index.js +45 -0
  131. package/dist/workspace-receipt/index.d.ts +15 -0
  132. package/dist/workspace-receipt/index.js +121 -0
  133. package/package.json +56 -3
@@ -0,0 +1,14 @@
1
+ import { type EventSink } from "./shared.ts";
2
+ type Callback = (input: unknown, toolUseId: string | undefined, options: unknown) => Promise<object>;
3
+ export interface BlackboxAgentHooks {
4
+ /** Pass as `options.hooks`. */
5
+ hooks: Record<"PreToolUse" | "PostToolUse" | "PostToolUseFailure", Array<{
6
+ hooks: Callback[];
7
+ }>>;
8
+ /** Call with every message `query()` yields; reports each model call's id once. */
9
+ observe(message: unknown): void;
10
+ }
11
+ export declare function blackboxAgentHooks(session: EventSink): BlackboxAgentHooks;
12
+ /** Audit S6: the Python SDK's name, so docs and examples can be written once. Called, not `new`ed. */
13
+ export declare const BlackboxAgentHooks: typeof blackboxAgentHooks;
14
+ export {};
@@ -0,0 +1,70 @@
1
+ // Claude Agent SDK hooks (spec/sdk.md §5). Mirrors hooks/claude_agent_sdk.py.
2
+ //
3
+ // const bbx = blackboxAgentHooks(session);
4
+ // for await (const message of query({ prompt, options: { hooks: bbx.hooks, env } })) {
5
+ // bbx.observe(message);
6
+ // …
7
+ // }
8
+ //
9
+ // Tool calls come from the SDK's PreToolUse / PostToolUse / PostToolUseFailure hooks; model calls
10
+ // from the assistant messages `query()` yields (the Agent SDK has no model-call hook). Structural
11
+ // types only, so the Agent SDK stays an optional dependency.
12
+ import { neverThrow } from "./shared.js";
13
+ const MAX_ERROR = 500;
14
+ export function blackboxAgentHooks(session) {
15
+ const seen = new Set();
16
+ const hook = (fn) => async (input) => {
17
+ neverThrow(() => fn((input ?? {})));
18
+ return {}; // no decision: the hook only records
19
+ };
20
+ const common = (i) => ({
21
+ ...(i.tool_use_id ? { tool_use_id: i.tool_use_id } : {}),
22
+ ...(i.agent_id ? { agent_id: i.agent_id } : {}),
23
+ });
24
+ const name = (i) => String(i.tool_name ?? "tool").slice(0, 256);
25
+ return {
26
+ hooks: {
27
+ PreToolUse: [
28
+ {
29
+ hooks: [
30
+ hook((i) => session.event("tool.call", name(i), {
31
+ ...common(i),
32
+ ...(i.tool_input === undefined ? {} : { args: i.tool_input }),
33
+ })),
34
+ ],
35
+ },
36
+ ],
37
+ PostToolUse: [
38
+ { hooks: [hook((i) => session.event("tool.result", name(i), { ok: true, ...common(i) }))] },
39
+ ],
40
+ PostToolUseFailure: [
41
+ {
42
+ hooks: [
43
+ hook((i) => session.event("tool.result", name(i), {
44
+ ok: false,
45
+ ...common(i),
46
+ error: String(i.error ?? "").slice(0, MAX_ERROR),
47
+ })),
48
+ ],
49
+ },
50
+ ],
51
+ },
52
+ observe(message) {
53
+ neverThrow(() => {
54
+ const m = message;
55
+ const id = m?.type === "assistant" ? m.message?.id : undefined;
56
+ // One API response can arrive as several assistant messages (one per content block).
57
+ if (typeof id !== "string" || seen.has(id))
58
+ return;
59
+ seen.add(id);
60
+ session.event("llm.call", "anthropic", {
61
+ provider: "anthropic",
62
+ message_id: id,
63
+ ...(typeof m.message?.model === "string" ? { model: m.message.model } : {}),
64
+ });
65
+ });
66
+ },
67
+ };
68
+ }
69
+ /** Audit S6: the Python SDK's name, so docs and examples can be written once. Called, not `new`ed. */
70
+ export const BlackboxAgentHooks = blackboxAgentHooks;
@@ -0,0 +1,7 @@
1
+ export { blackboxMiddleware } from "./ai-sdk.ts";
2
+ export { BlackboxAgentHooks, blackboxAgentHooks } from "./claude-agent-sdk.ts";
3
+ export { BlackboxCallbackHandler, type BlackboxCallbacks, blackboxCallbacks } from "./langchain.ts";
4
+ export { type MemorySink, type TrackMemoryOptions, trackMemory } from "./memory.ts";
5
+ export { attachBlackbox } from "./openai-agents.ts";
6
+ export { llmIdsOf, wrapAnthropic, wrapBedrock, wrapGemini, wrapOpenAI } from "./providers.ts";
7
+ export { type EventSink, llmIds } from "./shared.ts";
@@ -0,0 +1,10 @@
1
+ // Framework hooks (spec/sdk.md §5): framework callbacks → SDK session events. Agent-asserted structure
2
+ // only; every callback is wrapped so a hook can never break the framework. Mirrors
3
+ // sdks/python/src/zanii_blackbox/hooks/.
4
+ export { blackboxMiddleware } from "./ai-sdk.js";
5
+ export { BlackboxAgentHooks, blackboxAgentHooks } from "./claude-agent-sdk.js";
6
+ export { BlackboxCallbackHandler, blackboxCallbacks } from "./langchain.js";
7
+ export { trackMemory } from "./memory.js";
8
+ export { attachBlackbox } from "./openai-agents.js";
9
+ export { llmIdsOf, wrapAnthropic, wrapBedrock, wrapGemini, wrapOpenAI } from "./providers.js";
10
+ export { llmIds } from "./shared.js";
@@ -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 {};