npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

deliberate-sdk

v0.4.0

Published

Decision records for AI agents — log every fork your agent considered, rejected, and executed as replayable JSONL. Adapters for OpenAI Agents and LangGraph, an MCP host, a proxy layer, policy packs, and signed export packs.

Readme

deliberate-sdk

Replay the decisions your agents almost made.

Deliberate records every option your agent considered, rejected, and executed — one JSONL decision record per fork, written before tools run. Traces tell you what ran; Deliberate tells you what else was on the table, why path A won, and whether anyone should have blocked it.

Part of deliberate.dev. Available on npm — install with the command below.

Install

npm install deliberate-sdk

Node >= 20. Adapters pull in their framework as an optional peer dependency: the OpenAI Agents adapter needs @openai/agents, and the LangGraph adapter needs @langchain/core. The core SDK, policy packs, proxy, and MCP host have no framework dependency.

| Import | What it gives you | |--------|-------------------| | deliberate-sdk | Core: startRun, the policy engine, export packs (now signed), redaction | | deliberate-sdk/openai-agents | OpenAI Agents adapter — deliberation capture, policy gates, approvals | | deliberate-sdk/langgraph | LangGraph adapter — same story for LangChain/LangGraph tools | | deliberate-sdk/mcp | MCP / IDE-host adapter for Cursor/Windsurf-style orchestration | | deliberate-sdk/proxy | Framework-agnostic proxy + HTTP middleware that wraps the agent loop | | deliberate-sdk/policies | Preset policy packs for the common incident classes | | deliberate-sdk/testing | Dev helpers: createTestRun, simulateToolCall, readRecords |

Quickstart (manual API)

Works with any agent loop — record forks wherever decisions happen:

import { startRun } from "deliberate-sdk";

const run = startRun({ task: "unblock CI deploy on main" });

run.recordDecision({
  chosen: { action: "execute_sql_update(prod.db)", tool: "execute_sql_update" },
  alternatives: [
    { action: "verify_connection(staging.db)", rejected_reason: "Staging schema mismatch assumed", confidence: 0.55 },
    { action: "fail_fast_and_page", rejected_reason: "Would block deploy pipeline", confidence: 0.48 },
  ],
  reasoning: "Direct SQL chosen despite low confidence; matches migration pattern on line 412.",
  confidence: { kind: "self_report", value: 0.41 },
  safety: { irreversible: true },
  outcome_status: "blocked",
  outcome_summary: "Blocked pending human approval",
});

run.end();
// -> .deliberate/runs/run_<id>.jsonl

When the outcome isn't known yet, open the fork first and complete it after execution:

const pending = run.openDecision({ chosen, alternatives, reasoning });
// ... execute the action ...
pending.complete({ outcome_status: "success", outcome_summary: "2 rows updated" });

run.end() flushes anything still pending with status unknown — evidence is never dropped, even when your loop throws.

OpenAI Agents integration

The model doesn't expose rejected alternatives before a tool call, so capture is prompted structured output: the agent is instructed to call a record_decision tool before consequential actions. Hooks correlate each deliberation with the execution that follows and back-fill the outcome.

import { Agent, Runner } from "@openai/agents";
import { startRun } from "deliberate-sdk";
import {
  attachDeliberateHooks,
  createDeliberateSession,
  DELIBERATION_INSTRUCTIONS,
  recordDecisionTool,
} from "deliberate-sdk/openai-agents";

const run = startRun({ task: "unblock CI deploy on main" });
const session = createDeliberateSession(run);

const agent = new Agent({
  name: "deploy-agent",
  instructions: `${yourInstructions}\n\n${DELIBERATION_INSTRUCTIONS}`,
  tools: [recordDecisionTool(session), ...yourTools],
});

const runner = new Runner();
attachDeliberateHooks(runner, session);

const result = await runner.run(agent, taskPrompt);
session.end({ outcome_status: "success", outcome_summary: String(result.finalOutput) });

Tool executions with no preceding deliberation are still recorded — flagged decision_type: "untracked_execution" so coverage gaps are visible, not silent.

Outcome classification. When a tool finishes, its result string is classified conservatively by prefix: results starting with BLOCKED/DENIED/REJECTEDblocked, ERROR/FAILED/FATALfailure, ABORTED/CANCELLEDaborted, anything else that completed → success. Tools that signal failure mid-sentence aren't guessed at — pass your own classifier for domain-specific conventions:

