This repository has been archived on 2026-06-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
xiaoju 88bd30a1e4 feat(daemon): RFC-003 Phase 2 — AgentRegistry + echo adapter
- createAgentRegistry(agents) returns { get(name): AgentFn }
- get() throws with agent name in message if not found
- Echo adapter (type: 'echo') returns prompt as-is for testing
- Tests: 5 cases covering get/throw/echo/multi-agent/AbortSignal

Closes #236
Ref: #234
2026-04-29 04:49:22 +00:00

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, reacts via declarative Reflexes, and orchestrates multi-step Workflows. Built for the Uncaged agent framework.

Core Concepts

External World → Sense ─┬→ Signal → Reflex → Sense (scheduled compute)
                        │
                        └→ Workflow (Sense return with workflow directive) → Log
Concept Metaphor Role
Sense 👁️ Perception A compute() function that samples or derives data. Each sense has its own SQLite database.
Reflex Reaction Declarative rules that only schedule Sense computes (interval and/or on signal names). Reflex YAML cannot reference workflows.
Signal 📡 Notification Emitted when a sense returns a non-null value that is routed as a normal signal (see Sense → Workflow below). Other reflexes can listen via on.
Workflow 🔧 Action Stateful multi-step execution with Roles and a Moderator. Started from a Sense return value or from CLI/daemon IPC—not from reflex YAML.
Log 📝 Record Immutable audit trail. Queryable by senses, but cannot trigger reflexes (prevents feedback loops).

Sense → Workflow: if compute() returns a plain object with a string field workflow in the form name|maxRounds|prompt (only the first two | delimit name and rounds; the rest is the prompt), the engine starts that workflow and does not emit a Signal for that return. workflow: null or "" means “emit a signal” and strip the key from the payload. Invalid workflow strings are treated like a normal signal (directive stripped). See @uncaged/nerve-core routeSenseComputeOutput / parseSenseWorkflowDirective.

Three extension points for what / when / multi-step action — reflexes never replace Sense-driven workflow launches.

Packages

Package Description
@uncaged/nerve-core Shared types, config parser, Sense→workflow routing, daemon IPC protocol
@uncaged/nerve-store Append-only log SQLite, JSONL archive, CAS blob store, workflow run rows
@uncaged/nerve-daemon Kernel, workers, signal bus, 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
cat > senses/cpu-usage/compute.ts << 'EOF'
export async function compute() {
  const [load] = (await import("node:os")).loadavg();
  return load > 2.0 ? { load } : null;  // signal only when load is high
}
EOF

# Configure reflexes in nerve.yaml
cat > nerve.yaml << 'EOF'
senses:
  cpu-usage:
    group: system
    throttle: 10s

reflexes:
  - kind: sense
    sense: cpu-usage
    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, reflexes (sense-only), optional workflows (concurrency), and optional engine max_rounds:

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

reflexes:
  - kind: sense
    sense: cpu-usage
    interval: 30s          # periodic trigger
    on: [disk-pressure]    # also trigger on signals from other senses

workflows:
  cleanup:
    concurrency: 1
    overflow: drop         # discard if already running
  code-review:
    concurrency: 3
    overflow: queue
    max_queue: 20

YAML must not include workflow: under reflexes — the parser rejects it. Declare workflows under workflows: and start them from Sense compute() or nerve workflow trigger.

Example — Sense starts a workflow (senses/disk-pressure/compute.ts):

export async function compute() {
  const full = await diskNearlyFull();
  if (!full) return null;
  return {
    path: "/data",
    workflow: "cleanup|10|Disk partition nearly full", // name|maxRounds|prompt
  };
}

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  │   │          │        │
│         │            └────┬─────┘   └────┬─────┘   └────┬─────┘        │
│         │                 │              │              │            │
│         │                 └──────────────┼──────────────┘            │
│         │                                ▼                            │
│         │                        ┌──────────────┐                     │
│         │                        │  Signal Bus  │                     │
│         │                        └──────┬───────┘                     │
│         │                               ▼                             │
│         │                     ┌──────────────────┐                      │
│         │                     │ Reflex Scheduler│                      │
│         │                     └────────┬─────────┘                     │
│         │                              ▼                              │
│         │                    ┌───────────────────┐                    │
│         └───────────────────►│ Workflow Manager  │──→ @uncaged/nerve-store │
│                              └───────────────────┘    (logs.db, …)   │
└────────────────────────────────────────────────────────────────────────┘
  • Worker pool — one child process per sense group; isolation between groups.
  • Signal Bus — in-memory pub/sub for signal distribution.
  • Reflex Scheduler — interval timers + signal subscriptions, 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)
  • Drizzle ORM v1.0 for sense databases
  • 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

License

MIT

S
Description
Observation engine — Sense, Reflex, Workflow
Readme 5.4 MiB
Languages
TypeScript 97.2%
HTML 2%
JavaScript 0.5%
Shell 0.3%