Node ≥ 20 · TypeScript-first · zero runtime dependencies. npm install @nexusaiframework/runtime
npm install @nexusaiframework/runtime # the runtime, zero deps # or work from source, to run the tests and the offline demo: git clone https://github.com/BULMKT/NexusAIAgentFramework cd NexusAIAgentFramework npm install # dev deps only, the runtime itself has zero npm test # 171 tests npm run demo # full lifecycle offline, no API keys
import { NexusRuntime, FileStorage, AnthropicProvider, defineAgent, defineTool } from '@nexusaiframework/runtime'; const sendEmail = defineTool({ name: 'send_email', description: 'Send an email', parameters: { type: 'object', properties: { to: { type: 'string' } }, required: ['to'] }, requiresApproval: true, async execute({ to }) { /* … */ return `sent to ${to}`; }, }); const runtime = new NexusRuntime({ storage: new FileStorage('.nexus') }); runtime.register(defineAgent({ name: 'assistant', provider: new AnthropicProvider(), // or OpenAIProvider / OllamaProvider model: 'claude-sonnet-4-5', systemPrompt: 'You are a helpful operations assistant.', tools: [sendEmail], maxTurns: 10, budget: { maxUsd: 0.50, maxTotalTokens: 100_000 }, })); const run = await runtime.run('assistant', 'Email the team a status update.'); if (run.status === 'waiting_approval') { const done = await runtime.approve(run.id); // or from another process, later console.log(done.result); }
A tool is a name, description, JSON-Schema parameters, and an execute function. Set requiresApproval: true and the run pauses in waiting_approval before that tool ever executes. Safe sibling calls in the same turn run immediately; their results are buffered and never re-executed after the pause resolves. Denials become error tool-results the model sees and reacts to. Beyond approve and deny, a reviewer can approve with edited arguments, approve(runId, callId, { edited }), so the tool runs with corrected input, or answer the call directly, respond(runId, callId, text), so the tool never runs at all. Every variant is journaled with who decided.
Automate the easy decisions; escalate the rest. A throwing policy fails safe to 'ask'.
approvalPolicy: ({ toolName, arguments: a }) =>
toolName === 'issue_refund' && Number(a.amountUsd) < 50
? 'approve' // journaled: decidedBy 'policy'
: 'ask', // pause for a human, decidedBy 'human'
budget.maxUsd and budget.maxTotalTokens are enforced before each LLM call. At the cap the run halts as halted_budget, journaled, notifiable, resumable after you raise the limit. Costs use a built-in pricing table (override via new NexusRuntime({ pricing })); unknown models cost $0 but token caps still apply.
running → waiting_approval → running → completed running → halted_budget | halted_max_turns (resumable) running → failed | cancelled (terminal) await runtime.resume(runId); // crashed process? new deploy? same run continues await runtime.cancel(runId);
Tool execution is at-least-once on crash resume: if the process dies after a tool ran but before its result persisted, the tool re-executes. Results already recorded are never re-executed.
import { McpClient, mcpTools } from '@nexusaiframework/runtime'; const client = await McpClient.connect({ command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '/data'], // or: url: 'https://mcp.example.com/mcp' (Streamable HTTP, SSE supported) }); const tools = await mcpTools(client, { requiresApproval: ['write_file'], // or true, or (name) => boolean namespace: 'fs', // tools become fs__read_file, … include: ['read_file', 'write_file'], });
The reverse also ships: nexus mcp exposes Nexus itself as an MCP server over stdio, so Claude, Cursor, or any MCP client can drive durable runs, run, approve, deny, respond, resume, and list, as MCP tools.
$ nexus mcp # run/approve/deny/respond/resume/list over stdio
import { replayRun } from '@nexusaiframework/runtime'; // exact replay, offline, $0.00, verifies determinism const report = await replayRun(run); report.identical; // true // regression replay, recorded tools + live candidate model/prompt const reg = await replayRun(run, { provider: candidate, systemPrompt: v2 }); reg.divergences; // each tool call where behavior departed
| MemoryStorage | tests & throwaway scripts, not durable |
| FileStorage('.nexus') | JSON snapshot + JSONL journal, atomic writes, the zero-dep default |
| SqliteStorage | single SQLite file via built-in node:sqlite (Node 22+), import from '@nexusaiframework/runtime/sqlite' |
| PostgresStorage | multi-node deployments; bring-your-own pg client so it stays zero-dep, import from '@nexusaiframework/runtime/postgres' |
Four adapters ship in the box, memory, file, SQLite, and Postgres, over one Storage interface of five methods. Redis and others are straightforward contributions.
runtime.on('approval:required', ({ run, approvals }) => { /* … */ }); // also: run:start, llm:call, llm:token (streamed), llm:response, // tool:start, tool:end, retry, run:complete, run:failed, // run:halted, notify:error import { attachWebhook } from '@nexusaiframework/runtime'; attachWebhook(runtime, { url: process.env.SLACK_WEBHOOK_URL!, format: 'slack' }); attachWebhook(runtime, { url: 'https://ops.internal/hooks/nexus' }); // JSON
nexus serve starts the self-hosted dashboard (default 127.0.0.1:3838): live run list, transcripts, journals, approve/edit/deny/respond buttons, a stale-approval age badge, plus a JSON API at /api/runs. Pass --token (or NEXUS_DASHBOARD_TOKEN) to gate the API behind a shared secret; it is a gate, not a substitute for putting it behind your SSO/proxy.
One call attaches an OTLP exporter. Nexus emits GenAI-convention spans, invoke_agent, chat, execute_tool, with token usage, zero dependencies, prompt content off by default. Runs land in Datadog, Grafana, Langfuse, or any OTLP backend, no vendor SDK.
import { attachOtel } from '@nexusaiframework/runtime'; attachOtel(runtime, { endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT });
Tag any run and the tag flows through the record, journal, webhooks, and every span; then filter by it. This is how you attribute an LLM bill to the customer that generated it.
await runtime.run('support', input, { tags: { customerId: 'acme-42' } }); const theirs = await runtime.listRuns({ tags: { customerId: 'acme-42' } });
Providers also stream tokens as an llm:token event, and runtime.cancel(runId) aborts a live model call in flight, not just at the next checkpoint.
The runtime-level controls a security review checks for. The perimeter (SSO, SIEM, secrets, egress) stays yours; these are the parts Nexus owns.
// Kill switch, one command for on-call: await runtime.cancelAll(); # every non-terminal run, cross-process runtime.disableAgent('support', 'incident'); # refuse new runs // Page someone when approvals blow their SLA: import { watchStaleApprovals } from '@nexusaiframework/runtime'; watchStaleApprovals(runtime, { olderThanMs: 30*60_000, onStale: (run) => pagerduty.trigger(run.id, run.waitingMs) }); // Scrub PII from the audit journal before it ships to your SIEM. // The live record keeps real content so the run can resume. new NexusRuntime({ storage, redactStep: (type, detail) => redactPii(detail) });
nexus run <agent> <input…> # start a run and follow it nexus runs --status waiting_approval nexus show <runId> --steps # full audit journal nexus approve <runId> [callId] # --edit '{"amountUsd":50}' to fix args nexus deny <runId> [callId] --reason "…" nexus respond <runId> [callId] --text "…" # answer, tool never runs nexus resume <runId> nexus replay <runId> # offline re-execution + diff nexus runs --stale 30m # approvals past their SLA nexus cancel-all --agent support # kill switch nexus serve --token $TOKEN # dashboard + JSON API (gated) nexus mcp # expose Nexus as an MCP server (stdio) nexus agents # config: nexus.config.mjs default-exporting a NexusRuntime; --json everywhere
| new NexusRuntime(opts) | { storage?, pricing?, retry?, redactStep? } |
| runtime.register(agent) | agent: name, provider, model, systemPrompt?, tools?, maxTurns?, budget?, approvalPolicy? |
| runtime.run(name, input) | drives to terminal or paused state; returns the RunRecord, inspect status |
| approve / deny / respond / resume / cancel | by runId (+ toolCallId); approve(…, { edited }) fixes arguments, respond(…, text) answers a call without running it, cancel aborts in-flight |
| run(name, input, { tags }) | tags ride the record, journal, webhooks, and OTel spans |
| getRun / listRuns / getSteps | read the record and journal; listRuns({ status, tags }) filters |
| providers | AnthropicProvider · OpenAIProvider · OllamaProvider · MockProvider (scriptable, for tests); token streaming via llm:token |
| replayRun / createReplayProvider / createReplayTools | deterministic + regression replay |
| McpClient.connect / mcpTools · nexus mcp | wrap MCP servers (stdio + Streamable HTTP), or serve Nexus as one |
| cancelAll / disableAgent / enableAgent | the kill switch: stop every non-terminal run, refuse new runs of an agent |
| watchStaleApprovals / staleApprovals | page someone when an approval waits past its SLA |
| attachOtel / attachWebhook / createDashboard | OTLP tracing · Slack/JSON notifications · self-hosted UI ({ token } to gate) |
| validateRun / validateStep / validateJournal | zero-dep OAR conformance checks (oar/0.2) |
| storage | MemoryStorage · FileStorage · SqliteStorage · PostgresStorage |
The persisted format is the Open Agent Run spec (CC0, now oar/0.2) with hosted JSON Schemas and two independent conformance implementations: the zero-dep validate* functions above and a stdlib-only Python reader (python -m oar validate|timeline). Full source, tests, and examples: GitHub.