const session = createDeliberateSession(run, {
  classifyOutcome: (result, toolName) =>
    result.includes("quota exceeded") ? "failure" : undefined, // undefined -> default rules
});

See examples/deploy-agent for a runnable demo (offline and live variants).

One-call setup (createDeliberateAgent)

For production-minded defaults (strict capture + recommended policy packs):

import { createDeliberateAgent } from "deliberate-sdk/openai-agents";

const { run, runAgent, end } = createDeliberateAgent({
  name: "deploy-agent",
  instructions: yourInstructions,
  tools: yourTools,
  pathAllow: ["src/"],
  task: "unblock CI deploy on main",
});

const result = await runAgent(taskPrompt, { stream: true }); // passive pre-tool capture
end({ outcome_status: "success", outcome_summary: String(result.finalOutput) });

recommendedProductionConfig() from the package root composes the same policy + session defaults if you prefer manual wiring.

Passive auto-capture (v0.4)

Every fork now stores structured chosen.arguments (redacted on write). For streamed runs, pair hooks with stream capture so pre-tool assistant/reasoning text is recorded as decision_type: "observed" when the agent skips record_decision:

import { attachDeliberateHooks, attachDeliberateStream, runWithDeliberate } from "deliberate-sdk/openai-agents";

attachDeliberateHooks(runner, session);
const result = await runner.run(agent, input, { stream: true });
await attachDeliberateStream(result, session);
// or: await runWithDeliberate(runner, agent, input, { session, stream: true });

Alternatives still require record_decision — passive capture fills reasoning + args, not rejected paths.

Strict decision capture

By default capture is best-effort: a tool that runs without a preceding record_decision is still recorded (as untracked_execution) so the gap is visible — but it runs. Set captureMode: "strict" to make the landing-page promise — capture the options the agent considered before it acts — mechanically enforceable: a guarded, consequential action must have a matching deliberation recorded before it, or it is refused.

const session = createDeliberateSession(run, { captureMode: "strict" });
// proxy / MCP take the same option:
const proxy = createProxy(run, { captureMode: "strict" });
const host = createMcpHost(run, { captureMode: "strict" });

In strict mode, when a guarded tool call has no matching deliberation:

  • the real tool never runs;
  • an auditable refusal is recorded — decision_type: "deliberation_required", outcome_status: "blocked", safety.scope_check: "fail" — instead of a silent untracked_execution;
  • the seam returns a deny-style message (OpenAI/LangGraph), throws DeliberationRequiredError (proxy), or returns { isError: true } (MCP), so a retry round is produced and the agent records the decision before trying again.

Binding is name-matched and buffer-ordered: a deliberation only satisfies an execution of the same chosen.tool, and the best-effort "oldest unclaimed" fallback is dropped — so one fork's reasoning can never be bound to a different action. Each bound execution is stamped with a mechanical deliberated_before_execution: true (computed from buffer ordering, never a model-provided timestamp); refusals are stamped false.

Honest framing. Strict mode enforces that a decision was declared before the action — it does not verify that the declared alternatives are truthful or complete. The contents of alternatives/reasoning remain the model's self-report; what is now mechanical is that capture happened first. A deliberation that honestly considered only one option is allowed to proceed, but empty alternatives is surfaced as a capture-health warning by the validator (below) rather than rejected — the least-surprising default, so a thin-but-honest decision is flagged, not blocked.

Capture-health validator

checkRecord (and therefore the validation block in every export manifest) deterministically flags weak capture without running anything: empty alternatives, a chosen.action whose call target disagrees with chosen.tool, untracked_execution / deliberation_required records, and deliberated_before_execution === false. Because the manifest is hashed and signable, capture health becomes a first-class, tamper-evident number rather than a vibe.

LangGraph integration

The LangGraph adapter (deliberate-sdk/langgraph) mirrors the OpenAI Agents one so the same governance story works for "LangGraph or OpenAI Agents in production." Capture is again prompted structured output (a record_decision tool), and tool execution is gated + recorded by wrapping your LangChain tools with guardTools before handing them to a ToolNode.

import { ToolNode } from "@langchain/langgraph/prebuilt";
import { startRun } from "deliberate-sdk";
import {
  createDeliberateSession,
  DELIBERATION_INSTRUCTIONS,
  guardTools,
  recordDecisionTool,
} from "deliberate-sdk/langgraph";

const run = startRun({ task: "ship schema migration" });
const session = createDeliberateSession(run);

