- Create packages/workflow/ with types.ts (from core/workflow.ts) and config.ts - Core re-exports workflow types from @uncaged/workflow - Delete packages/core/src/workflow.ts Phase 1+2 of #320, Testing: #321
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 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/<name>.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 |
Shared types, config parser, sense trigger validation (parseSenseTrigger), daemon IPC protocol |
@uncaged/nerve-store |
Append-only log SQLite, JSONL archive, CAS blob store, workflow run rows |
@uncaged/nerve-daemon |
Kernel, sense workers, sense scheduler, workflow manager, file watcher, IPC |
@uncaged/nerve-cli |
CLI (nerve) — init, validate, daemon, dev, logs, sense, store, workflow |
Quick Start
# 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.
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):
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 +
onreverse-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
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 — historical sense / scheduling model (superseded in places by stateful senses — see
CLAUDE.md) - RFC-002: Workflow Engine — Stateful workflow execution
- Coding Conventions
License
MIT