# nerve **Observation engine for autonomous agents** — sense the world, react to changes, run workflows. Nerve is a lightweight daemon that continuously observes external state through **Senses** (stateful `compute(state)` + JSON persistence), schedules them via **`interval` / `on`** in `nerve.yaml`, and orchestrates multi-step **Workflows**. Built for the [Uncaged](https://github.com/uncaged) agent framework. ## Core Concepts ``` External World → Sense(state) → { state, trigger? } → (shell in worker) / Log │ Workflow → Log (CLI / daemon IPC only) ↑ scheduling: interval / on (per sense in nerve.yaml) ``` | Concept | Metaphor | Role | |---------|----------|------| | **Sense** | 👁️ Perception | Stateful `compute(state)` returning `{ state, trigger }`. State lives in `data/senses/.json`. | | **Schedule** | ⏱️ When | Each sense entry sets optional `interval` (periodic) and `on: [other senses]` (run after those senses complete a compute). | | **Workflow** | 🔧 Action | Stateful multi-step execution with Roles and a Moderator. Started via CLI / daemon IPC (`nerve workflow trigger`, transport). Not started from sense `compute()` results. | | **Log** | 📝 Record | Immutable audit trail. **Cannot** schedule senses or workflows (prevents feedback loops). | **Sense → shell:** when `trigger` is non-null it must be `{ command: string }`. The sense worker runs it with `shell: true` (cwd = nerve root). Use `trigger: null` when no command should run. To start a workflow, invoke it from that shell command (for example calling the CLI) or trigger workflows separately via IPC. Two extension points for **what to observe (+ when)** vs **multi-step action** — scheduling is declarative config on each sense, not a separate YAML section. ## Packages | Package | Description | |---------|-------------| | [`@uncaged/nerve-core`](./packages/core) | Shared types, config parser, sense trigger validation (`parseSenseTrigger`), daemon IPC protocol | | [`@uncaged/nerve-store`](./packages/store) | Append-only log SQLite, JSONL archive, CAS blob store, workflow run rows | | [`@uncaged/nerve-daemon`](./packages/daemon) | Kernel, sense workers, sense scheduler, workflow manager, file watcher, IPC | | [`@uncaged/nerve-cli`](./packages/cli) | CLI (`nerve`) — init, validate, daemon, dev, logs, sense, store, workflow | ## Quick Start ```bash # Requirements: Node.js ≥ 22.5, pnpm pnpm add -g @uncaged/nerve-cli # Initialize a workspace mkdir my-agent && cd my-agent nerve init # Write a sense (see `nerve init` for the full template) mkdir -p senses/cpu-usage/src cat > senses/cpu-usage/src/index.ts << 'EOF' import { loadavg } from "node:os"; type CpuState = { lastLoad: number }; export const initialState: CpuState = { lastLoad: 0 }; export async function compute(state: CpuState) { const [oneMin] = loadavg(); const lastLoad = typeof oneMin === "number" && !Number.isNaN(oneMin) ? oneMin : 0; return { state: { lastLoad }, workflow: null }; } EOF # Configure scheduling on each sense in nerve.yaml cat > nerve.yaml << 'EOF' senses: cpu-usage: group: system throttle: 10s interval: 30s EOF # Run nerve dev # foreground (development) nerve daemon start # background (production) nerve status # check health nerve logs # view logs ``` ## Configuration `nerve.yaml` declares senses (each with optional `interval` / `on`), optional workflows (concurrency), and optional engine `max_rounds`. Top-level `reflexes` is **not** supported — use `interval` and `on` on each sense. ```yaml max_rounds: 100 # default moderator cap (e.g. CLI workflow trigger) senses: cpu-usage: group: system # senses in the same group share a worker process throttle: 10s # min interval between computes timeout: 30s # max compute duration grace_period: 5s # wait before first compute after startup interval: 30s # periodic trigger derived-example: group: system throttle: null timeout: 30s grace_period: null on: - cpu-usage # run after cpu-usage completes a compute workflows: cleanup: concurrency: 1 overflow: drop # discard if already running code-review: concurrency: 3 overflow: queue max_queue: 20 ``` Declare workflows under `workflows:` and start them from Sense `compute()` (non-null `workflow`) or `nerve workflow trigger`. **Example — Sense starts a workflow** (`senses/disk-pressure/src/index.ts`): ```typescript export const initialState = { checked: false }; export async function compute(state: typeof initialState) { const full = await diskNearlyFull(); if (!full) return { state: { ...state, checked: true }, workflow: null }; return { state: { ...state, checked: true }, workflow: { name: "cleanup", maxRounds: 10, prompt: "Disk partition nearly full", dryRun: false, }, }; } ``` ## Architecture ``` ┌────────────────────────────────────────────────────────────────────────┐ │ Kernel │ │ │ │ ┌──────────────┐ watches nerve.yaml / senses / workflows │ │ │ File Watcher ├──────────────────────────────────────────┐ │ │ └──────────────┘ │ │ │ ┌──────────────┐ CLI ↔ newline JSON (trigger-workflow, │ │ │ │ Daemon IPC │ trigger-sense, list-senses) │ │ │ └──────┬───────┘ ▼ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ Worker │ │ Worker │ │ Worker │ (1 per│ │ │ │ (group A)│ │ (group B)│ │ (group C)│ group) │ │ │ │ sense-1 │ │ sense-3 │ │ sense-5 │ │ │ │ │ sense-2 │ │ sense-4 │ │ │ │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ │ │ └──────────────┼──────────────┘ │ │ │ ▼ │ │ │ ┌──────────────┐ │ │ │ │Sense Scheduler │ │ │ │(interval + on) │ │ │ └──────┬───────┘ │ │ │ ▼ │ │ │ ┌───────────────────┐ │ │ └───────────────────►│ Workflow Manager │──→ @uncaged/nerve-store │ │ └───────────────────┘ (logs.db, …) │ └────────────────────────────────────────────────────────────────────────┘ ``` - **Worker pool** — one child process per sense group; isolation between groups. - **Sense scheduler** — interval timers + `on` reverse-index (upstream sense → dependents), with throttle/coalesce. - **Workflow Manager** — concurrency (drop/queue), per-workflow workers, crash recovery. - **File watcher** — hot reload for config, sense modules, and workflow modules. - **Daemon IPC** — Unix domain socket; used by the CLI when the daemon is running. - **Log / blob storage** — implemented in `@uncaged/nerve-store` (WAL SQLite, JSONL archive, CAS blobs). ## Tech Stack - **Zero native addons** — uses Node.js built-in `node:sqlite` (DatabaseSync) for logs / workflow persistence via `@uncaged/nerve-store` - **Sense state as JSON** — files under `data/senses/` written by sense workers - **rslib** (rspack) for building - **Biome** for formatting/linting - **Vitest** for testing - **pnpm** workspaces for monorepo management ## Development ```bash git clone https://git.shazhou.work/uncaged/nerve.git cd nerve pnpm install pnpm build pnpm -r test # run all tests ``` ## Design Documents - [RFC-001: Observation Engine](./docs/rfc-001-observation-engine.md) — historical sense / scheduling model (superseded in places by stateful senses — see `CLAUDE.md`) - [RFC-002: Workflow Engine](./docs/rfc-002-workflow-engine.md) — Stateful workflow execution - [Coding Conventions](./docs/coding-conventions.md) ## License MIT