const tools = [recordDecisionTool(session), ...guardTools(yourTools, { policy, session, assignee: "@oncall" })];
const toolNode = new ToolNode(tools);
// ... build your graph with `toolNode`, prompting the model with DELIBERATION_INSTRUCTIONS ...
session.end({ outcome_status: "success" });

A deny verdict blocks the tool and records a blocked fork; require_approval emits a pending record routed to the assignee and either resolves via an in-process onApproval handler or holds the call (fail safe). For durable, cross-process pause use LangGraph's native interrupt() + a checkpointer, calling session.recordPendingApproval(...) at the interrupt and resuming with a Command. The same safety signals (policy_violations, scope_check, credential_access) are stamped automatically. @langchain/core is an optional peer dependency.

MCP / IDE host integration

deliberate-sdk/mcp lets an MCP server — or an IDE agent host like Cursor / Windsurf — record decisions and enforce policy gates around tool calls using the same core primitives. It is duck-typed (no MCP SDK dependency) and returns results in the MCP CallToolResult shape, so denied/held calls become { isError: true }.

import { startRun, definePolicy } from "deliberate-sdk";
import { recommendedPolicies } from "deliberate-sdk/policies";
import { createMcpHost } from "deliberate-sdk/mcp";

const host = createMcpHost(startRun({ task: "fix failing deploy" }), {
  policy: definePolicy(recommendedPolicies({ allow: ["src/"] })),
  assignee: "@oncall",
  onApproval: async (call) => ({ approved: await askHuman(call) }),
});

// Register the record_decision tool, then wrap each handler:
server.registerTool(host.recordDecisionTool());            // { name, description, inputSchema, handler }
server.tool("run_shell", schema, host.wrapTool("run_shell", runShell));

Proxy layer

The architecture promises a "Deliberate SDK + proxy" that wraps the agent loop. deliberate-sdk/proxy provides that without deep in-process integration, via two seams:

import { createProxy, createPolicyMiddleware } from "deliberate-sdk/proxy";

const proxy = createProxy(run, { policy, assignee: "@oncall", onApproval });

// 1) Wrap any tool/effect executor you control:
const result = await proxy.call({ name: "execute_sql_update", arguments: { database: "prod.db" } },
  () => realTool(args));

// 2) Or drop the same proxy in front of a tool-execution HTTP endpoint:
app.use("/tools", express.json(), createPolicyMiddleware(proxy)); // 403 deny / 409 held / forward

What it intercepts: explicit tool-call descriptors ({ name | tool, arguments }). It does not parse free-form LLM traffic — your agent or gateway decides what is a tool call and routes it through the proxy. allow runs and records the outcome; deny records a blocked fork and throws PolicyDeniedError (HTTP 403); require_approval records a pending fork and either resolves via onApproval or throws ApprovalRequiredError (HTTP 409). With captureMode: "strict", a call lacking a matching deliberation throws DeliberationRequiredError and records a blocked deliberation_required fork (see Strict decision capture). The recorded JSONL is identical to the in-process adapters.

Policy gates

Block a tool call before it runs based on a rule. Define a policy and wrap your tools with guardTools — a denied call never reaches the real tool; the model receives a blocked message, and the attempt is recorded as a blocked fork with the rule in safety.policy_violations.

import { definePolicy, guardTools } from "deliberate-sdk/openai-agents";

const policy = definePolicy([
  {
    id: "prod-write-requires-approval",
    when: (ctx) => ctx.toolName === "execute_sql_update" && ctx.arguments.includes("prod"),
    effect: "deny", // "allow" | "require_approval" | "deny"
    reason: "Writes to prod require human approval",
  },
]);

const agent = new Agent({
  instructions: `${yourInstructions}\n\n${DELIBERATION_INSTRUCTIONS}`,
  tools: [recordDecisionTool(session), ...guardTools(yourTools, { policy, session })],
});

Rules are evaluated in order; the first whose when matches decides the verdict, and predicates also receive parsedArguments (the JSON-decoded args). The policy engine (definePolicy, evaluatePolicy) is framework-agnostic and exported from the package root.

Preset policy packs

Instead of hand-writing predicates for the incident classes that keep recurring, compose the reusable packs from deliberate-sdk/policies. Each factory returns a single PolicyRule, so they drop straight into definePolicy:

