docs: update all docs/conventions for stateful sense, remove stale refs
Phase 4 of RFC #308: Stateful Sense refactor. - CLAUDE.md: updated diagram, tables, examples (no more Signal) - Cleaned stale Signal Bus / DrizzleDB / _signals / retention refs across READMEs, .cursor rules, copilot instructions, .knowledge - Removed drizzle-orm from core package.json (no longer used) - Updated pnpm-lock.yaml Refs #308
This commit is contained in:
+18
-20
@@ -9,7 +9,7 @@ alwaysApply: true
|
||||
## Core Concepts
|
||||
|
||||
```
|
||||
External World → Sense → Signal → Reflex → Workflow → Log
|
||||
External World → Sense(state) → { newState, workflow? } → Workflow → Log
|
||||
↑ ↑
|
||||
"what to observe" "what to do"
|
||||
```
|
||||
@@ -20,19 +20,17 @@ External World → Sense → Signal → Reflex → Workflow → Log
|
||||
|
||||
| Concept | What it is |
|
||||
|---------|-----------|
|
||||
| **Sense** | A `compute()` function that samples or derives data. Returns `T \| null` — non-null emits a Signal, null is silent. Each Sense has its own SQLite database. |
|
||||
| **Signal** | A notification emitted when a Sense returns non-null. Pure fact, no intent. Distributed via an in-memory Signal Bus. Not persisted. |
|
||||
| **Reflex** | A declarative trigger (YAML) connecting Senses to actions. Trigger types: `interval` (periodic), `on` (react to Signals). Action types: trigger a Sense, or start a Workflow. |
|
||||
| **Sense** | A stateful `compute(state)` function. Returns new state and an optional workflow trigger. State persisted as JSON. Scheduling (`interval`, `on`) is configured per sense in nerve.yaml. |
|
||||
| **Workflow** | A stateful multi-step execution. Contains **Roles** (actors with side effects) and a **Moderator** (pure router). Each instance is a **Thread** with a unique `runId`. |
|
||||
| **Log** | Immutable audit trail. Records executions, state transitions, errors. **Cannot trigger Reflexes** — prevents feedback loops. |
|
||||
| **Engine** | The kernel orchestrating everything. Holds Signal Bus, Reflex Scheduler, Process Manager, Workflow Manager. Never loads user code directly — all user code runs in isolated Workers. |
|
||||
| **Log** | Immutable audit trail. Records executions, state transitions, errors. **Cannot trigger senses or workflows** — prevents feedback loops. |
|
||||
| **Engine** | The kernel orchestrating everything. Holds Process Manager, Workflow Manager, Sense Scheduler. Never loads user code directly — all user code runs in isolated Workers. |
|
||||
| **Daemon** | The `nerve-daemon` package — engine runtime. Runs as a background process. |
|
||||
|
||||
### Architecture Rules
|
||||
|
||||
- **Three orthogonal extension points**: Sense (what to compute), Reflex (when to compute), Workflow (what to do)
|
||||
- **Two orthogonal extension points**: Sense (what to observe + when), Workflow (what to do)
|
||||
- **Process isolation**: One worker per Sense group (long-lived), one per Workflow type (on-demand). Workers never talk to each other.
|
||||
- **Causality is one-directional**: External world → Sense → Signal → Reflex → Action + Log. Logs are the end of the chain.
|
||||
- **Causality is one-directional**: External world → Sense(state) → Workflow (when triggered) + Log. Logs are the end of the chain.
|
||||
|
||||
|
||||
|
||||
@@ -44,18 +42,18 @@ Use `function` + `type`, not `class` + `interface`.
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
type Signal = {
|
||||
senseId: string;
|
||||
value: unknown;
|
||||
type WorkflowLaunch = {
|
||||
senseName: string;
|
||||
workflowName: string;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
function createSignal(senseId: string, value: unknown): Signal {
|
||||
return { senseId, value, ts: Date.now() };
|
||||
function recordWorkflowLaunch(senseName: string, workflowName: string): WorkflowLaunch {
|
||||
return { senseName, workflowName, ts: Date.now() };
|
||||
}
|
||||
|
||||
// ❌ Bad — no class, no interface
|
||||
class Signal implements ISignal { ... }
|
||||
class WorkflowLaunch implements IWorkflowLaunch { ... }
|
||||
```
|
||||
|
||||
### Rules
|
||||
@@ -100,9 +98,9 @@ For mutually exclusive fields, use discriminated unions:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
type ReflexConfig =
|
||||
| { kind: "sense"; sense: string; interval: string | null; on: string[] | null }
|
||||
| { kind: "workflow"; workflow: string; on: string[] | null };
|
||||
type WorkflowConfig =
|
||||
| { concurrency: number; overflow: "drop" }
|
||||
| { concurrency: number; overflow: "queue"; maxQueue: number };
|
||||
```
|
||||
|
||||
## Modules & Exports
|
||||
@@ -123,9 +121,9 @@ export default function startEngine() { ... }
|
||||
|
||||
| Type | Style | Example |
|
||||
|------|-------|---------|
|
||||
| Files | kebab-case | `signal-bus.ts` |
|
||||
| Types | PascalCase | `SignalBus` |
|
||||
| Functions/variables | camelCase | `createSignalBus` |
|
||||
| Files | kebab-case | `sense-scheduler.ts` |
|
||||
| Types | PascalCase | `SenseScheduler` |
|
||||
| Functions/variables | camelCase | `createSenseScheduler` |
|
||||
| Constants | UPPER_SNAKE | `MAX_RETRY_COUNT` |
|
||||
| Generics | Single letter or descriptive | `T`, `TValue` |
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Core Concepts
|
||||
|
||||
```
|
||||
External World → Sense → Signal → Reflex → Workflow → Log
|
||||
External World → Sense(state) → { newState, workflow? } → Workflow → Log
|
||||
↑ ↑
|
||||
"what to observe" "what to do"
|
||||
```
|
||||
@@ -14,19 +14,17 @@ External World → Sense → Signal → Reflex → Workflow → Log
|
||||
|
||||
| Concept | What it is |
|
||||
|---------|-----------|
|
||||
| **Sense** | A `compute()` function that samples or derives data. Returns `T \| null` — non-null emits a Signal, null is silent. Each Sense has its own SQLite database. |
|
||||
| **Signal** | A notification emitted when a Sense returns non-null. Pure fact, no intent. Distributed via an in-memory Signal Bus. Not persisted. |
|
||||
| **Reflex** | A declarative trigger (YAML) connecting Senses to actions. Trigger types: `interval` (periodic), `on` (react to Signals). Action types: trigger a Sense, or start a Workflow. |
|
||||
| **Sense** | A stateful `compute(state)` function. Returns new state and an optional workflow trigger. State persisted as JSON. Scheduling (`interval`, `on`) is configured per sense in nerve.yaml. |
|
||||
| **Workflow** | A stateful multi-step execution. Contains **Roles** (actors with side effects) and a **Moderator** (pure router). Each instance is a **Thread** with a unique `runId`. |
|
||||
| **Log** | Immutable audit trail. Records executions, state transitions, errors. **Cannot trigger Reflexes** — prevents feedback loops. |
|
||||
| **Engine** | The kernel orchestrating everything. Holds Signal Bus, Reflex Scheduler, Process Manager, Workflow Manager. Never loads user code directly — all user code runs in isolated Workers. |
|
||||
| **Log** | Immutable audit trail. Records executions, state transitions, errors. **Cannot trigger senses or workflows** — prevents feedback loops. |
|
||||
| **Engine** | The kernel orchestrating everything. Holds Process Manager, Workflow Manager, Sense Scheduler. Never loads user code directly — all user code runs in isolated Workers. |
|
||||
| **Daemon** | The `nerve-daemon` package — engine runtime. Runs as a background process. |
|
||||
|
||||
### Architecture Rules
|
||||
|
||||
- **Three orthogonal extension points**: Sense (what to compute), Reflex (when to compute), Workflow (what to do)
|
||||
- **Two orthogonal extension points**: Sense (what to observe + when), Workflow (what to do)
|
||||
- **Process isolation**: One worker per Sense group (long-lived), one per Workflow type (on-demand). Workers never talk to each other.
|
||||
- **Causality is one-directional**: External world → Sense → Signal → Reflex → Action + Log. Logs are the end of the chain.
|
||||
- **Causality is one-directional**: External world → Sense(state) → Workflow (when triggered) + Log. Logs are the end of the chain.
|
||||
|
||||
|
||||
|
||||
@@ -38,18 +36,18 @@ Use `function` + `type`, not `class` + `interface`.
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
type Signal = {
|
||||
senseId: string;
|
||||
value: unknown;
|
||||
type WorkflowLaunch = {
|
||||
senseName: string;
|
||||
workflowName: string;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
function createSignal(senseId: string, value: unknown): Signal {
|
||||
return { senseId, value, ts: Date.now() };
|
||||
function recordWorkflowLaunch(senseName: string, workflowName: string): WorkflowLaunch {
|
||||
return { senseName, workflowName, ts: Date.now() };
|
||||
}
|
||||
|
||||
// ❌ Bad — no class, no interface
|
||||
class Signal implements ISignal { ... }
|
||||
class WorkflowLaunch implements IWorkflowLaunch { ... }
|
||||
```
|
||||
|
||||
### Rules
|
||||
@@ -94,9 +92,9 @@ For mutually exclusive fields, use discriminated unions:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
type ReflexConfig =
|
||||
| { kind: "sense"; sense: string; interval: string | null; on: string[] | null }
|
||||
| { kind: "workflow"; workflow: string; on: string[] | null };
|
||||
type WorkflowConfig =
|
||||
| { concurrency: number; overflow: "drop" }
|
||||
| { concurrency: number; overflow: "queue"; maxQueue: number };
|
||||
```
|
||||
|
||||
## Modules & Exports
|
||||
@@ -108,9 +106,9 @@ type ReflexConfig =
|
||||
|
||||
| Type | Style | Example |
|
||||
|------|-------|---------|
|
||||
| Files | kebab-case | `signal-bus.ts` |
|
||||
| Types | PascalCase | `SignalBus` |
|
||||
| Functions/variables | camelCase | `createSignalBus` |
|
||||
| Files | kebab-case | `sense-scheduler.ts` |
|
||||
| Types | PascalCase | `SenseScheduler` |
|
||||
| Functions/variables | camelCase | `createSenseScheduler` |
|
||||
| Constants | UPPER_SNAKE | `MAX_RETRY_COUNT` |
|
||||
| Generics | Single letter or descriptive | `T`, `TValue` |
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ nerve/
|
||||
packages/
|
||||
core/ # @uncaged/nerve-core — shared types, config parser, Result, spawn-safe
|
||||
cli/ # @uncaged/nerve-cli — CLI (init, validate, dev, daemon, knowledge)
|
||||
daemon/ # @uncaged/nerve-daemon — kernel, workers, signal bus, scheduler
|
||||
daemon/ # @uncaged/nerve-daemon — kernel, workers, sense scheduler, workflow manager
|
||||
store/ # @uncaged/nerve-store — append-only log, SQLite, CAS blob store
|
||||
workflow-utils/ # @uncaged/nerve-workflow-utils — role factories, extract, LLM helpers
|
||||
adapter-cursor/ # @uncaged/nerve-adapter-cursor — cursor-agent CLI adapter
|
||||
|
||||
@@ -1,91 +1,31 @@
|
||||
# Signal Routing
|
||||
# Sense compute → workflow (RFC #308)
|
||||
|
||||
Signal routing is the core mechanism that determines how Sense outputs flow through the Nerve system.
|
||||
Stateful senses no longer emit signals or pass outputs through `routeSenseComputeOutput`. The worker runs `compute(state)` and returns `{ state, workflow }`.
|
||||
|
||||
## Routing Logic
|
||||
|
||||
When a Sense `compute()` function returns non-null, the output goes through `routeSenseComputeOutput()` in `packages/core/src/sense-workflow-directive.ts`:
|
||||
## Flow
|
||||
|
||||
```
|
||||
Sense compute() → non-null → routeSenseComputeOutput() → { signal, workflow }
|
||||
↓
|
||||
kernel.ts → signal ALWAYS emitted + optional workflow start
|
||||
Sense worker: compute(state) → { state, workflow }
|
||||
↓
|
||||
persist state JSON (data/senses/<name>.json)
|
||||
↓
|
||||
IPC compute-result → kernel
|
||||
↓
|
||||
workflow !== null → parseWorkflowTrigger (validation) → workflowManager.startWorkflow
|
||||
scheduler.onSenseCompleted(senseName) → dependents with `on: [senseName]`
|
||||
```
|
||||
|
||||
## Two Output Formats
|
||||
## Workflow trigger shape
|
||||
|
||||
### 1. Explicit Format
|
||||
```typescript
|
||||
{
|
||||
signal: any, // emitted as signal
|
||||
workflow: { // optional workflow trigger
|
||||
name: string,
|
||||
maxRounds: number,
|
||||
prompt: string,
|
||||
dryRun: boolean
|
||||
} | null
|
||||
}
|
||||
```
|
||||
When `workflow` is non-null it must be a plain object validated by `parseWorkflowTrigger()` in `packages/core/src/sense.ts`:
|
||||
|
||||
### 2. Shorthand Format
|
||||
Any other value is treated as:
|
||||
```typescript
|
||||
{ signal: payload, workflow: null }
|
||||
```
|
||||
- `name`: non-empty string
|
||||
- `maxRounds`: integer ≥ 1
|
||||
- `prompt`: string
|
||||
- `dryRun`: boolean
|
||||
|
||||
## Workflow Directive Parsing
|
||||
Invalid triggers are rejected by the daemon when handling the worker message (workflow is not started).
|
||||
|
||||
## Concrete Routing Predicates
|
||||
## Scheduling
|
||||
|
||||
The routing decision is implemented in `routeSenseComputeOutput()` using these exact matching criteria:
|
||||
|
||||
### 1. Explicit Format Detection
|
||||
```typescript
|
||||
if (isPlainRecord(payload) && Object.hasOwn(payload, "signal"))
|
||||
```
|
||||
- Payload must be a plain object
|
||||
- Must have `signal` property (any value)
|
||||
- Workflow extracted from `workflow` property or defaults to null
|
||||
|
||||
### 2. Workflow Validation
|
||||
When workflow is non-null, it's validated via `parseWorkflowTrigger()`:
|
||||
- `name`: non-empty string (trimmed)
|
||||
- `maxRounds`: positive integer >= 1
|
||||
- `prompt`: string
|
||||
- `dryRun`: boolean
|
||||
|
||||
**Critical behavior**: Invalid workflows are silently dropped (become null) but signal emission continues. This prevents malformed workflow config from blocking signals.
|
||||
|
||||
### 3. Fallback to Shorthand
|
||||
Any value that doesn't match explicit format becomes:
|
||||
```typescript
|
||||
{ signal: payload, workflow: null }
|
||||
```
|
||||
|
||||
## Processing Flow
|
||||
|
||||
```typescript
|
||||
// In kernel.ts handleSenseWorkerSignal()
|
||||
const { signal: signalPayload, workflow } = routeResult.value;
|
||||
|
||||
// Signal is ALWAYS emitted when compute returns non-null
|
||||
bus.emit({ id, senseId, payload: signalPayload, timestamp });
|
||||
|
||||
// Workflow is started ONLY if workflow is non-null
|
||||
if (workflow !== null) {
|
||||
workflowManager.startWorkflow(workflow.name, { ... });
|
||||
}
|
||||
```
|
||||
|
||||
## Legacy String Format (Deprecated)
|
||||
|
||||
The old `"name|maxRounds|prompt"` string format is converted to the structured format internally but should not be used in new code.
|
||||
|
||||
## Key Behaviors
|
||||
|
||||
1. **Signal priority**: Every non-null compute result emits a signal, regardless of workflow
|
||||
2. **Additive behavior**: Valid workflow triggers are executed in addition to signal emission
|
||||
3. **Failure tolerance**: Invalid workflow directives are silently ignored, signal still emits
|
||||
4. **Structure-based routing**: No complex predicates - simply checks object structure and property existence
|
||||
|
||||
This routing mechanism ensures clean separation between perception (signals) and action (workflows) while maintaining backward compatibility.
|
||||
Other senses list this sense under `on` in `nerve.yaml` to be scheduled when this sense completes a successful compute (see sense scheduler reverse-index in the daemon).
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Core Concepts
|
||||
|
||||
```
|
||||
External World → Sense → Signal → Workflow → Log
|
||||
External World → Sense(state) → { newState, workflow? } → Workflow → Log
|
||||
↑ ↑
|
||||
"what to observe" "what to do"
|
||||
```
|
||||
@@ -14,18 +14,17 @@ External World → Sense → Signal → Workflow → Log
|
||||
|
||||
| Concept | What it is |
|
||||
|---------|-----------|
|
||||
| **Sense** | A `compute()` function that samples or derives data. Returns `ComputeResult<T>` — non-null emits a Signal (and optionally triggers a Workflow), null is silent. Each Sense has its own SQLite database. Scheduling (interval, on) is configured in nerve.yaml. |
|
||||
| **Signal** | A notification emitted when a Sense returns non-null. Pure fact, no intent. Distributed via an in-memory Signal Bus. Not persisted. |
|
||||
| **Sense** | A stateful `compute(state)` function. Returns new state and an optional workflow trigger. State persisted as JSON. Scheduling configured in nerve.yaml. |
|
||||
| **Workflow** | A stateful multi-step execution. Contains **Roles** (actors with side effects) and a **Moderator** (pure router). Each instance is a **Thread** with a unique `runId`. |
|
||||
| **Log** | Immutable audit trail. Records executions, state transitions, errors. Cannot trigger Senses — prevents feedback loops. |
|
||||
| **Engine** | The kernel orchestrating everything. Holds Signal Bus, Process Manager, Workflow Manager. Never loads user code directly — all user code runs in isolated Workers. |
|
||||
| **Engine** | The kernel orchestrating everything. Holds Process Manager, Workflow Manager, Sense Scheduler. Never loads user code directly — all user code runs in isolated Workers. |
|
||||
| **Daemon** | The `nerve-daemon` package — engine runtime. Runs as a background process. |
|
||||
|
||||
### Architecture Rules
|
||||
|
||||
- **Two extension points**: Sense (what to observe + when), Workflow (what to do)
|
||||
- **Process isolation**: One worker per Sense group (long-lived), one per Workflow type (on-demand). Workers never talk to each other.
|
||||
- **Causality is one-directional**: External world → Sense → Signal → Workflow + Log. Logs are the end of the chain.
|
||||
- **Causality is one-directional**: External world → Sense(state) → Workflow (when triggered) + Log. Logs are the end of the chain.
|
||||
|
||||
|
||||
|
||||
@@ -37,18 +36,18 @@ Use `function` + `type`, not `class` + `interface`.
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
type Signal = {
|
||||
senseId: string;
|
||||
value: unknown;
|
||||
type WorkflowLaunch = {
|
||||
senseName: string;
|
||||
workflowName: string;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
function createSignal(senseId: string, value: unknown): Signal {
|
||||
return { senseId, value, ts: Date.now() };
|
||||
function recordWorkflowLaunch(senseName: string, workflowName: string): WorkflowLaunch {
|
||||
return { senseName, workflowName, ts: Date.now() };
|
||||
}
|
||||
|
||||
// ❌ Bad — no class, no interface
|
||||
class Signal implements ISignal { ... }
|
||||
class WorkflowLaunch implements IWorkflowLaunch { ... }
|
||||
```
|
||||
|
||||
### Rules
|
||||
@@ -92,10 +91,11 @@ type SenseConfig = {
|
||||
For mutually exclusive fields, use discriminated unions:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
type ComputeResult<T> =
|
||||
| null
|
||||
| { signal: T; workflow: WorkflowTrigger | null };
|
||||
// ✅ Good — sense modules return explicit next state + optional workflow trigger
|
||||
type SenseComputeReturn<S> = {
|
||||
state: S;
|
||||
workflow: WorkflowTrigger | null;
|
||||
};
|
||||
```
|
||||
|
||||
### Workflow Naming
|
||||
@@ -139,9 +139,9 @@ const workflow: WorkflowDefinition<Record<"planner", MyMeta>> = {
|
||||
|
||||
| Type | Style | Example |
|
||||
|------|-------|---------|
|
||||
| Files | kebab-case | `signal-bus.ts` |
|
||||
| Types | PascalCase | `SignalBus` |
|
||||
| Functions/variables | camelCase | `createSignalBus` |
|
||||
| Files | kebab-case | `sense-scheduler.ts` |
|
||||
| Types | PascalCase | `SenseScheduler` |
|
||||
| Functions/variables | camelCase | `createSenseScheduler` |
|
||||
| Constants | UPPER_SNAKE | `MAX_RETRY_COUNT` |
|
||||
| Generics | Single letter or descriptive | `T`, `TValue` |
|
||||
|
||||
|
||||
@@ -2,35 +2,34 @@
|
||||
|
||||
**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](https://github.com/uncaged) agent framework.
|
||||
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 ─┬→ Signal → Reflex → Sense (scheduled compute)
|
||||
│
|
||||
└→ Workflow (Sense return with workflow directive) → Log
|
||||
External World → Sense(state) → { newState, workflow? } → Workflow → Log
|
||||
↑
|
||||
scheduling: interval / on (per sense in nerve.yaml)
|
||||
```
|
||||
|
||||
| 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** | 👁️ Perception | Stateful `compute(state)` returning `{ state, workflow }`. 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 when `workflow` is non-null in the compute result, or via CLI/daemon IPC. |
|
||||
| **Log** | 📝 Record | Immutable audit trail. **Cannot** schedule senses or workflows (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`.
|
||||
**Sense → Workflow:** when `workflow` is a structured object `{ name, maxRounds, prompt, dryRun }`, the kernel validates it (`@uncaged/nerve-core` `parseWorkflowTrigger`) and starts that workflow. Use `workflow: null` when no run should start.
|
||||
|
||||
Three extension points for **what / when / multi-step action** — reflexes never replace Sense-driven workflow launches.
|
||||
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→workflow routing, daemon IPC protocol |
|
||||
| [`@uncaged/nerve-core`](./packages/core) | Shared types, config parser, workflow trigger validation, 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, workers, signal bus, sense scheduler, workflow manager, file watcher, IPC |
|
||||
| [`@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
|
||||
@@ -43,24 +42,28 @@ pnpm add -g @uncaged/nerve-cli
|
||||
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
|
||||
# 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 reflexes in nerve.yaml
|
||||
# Configure scheduling on each sense in nerve.yaml
|
||||
cat > nerve.yaml << 'EOF'
|
||||
senses:
|
||||
cpu-usage:
|
||||
group: system
|
||||
throttle: 10s
|
||||
|
||||
reflexes:
|
||||
- kind: sense
|
||||
sense: cpu-usage
|
||||
interval: 30s
|
||||
EOF
|
||||
|
||||
@@ -73,7 +76,7 @@ nerve logs # view logs
|
||||
|
||||
## Configuration
|
||||
|
||||
`nerve.yaml` declares senses, reflexes (sense-only), optional workflows (concurrency), and optional engine `max_rounds`:
|
||||
`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)
|
||||
@@ -84,12 +87,15 @@ senses:
|
||||
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
|
||||
|
||||
derived-example:
|
||||
group: system
|
||||
throttle: null
|
||||
timeout: 30s
|
||||
grace_period: null
|
||||
on:
|
||||
- cpu-usage # run after cpu-usage completes a compute
|
||||
|
||||
workflows:
|
||||
cleanup:
|
||||
@@ -101,17 +107,24 @@ workflows:
|
||||
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`.
|
||||
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/compute.ts`):
|
||||
**Example — Sense starts a workflow** (`senses/disk-pressure/src/index.ts`):
|
||||
|
||||
```typescript
|
||||
export async function compute() {
|
||||
export const initialState = { checked: false };
|
||||
|
||||
export async function compute(state: typeof initialState) {
|
||||
const full = await diskNearlyFull();
|
||||
if (!full) return null;
|
||||
if (!full) return { state: { ...state, checked: true }, workflow: null };
|
||||
return {
|
||||
path: "/data",
|
||||
workflow: "cleanup|10|Disk partition nearly full", // name|maxRounds|prompt
|
||||
state: { ...state, checked: true },
|
||||
workflow: {
|
||||
name: "cleanup",
|
||||
maxRounds: 10,
|
||||
prompt: "Disk partition nearly full",
|
||||
dryRun: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -138,12 +151,9 @@ export async function compute() {
|
||||
│ │ └──────────────┼──────────────┘ │
|
||||
│ │ ▼ │
|
||||
│ │ ┌──────────────┐ │
|
||||
│ │ │ Signal Bus │ │
|
||||
│ │ │Sense Scheduler │
|
||||
│ │ │(interval + on) │
|
||||
│ │ └──────┬───────┘ │
|
||||
│ │ ▼ │
|
||||
│ │ ┌──────────────────┐ │
|
||||
│ │ │ Reflex Scheduler│ │
|
||||
│ │ └────────┬─────────┘ │
|
||||
│ │ ▼ │
|
||||
│ │ ┌───────────────────┐ │
|
||||
│ └───────────────────►│ Workflow Manager │──→ @uncaged/nerve-store │
|
||||
@@ -152,8 +162,7 @@ export async function compute() {
|
||||
```
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
@@ -161,8 +170,8 @@ export async function compute() {
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Zero native addons** — uses Node.js built-in `node:sqlite` (DatabaseSync)
|
||||
- **Drizzle ORM** v1.0 for sense databases
|
||||
- **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
|
||||
@@ -180,7 +189,7 @@ pnpm -r test # run all tests
|
||||
|
||||
## Design Documents
|
||||
|
||||
- [RFC-001: Observation Engine](./docs/rfc-001-observation-engine.md) — Sense, Signal, Reflex model
|
||||
- [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)
|
||||
|
||||
|
||||
+26
-23
@@ -8,25 +8,29 @@
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
type Signal = {
|
||||
senseId: string
|
||||
value: unknown
|
||||
type WorkflowLaunch = {
|
||||
senseName: string
|
||||
workflowName: string
|
||||
ts: number
|
||||
}
|
||||
|
||||
function createSignal(senseId: string, value: unknown): Signal {
|
||||
return { senseId, value, ts: Date.now() }
|
||||
function recordWorkflowLaunch(senseName: string, workflowName: string): WorkflowLaunch {
|
||||
return { senseName, workflowName, ts: Date.now() }
|
||||
}
|
||||
|
||||
// ❌ Bad
|
||||
interface ISignal {
|
||||
senseId: string
|
||||
value: unknown
|
||||
interface IWorkflowLaunch {
|
||||
senseName: string
|
||||
workflowName: string
|
||||
ts: number
|
||||
}
|
||||
|
||||
class Signal implements ISignal {
|
||||
constructor(public senseId: string, public value: unknown, public ts: number) {}
|
||||
class WorkflowLaunch implements IWorkflowLaunch {
|
||||
constructor(
|
||||
public senseName: string,
|
||||
public workflowName: string,
|
||||
public ts: number,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -65,17 +69,16 @@ type SenseConfig = {
|
||||
当多个字段互斥时,用 discriminated union 代替一堆 optional:
|
||||
|
||||
```typescript
|
||||
// ✅ Good — 编译器保证 sense 和 workflow 不会同时出现
|
||||
type ReflexConfig =
|
||||
| { kind: "sense"; sense: string; interval: string | null; on: string[] | null }
|
||||
| { kind: "workflow"; workflow: string; on: string[] | null }
|
||||
// ✅ Good — 编译器保证两种 overflow 形态互斥且字段完整
|
||||
type WorkflowConfig =
|
||||
| { concurrency: number; overflow: "drop" }
|
||||
| { concurrency: number; overflow: "queue"; maxQueue: number }
|
||||
|
||||
// ❌ Bad — sense 和 workflow 都 optional,运行时才知道到底填了哪个
|
||||
type ReflexConfig = {
|
||||
sense?: string;
|
||||
workflow?: string;
|
||||
interval?: string;
|
||||
on?: string[];
|
||||
// ❌ Bad — 字段一堆 optional,运行时才知道到底填了哪种并发策略
|
||||
type WorkflowConfig = {
|
||||
concurrency?: number;
|
||||
overflow?: string;
|
||||
maxQueue?: number;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -103,9 +106,9 @@ export default function startEngine() { ... }
|
||||
|
||||
| 类型 | 风格 | 示例 |
|
||||
|------|------|------|
|
||||
| 文件 | kebab-case | `signal-bus.ts` |
|
||||
| 类型 | PascalCase | `SignalBus` |
|
||||
| 函数/变量 | camelCase | `createSignalBus` |
|
||||
| 文件 | kebab-case | `sense-scheduler.ts` |
|
||||
| 类型 | PascalCase | `SenseScheduler` |
|
||||
| 函数/变量 | camelCase | `createSenseScheduler` |
|
||||
| 常量 | UPPER_SNAKE | `MAX_RETRY_COUNT` |
|
||||
| 泛型参数 | 单字母或描述性 | `T`, `TValue` |
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
|
||||
| 项目 | 位置 | 说明 | 置信度 | 建议 |
|
||||
|------|------|------|--------|------|
|
||||
| 已更名 API 仍出现在 README | `packages/core/README.md` | 仍描述 `parseSenseWorkflowDirective`、`ParsedSenseWorkflowDirective`、`SenseComputeRoute`;源码已为 `parseWorkflowTrigger` / `routeSenseComputeOutput` / `RoutedSenseOutput` | **高** | **更新文档**(本次分析不改代码,仅记录)。 |
|
||||
| ~~已更名 API 仍出现在 README~~ | `packages/core/README.md` | (已修正)文档与 stateful sense、`parseWorkflowTrigger` 对齐;`routeSenseComputeOutput` 已移除 | — | 关闭 |
|
||||
| Hermes 选项合并注释 | `packages/workflow-utils/src/shared/hermes-agent.ts` | 注释称 absorbed from `hermes-options.ts`,该文件已不存在 | **中** | **清理注释**,避免误导。 |
|
||||
| `KNOWN_AGENT_ADAPTER_IDS` 含 `codex` | `packages/core/src/agent.ts` | 仓内无 `codex` 适配器包;与常量未被引用叠加 | **中** | **对齐产品**:实现适配器或从列表移除。 |
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
1. **高优先级调查**: `createEchoAgent` 与 `KNOWN_AGENT_ADAPTER_IDS` — 要么接入运行时,要么删减以免维护假象。
|
||||
2. **API 面收敛**: `parseDurationStringToMs`、`labelSenseTrigger` 若无意对外,可从 `core` 公共导出移除。
|
||||
3. **`workflow-utils`**: 评估 `isDryRun` 删除;`spawnSafe` 等从 `workflow-utils` 再导出是否仍有必要。
|
||||
4. **文档**: 修正 `packages/core/README.md` 中 Sense→Workflow 路由 API 名称。
|
||||
4. ~~**文档**: 修正 `packages/core/README.md` 中 Sense→Workflow 路由 API 名称。~~(已完成)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
# Example nerve.yaml demonstrating Signal Bus & Reflex Scheduler (Phase 3)
|
||||
# Example nerve.yaml demonstrating per-sense scheduling (interval + on)
|
||||
#
|
||||
# Layout:
|
||||
# - cpu-usage: periodic every 10s, throttled to 5s minimum between computes
|
||||
# - disk-usage: periodic every 30s
|
||||
# - system-health: derived sense, triggered whenever cpu-usage OR disk-usage emits
|
||||
# - system-health: derived sense, scheduled when cpu-usage OR disk-usage completes a compute
|
||||
|
||||
senses:
|
||||
cpu-usage:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"build": "pnpm -r run build",
|
||||
"test": "pnpm -r test",
|
||||
"check": "biome check .",
|
||||
"format": "biome format --write .",
|
||||
"link:dev": "bash scripts/link-dev.sh"
|
||||
|
||||
@@ -51,10 +51,10 @@ Structured rows in `data/logs.db` are surfaced via **`nerve workflow inspect`**
|
||||
```bash
|
||||
nerve sense list # List senses (live fields from daemon IPC when running)
|
||||
nerve sense trigger <name> # IPC trigger-sense — queue a compute for that sense
|
||||
nerve sense query <name> # Read-only SQL on data/senses/<name>.db (optional SQL args)
|
||||
nerve sense schema <name> # Print CREATE TABLE statements for that sense DB
|
||||
```
|
||||
|
||||
Sense state is persisted as JSON under `data/senses/<name>.json` by the sense worker after each successful compute.
|
||||
|
||||
### Store maintenance
|
||||
|
||||
```bash
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## sense list
|
||||
|
||||
- ✅ prints sense list with name, group, throttle, triggers, and last signal time
|
||||
- ✅ prints sense list with name, group, throttle, triggers
|
||||
- 🔲 empty state — no senses registered, prints empty message
|
||||
- 🔲 `--json` — outputs valid JSON array
|
||||
|
||||
@@ -11,19 +11,3 @@
|
||||
- ✅ trigger known sense exits 0, stdout contains "Triggered"
|
||||
- ✅ trigger non-existent sense writes error to stderr and exits 1
|
||||
- ✅ sends correct IPC message `{ type: trigger-sense, sense: <name> }` to daemon
|
||||
|
||||
## sense query
|
||||
|
||||
- ✅ after trigger, persisted `_signals` table has at least one row
|
||||
- ✅ default output lists payload column and counter count
|
||||
- ✅ `--json` prints valid JSON array with payload on each row
|
||||
- ✅ `--sql` runs custom read-only SQL and prints result
|
||||
- 🔲 query non-existent sense — error message
|
||||
- 🔲 `--limit` / `--offset` pagination
|
||||
|
||||
## sense schema
|
||||
|
||||
- ✅ prints CREATE TABLE statements for the sense database
|
||||
- ✅ includes `_signals` table in output
|
||||
- ✅ `--json` prints valid JSON array of SQL strings
|
||||
- 🔲 schema for non-existent sense — error message
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
Full round-trip integration tests that exercise multiple subcommands together.
|
||||
|
||||
- ✅ sense list + sense query after trigger — registers sense, triggers, verifies persisted signal and query output
|
||||
- ✅ sense list after trigger — daemon lists configured senses; trigger queues a compute (state persisted under `data/senses/` by the worker)
|
||||
- 🔲 init → dev → trigger workflow → thread inspect round-trip
|
||||
|
||||
@@ -72,8 +72,8 @@ describe("queryKnowledgeRepo (word overlap fallback)", () => {
|
||||
path: "a.md",
|
||||
slug: "a.md#0",
|
||||
chunkIndex: 0,
|
||||
text: "the signal bus emits notifications",
|
||||
contentHash: contentHash("the signal bus emits notifications"),
|
||||
text: "the sense scheduler triggers computes",
|
||||
contentHash: contentHash("the sense scheduler triggers computes"),
|
||||
embedding: fakeEmbeddingBytes("a"),
|
||||
},
|
||||
{
|
||||
@@ -89,7 +89,7 @@ describe("queryKnowledgeRepo (word overlap fallback)", () => {
|
||||
db.close();
|
||||
}
|
||||
|
||||
const ranked = await queryKnowledgeRepo(root, dbPath, "signal bus", 10);
|
||||
const ranked = await queryKnowledgeRepo(root, dbPath, "sense scheduler", 10);
|
||||
expect(ranked.length).toBe(2);
|
||||
expect(ranked[0]?.path).toBe("a.md");
|
||||
expect(ranked[1]?.path).toBe("b.md");
|
||||
|
||||
+8
-21
@@ -4,9 +4,9 @@ Shared types and configuration parser for the [nerve](../../README.md) observati
|
||||
|
||||
## What's Inside
|
||||
|
||||
- **Type definitions** — `Signal`, `SenseConfig`, `SenseInfo`, `WorkflowConfig`, `NerveConfig`, and related types
|
||||
- **Config parser** — `parseNerveConfig(yaml)` validates and parses `nerve.yaml` into `NerveConfig` (rejects reflex entries that declare a `workflow` key; reflexes only schedule senses)
|
||||
- **Sense → workflow routing** — `parseWorkflowTrigger`, `routeSenseComputeOutput`, and types `WorkflowTrigger`, `RoutedSenseOutput`
|
||||
- **Type definitions** — `SenseConfig`, `SenseInfo`, `SenseComputeFn`, `SenseModule`, `WorkflowConfig`, `NerveConfig`, `WorkflowTrigger`, and related types
|
||||
- **Config parser** — `parseNerveConfig(yaml)` validates and parses `nerve.yaml` into `NerveConfig` (top-level `reflexes` is rejected; use `interval` / `on` on each sense)
|
||||
- **Workflow triggers** — `parseWorkflowTrigger` validates structured workflow launch objects from Sense compute results or IPC
|
||||
- **Daemon IPC protocol** — request/response types (`DaemonIpcRequest`, `DaemonIpcResponse`, …) and `parseDaemonIpcRequest` for newline-delimited JSON on the CLI ↔ daemon socket
|
||||
- **Workflow automaton types** — `START` / `END` sentinel constants, `WorkflowMessage`, `StartStep`, `RoleStep`, `ModeratorContext` (`start` + `steps`; empty `steps` on first moderator call), `Moderator` (single `context` argument), `WorkflowDefinition`, `Role`, `RoleResult`, plus `DEFAULT_ENGINE_MAX_ROUNDS`
|
||||
- **Result type** — `Result<T>` with `ok()` / `err()` helpers for explicit error handling (no thrown exceptions for parse paths)
|
||||
@@ -15,7 +15,7 @@ Shared types and configuration parser for the [nerve](../../README.md) observati
|
||||
|
||||
```typescript
|
||||
import { parseNerveConfig, ok, err } from "@uncaged/nerve-core";
|
||||
import type { NerveConfig, Signal, Result } from "@uncaged/nerve-core";
|
||||
import type { NerveConfig, Result } from "@uncaged/nerve-core";
|
||||
|
||||
const result: Result<NerveConfig> = parseNerveConfig(yamlString);
|
||||
if (result.ok) {
|
||||
@@ -23,10 +23,10 @@ if (result.ok) {
|
||||
}
|
||||
```
|
||||
|
||||
### Sense return → signal vs workflow
|
||||
### Workflow trigger validation
|
||||
|
||||
```typescript
|
||||
import { parseWorkflowTrigger, routeSenseComputeOutput } from "@uncaged/nerve-core";
|
||||
import { parseWorkflowTrigger } from "@uncaged/nerve-core";
|
||||
|
||||
const directive = parseWorkflowTrigger({
|
||||
name: "my-workflow",
|
||||
@@ -37,23 +37,10 @@ const directive = parseWorkflowTrigger({
|
||||
if (directive.ok) {
|
||||
console.log(directive.value.name, directive.value.maxRounds, directive.value.prompt);
|
||||
}
|
||||
|
||||
const route = routeSenseComputeOutput({
|
||||
signal: { metric: 42 },
|
||||
workflow: {
|
||||
name: "my-workflow",
|
||||
maxRounds: 8,
|
||||
prompt: "Run now",
|
||||
dryRun: false,
|
||||
},
|
||||
});
|
||||
if (route.ok && route.value.workflow !== null) {
|
||||
console.log(route.value.workflow);
|
||||
} else if (route.ok) {
|
||||
console.log(route.value.signal);
|
||||
}
|
||||
```
|
||||
|
||||
Sense modules return `{ state, workflow }` from `compute(state)`; when `workflow` is non-null it must satisfy the shape validated by `parseWorkflowTrigger` (the daemon validates before starting a run).
|
||||
|
||||
## Duration Format
|
||||
|
||||
Config fields like `throttle`, `timeout`, and `interval` accept human-readable durations:
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "1.0.0-beta.23-c10d10c",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -10,7 +10,7 @@ export type SenseConfig = {
|
||||
gracePeriod: number | null;
|
||||
/** Polling interval (ms). When set, the sense is triggered periodically. */
|
||||
interval: number | null;
|
||||
/** Other sense names whose signals trigger this sense. */
|
||||
/** Other sense names whose successful computes schedule this sense (kernel reverse-index). */
|
||||
on: string[];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
# @uncaged/nerve-daemon
|
||||
|
||||
The observation engine runtime for [nerve](../../README.md) — runs senses, routes signals, runs the sense scheduler, and manages workflows.
|
||||
The observation engine runtime for [nerve](../../README.md) — runs senses, persists JSON state, runs the sense scheduler, and manages workflows.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Module | Source (indicative) | Responsibility |
|
||||
|--------|---------------------|----------------|
|
||||
| **Kernel** | `kernel.ts` | Orchestrator — worker pool, signal bus, sense scheduler, workflow manager, optional file watcher and daemon IPC, config reload hooks |
|
||||
| **Kernel** | `kernel.ts` | Orchestrator — worker pool, sense scheduler, workflow manager, optional file watcher and daemon IPC, config reload hooks |
|
||||
| **Worker pool** | `worker-pool.ts` | Fork and supervise one child process per sense group; restart/shutdown; crash cleanup hooks for scheduler state |
|
||||
| **Kernel sense groups** | `kernel-sense-groups.ts` | Derive sense groups from config; list senses per group for scheduling |
|
||||
| **Sense runtime** | sense worker + Drizzle | Per-sense SQLite (`node:sqlite`), migrations, peer DB reads |
|
||||
| **Sense worker** | `sense-worker.ts` (fork target) | Child process entry — runs `compute()` per sense in a group |
|
||||
| **Signal bus** | `signal-bus.ts` | In-memory pub/sub for sense signals |
|
||||
| **Sense scheduler** | `sense-scheduler.ts` | Interval + `on` subscriptions, throttle/coalesce |
|
||||
| **Sense runtime** | `sense-runtime.ts` + sense worker | Loads user modules (`compute`, `initialState`), reads/writes `data/senses/<name>.json` |
|
||||
| **Sense worker** | `sense-worker.ts` (fork target) | Child process entry — runs `compute(state)` per sense in a group |
|
||||
| **Sense scheduler** | `sense-scheduler.ts` | Interval + `on` subscriptions (reverse-index by upstream sense), throttle/coalesce |
|
||||
| **Workflow manager** | `workflow-manager.ts` | One worker per workflow name, concurrency (drop/queue), queue caps |
|
||||
| **Workflow worker** | `workflow-worker.ts` | Child process — runs RFC-002 threads (`start-thread`, `resume-thread` IPC) |
|
||||
| **IPC (parent ↔ workers)** | `ipc.ts` | Typed messages for sense and workflow workers (includes `resume-thread` for recovery) |
|
||||
| **Log / workflow persistence** | via `@uncaged/nerve-store` | Structured logs, `workflow_runs`, thread messages (used for recovery) |
|
||||
| **Blob store** | `@uncaged/nerve-store` | CAS under `data/blobs/` — sense workers construct `createBlobStore(join(nerveRoot, "data", "blobs"))` for artifact writes |
|
||||
| **Blob store** | `@uncaged/nerve-store` | CAS under `data/blobs/` — workflows use blob storage for artifacts as configured |
|
||||
| **File watcher** | `file-watcher.ts` | Watches workspace paths for config / sense / workflow file changes |
|
||||
| **Kernel file watch** | `kernel-file-watch.ts` | Maps watcher events to `reloadConfig`, sense group restart, workflow `drainAndRespawn` |
|
||||
| **Daemon IPC** | `daemon-ipc.ts` | Unix socket server — parses `@uncaged/nerve-core` `DaemonIpcRequest`, dispatches trigger-workflow / trigger-sense / list-senses |
|
||||
@@ -35,9 +34,9 @@ Hot reload (`drainAndRespawn`) uses a controlled drain: in-flight runs may be ma
|
||||
## Key Design Decisions
|
||||
|
||||
- **One worker process per sense group** — isolation between groups, shared compute within a group
|
||||
- **`node:sqlite` (DatabaseSync)** — zero native addons, WAL mode, built into Node.js ≥ 22.5
|
||||
- **Sense state as JSON** — `data/senses/<name>.json`, updated after each successful compute in the worker
|
||||
- **Throttle + coalesce** — if compute is in-flight, at most one pending trigger is queued (no unbounded accumulation)
|
||||
- **Log ≠ Signal** — logs are queryable data assets but cannot trigger the sense scheduler or workflows (prevents feedback loops)
|
||||
- **Log ≠ Sense trigger** — logs are queryable data assets but cannot schedule sense computes or workflows (prevents feedback loops)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -85,7 +84,7 @@ await kernel.stop();
|
||||
pnpm add @uncaged/nerve-daemon
|
||||
```
|
||||
|
||||
Requires Node.js ≥ 22.5 (for `node:sqlite`).
|
||||
Requires Node.js ≥ 22.5 (for `node:sqlite` in the log store and related persistence).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("createFileWatcher", () => {
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
writeFileSync(
|
||||
join(root, "senses", "cpu-usage", "index.js"),
|
||||
"export async function compute() { return { signal: 42, workflow: null }; }",
|
||||
"export const initialState = {}; export async function compute(state) { return { state, workflow: null }; }",
|
||||
);
|
||||
|
||||
await waitFor(() => changes.length > 0, 3000);
|
||||
|
||||
@@ -21,7 +21,7 @@ export function resolveWorkerScript(): string {
|
||||
export type SenseWorkerPoolOptions = {
|
||||
nerveRoot: string;
|
||||
workerScript: string;
|
||||
/** Invoked for every IPC message from a worker (including ready / signal / error). */
|
||||
/** Invoked for every IPC message from a worker (including ready / compute-result / error). */
|
||||
onWorkerMessage: (raw: unknown) => void;
|
||||
/** Sense names in a group — reserved for scheduler-aligned cleanup (kernel passes current config). */
|
||||
sensesForGroup: (group: string) => string[];
|
||||
|
||||
@@ -41,7 +41,7 @@ export function readNerveYaml(options: ReadNerveYamlOptions): Result<string, Ner
|
||||
* Shared context for workflow agents: how Nerve fits together and common CLI verbs.
|
||||
*/
|
||||
export const nerveAgentContext = `
|
||||
Nerve observes the world through **Senses** (each has its own SQLite DB and a \`compute()\` function).
|
||||
**Reflexes** (YAML) schedule sense runs or start **Workflows** on intervals or signals.
|
||||
The \`nerve\` CLI manages config, triggers, and queries; keep paths and commands aligned with the host nerve.yaml and senses directory.
|
||||
Nerve observes the world through **Senses**: each exports \`compute(state)\`, \`initialState\`, and persists JSON state under \`data/senses/\`.
|
||||
Per-sense \`interval\` and \`on\` in \`nerve.yaml\` schedule computes; a non-null \`workflow\` in the return starts a **Workflow**.
|
||||
The \`nerve\` CLI manages config and triggers; keep paths aligned with the workspace \`nerve.yaml\` and \`senses/\` directory.
|
||||
`.trim();
|
||||
|
||||
Generated
-1141
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user