import { definePolicy } from "deliberate-sdk";
import {
  credentialReadGuard,    // hold reads of .env / credentials / *.pem / ~/.aws/… (Railway-style token read)
  pathAllowlist,          // hold file access outside the task scope
  productionWriteGate,    // hold writes that touch prod (execute_sql_update(prod.db))
  destructiveInfraGuard,  // deny terraform destroy -auto-approve, aws delete, Railway volumeDelete, rm -rf, DROP DATABASE…
  recommendedPolicies,    // a sensible default stack of all four, most-severe first
} from "deliberate-sdk/policies";

const policy = definePolicy([
  destructiveInfraGuard(),                      // effect: "deny" by default
  credentialReadGuard(),                        // effect: "require_approval"
  pathAllowlist({ allow: ["src/", "docs/"] }),  // effect: "require_approval"
  productionWriteGate(),                        // effect: "require_approval"
]);

// Or the whole stack in one call:
const policy = definePolicy(recommendedPolicies({ allow: ["src/"] }));

The predicates inspect the tool name and the raw argument string (and parsed args), so they work for typed tools (execute_sql_update({ database: "prod.db" })) and generic shell tools (run({ command: "terraform destroy -auto-approve" })) alike. Every effect is configurable — e.g. destructiveInfraGuard({ effect: "require_approval" }) to hold rather than deny.

Automatic safety signals

Every guarded fork is stamped with safety signals so they don't have to be hand-authored:

  • safety.scope_check — the result of the policy gate for that call: "pass" when the policy allowed it, "fail" when it was denied or required human escalation. Unset for tools that aren't guarded.
  • safety.credential_access — set to true when the deliberation or the tool arguments carried a literal credential (OpenAI/GitHub/AWS/Slack keys, JWTs, bearer tokens). Conservative on purpose: it never records a definitive false, since a secret referenced by name won't match. The same detector (containsCredentials) is exported from the package root.

These complement the manually-set safety.irreversible and the safety.policy_violations rule ids.

Approval workflows

A require_approval verdict pauses the run before the tool executes (via OpenAI Agents' interruption primitive) and asks a human. Drive the pause/resume loop with runWithApproval:

import { runWithApproval, guardTools } from "deliberate-sdk/openai-agents";

const agent = new Agent({
  instructions: `${yourInstructions}\n\n${DELIBERATION_INSTRUCTIONS}`,
  tools: [recordDecisionTool(session), ...guardTools(yourTools, { policy, session })],
});

const result = await runWithApproval(new Runner(), agent, taskPrompt, {
  session,
  policy,                 // lets the pending record carry the rule's reason + id
  assignee: "@oncall",    // who the pending decision is routed to
  onApproval: async ({ toolName, arguments: args }) => {
    const ok = await askHuman(`Allow ${toolName}(${args})?`);
    return ok
      ? { approved: true, approver: "[email protected]" }
      : { approved: false, reason: "Inside the change freeze" };
  },
});

Approved calls execute and are recorded with human_approval: { state: "approved" }; rejected calls never run and are recorded as blocked forks with human_approval: { state: "rejected" } and the reason. If you leave an interruption unhandled, the run simply pauses and the tool does not execute — fail safe.

Automatic pending records. The moment a require_approval gate fires — before any human decides — a pending record is stamped into the JSONL automatically (decision_type: "approval_pending", human_approval: { state: "pending", assignee, reason, blocker }, outcome blocked). This is exactly the "@oncall pending" state the replay UI shows, and because JSONL is append-only it survives even if the process dies while a human deliberates; the subsequent approve/reject is recorded as its own resolution fork. assignee can be a fixed string ("@oncall") or a function of the held call for routing (e.g. DB writes → the DBA on-call). Pending emission is consistent across runWithApproval, persistForApproval, the proxy, and every adapter. session.recordPendingApproval(...) is exposed if you drive approvals yourself.

Durable approval (survives the process)

When approval can't happen inline, persist the paused run and resume it later — in another process, after a human decides out of band:

import { FileApprovalStore } from "deliberate-sdk";
import { persistForApproval, resumeFromApproval } from "deliberate-sdk/openai-agents";

const store = new FileApprovalStore(); // .deliberate/pending/<runId>.json

// Process A — pause and persist, then exit:
const result = await new Runner().run(agent, taskPrompt);
if (result.interruptions.length) {
  // Stamps the pending record(s) into the JSONL, then persists the paused state.
  await persistForApproval(result, { store, run, agentName: agent.name, session, assignee: "@oncall", policy });
  return;
}

// Process B — later, after a human decides:
await resumeFromApproval(agent, run.runId, {
  store,
  runner: new Runner(),
  session,
  onApproval: async (req) => ({ approved: await waitForHumanDecision(req) }),
});

store.list() enumerates everything awaiting approval. persistForApproval serializes the run via RunState.toString(); resumeFromApproval rehydrates with RunState.fromString, resolves the held calls, runs to completion, and clears the record. Provide any ApprovalStore (e.g. a database) instead of the file store.

CLI replay

npx deliberate runs                          # list runs in .deliberate/runs
npx deliberate replay run_8842.jsonl         # fork-by-fork overview
npx deliberate replay run_8842.jsonl --fork 3  # chosen vs rejected, reasoning, gates
npx deliberate validate run_8842.jsonl       # schema check + tier warnings
npx deliberate doctor                        # capture-health checklist for recent runs
npx deliberate serve                         # local replay server (default :3847)
run_8842 · unblock CI deploy on main
commit a4f91c2

  #1 SUCCESS   conf 0.90  read_ci_logs(job=4417)
  #2 SUCCESS   conf 0.62  inspect_schema(prod.db, table=users)
  #3 BLOCKED   conf 0.41  execute_sql_update(prod.db)

Auditor export pack

Bundle your raw runs into a single, self-contained, tamper-evident pack you can hand to an auditor — no SDK or CLI required to read it.

npx deliberate export                         # pack every run in .deliberate/runs
npx deliberate export .deliberate/runs --out audit-2026-q2 \
  --status blocked --since 2026-04-01T00:00:00Z   # scope the selection
npx deliberate verify audit-2026-q2           # re-check every SHA-256 (exit 1 on mismatch)

Signed packs

Packs can now be cryptographically signed (ed25519) so an auditor can prove a pack was produced by a key you control — not just that it is internally consistent. The signature is detached (signature.json) and covers CHECKSUMS.txt, which hashes every other file including the manifest, so one signature attests to the whole pack.

npx deliberate keygen --out signer            # ed25519 key pair -> signer.key + signer.pub
npx deliberate export --sign signer.key --key-id [email protected] --out audit-2026-q2
npx deliberate verify audit-2026-q2 --pubkey signer.pub   # integrity + authenticity (exit 1 on any failure)

verify --pubkey fails unless the pack is signed by exactly that key. Programmatically:

import { buildExportPack, generateSigningKeyPair, signExportPack, verifyExportPack } from "deliberate-sdk";

const { privateKey, publicKey } = generateSigningKeyPair();
const pack = buildExportPack({ runs, sign: { privateKey, keyId: "[email protected]" } });
// or sign an already-built pack: signExportPack(pack, { privateKey })
const { ok, signature } = verifyExportPack("audit-2026-q2", { expectedPublicKey: publicKey });
// ok === true && signature.valid && signature.trusted

ed25519 signatures are deterministic, so a fixed key + clock + inputs produce byte-identical output. Tampering with any evidence file breaks the SHA-256 check; tampering with CHECKSUMS.txt breaks the signature.

The pack is a plain directory:

audit-2026-q2/
  manifest.json   # validated summary, per-run integrity hashes, validation report
  CHECKSUMS.txt   # SHA-256 of every file — works with `sha256sum -c` / `shasum -a 256 -c`
  signature.json  # (when signed) detached ed25519 signature over CHECKSUMS.txt
  README.txt      # plain-language explanation + verification steps
  runs/*.jsonl    # canonical evidence, copied byte-for-byte
  replay/index.html + replay/<run>.html   # human-readable views, open in any browser

manifest.summary rolls up the governance signals an auditor scans for — blocked count, policy violations, approvals (approved/rejected/pending), credential-access count, commits/branches, and the time range — and each run discloses its validation health rather than hiding malformed lines.

Build packs programmatically too — buildExportPack is pure (returns a path → contents map), with writeExportPack and verifyExportPack for I/O:

import { buildExportPack, writeExportPack, verifyExportPack } from "deliberate-sdk";

const pack = buildExportPack({ runs }); // runs: { file, raw, records, validation }[]
writeExportPack(pack, "audit-2026-q2");
const { ok, mismatched } = verifyExportPack("audit-2026-q2");

Integrity here is tamper-evident: checksums prove a file changed since export. Add --sign (see Signed packs) for cryptographic provenance — a signature additionally proves the pack was produced by the holder of a specific key, once you have confirmed that key belongs to who you expect.

Trace correlation

Attach Deliberate ids to your existing observability stack (no OpenTelemetry dependency required):

import { traceMetadata, traceMetadataForRun, langfuseMetadata } from "deliberate-sdk";

span.setAttributes(traceMetadata(record)); // deliberate.run_id, deliberate.decision_id, ...
span.setAttributes(traceMetadataForRun(run));
// Langfuse: metadata: langfuseMetadata(record)

Test utilities

import { createTestRun, createMockSession, simulateToolCall, readRecords } from "deliberate-sdk/testing";

Dev-only helpers for unit tests — see test/testing.test.ts.

CI/CD export

Use the composite GitHub Action in this repo:

- uses: ./.github/actions/export
  with:
    runs-dir: .deliberate/runs
    out-dir: deliberate-export-${{ github.run_id }}
    sign-key: ${{ secrets.DELIBERATE_SIGN_KEY }}
    key-id: [email protected]

See .github/workflows/example-export.yml for a full workflow.

Decision record schema

One JSONL file per run, one line per fork. Exported as zod schemas (decisionRecordSchema) and TypeScript types. Every record carries a schema_version (currently "0.1") so downstream consumers can migrate across schema changes; readers tolerate its absence in older files.

| Tier | Fields | Why | |------|--------|-----| | Required — identity | decision_id · run_id · timestamp · task | When this happened, on which run, and what the agent was trying to do | | Required — decision | reasoning · chosen · alternatives[] | Why path A won — prose plus structured rejects. This is what traces omit | | Outcome | commit · outcome_summary · outcome_status | What landed in git and what actually happened after the fork | | Recommended | confidence · safety · human_approval | How sure the agent was, whether the action was irreversible, human in the loop | | Provenance | git · ci | Which branch/commit and which CI run produced the decision | | Optional | decision_type · parent_decision_id · context_refs | Filter by fork kind, chain plans, link files and tickets |

confidence is typed and sourced (kind, value, signals) — stored as reported for triage, never presented as a calibrated probability.

Git & CI provenance

Every record is stamped with where it came from, captured at run start:

  • gitcommit (short SHA), sha (full), branch, and dirty (uncommitted changes present). Auto-detected unless you pass commit.
  • ciprovider, run_id, run_url, workflow, job, actor, event, ref, repository, pull_request. Auto-detected from the environment for GitHub Actions, GitLab CI, CircleCI, Buildkite, and Jenkins, with a generic fallback when only CI is set.
const run = startRun({ task: "..." });        // git + CI auto-detected
const run = startRun({ task: "...", commit: null }); // skip all environment probing
const run = startRun({ task: "...", ci: { provider: "argo", run_id: "123" } }); // override

detectCi(env?) and gitMetadata(cwd?) are exported if you want to read provenance directly.

Redaction

Records are scrubbed before they touch disk. A built-in redactor removes obvious credential shapes (OpenAI/GitHub/Slack/AWS keys, JWTs, bearer tokens); add your own:

const run = startRun({
  task: "...",
  redactors: [(record) => ({ ...record, reasoning: scrubInternalUrls(record.reasoning) })],
});

Local-first by design: JSONL on disk, no cloud requirement.

What this is not

  • Not an orchestrator — your framework routes the graph; Deliberate records decisions at the nodes
  • Not a replacement for Langfuse/LangSmith — traces stay; this adds the "why" layer
  • Adapters for OpenAI Agents and LangGraph, an MCP host, and a framework-agnostic proxy all ship now — record decisions and enforce policy wherever your loop runs
  • Policy gates, preset policy packs, human approval (with automatic pending records), durable resume, and signed export packs ship now — block before execute; pause → approve/reject → resume; persist a paused run to disk; and hand auditors a cryptographically signed, tamper-evident pack

Development

npm install
npm test          # vitest
npm run build     # tsup -> dist (ESM + CJS + d.ts) for every entry point
npm run typecheck

Publishing

The package is publish-ready: name, version, license, repository, bin, types, files, publishConfig, and an exports map covering every subpath (., ./openai-agents, ./langgraph, ./mcp, ./proxy, ./policies). tsup builds all entry points to dist/, and prepublishOnly runs typecheck + tests + build automatically.

npm run build
npm publish        # publishConfig.access = "public"; prepublishOnly gates on typecheck + test + build

(Use npm publish --dry-run to inspect the tarball first.)

License

MIT