# Regulator Design — Path 2 (Session 15)
Design document for Noos's pivot to **Rust-native reliability infrastructure for LLM agents**. Authored 2026-04-15 after 4-agent research synthesis (SSM ecosystem / Rust LLM ecosystem / agent regulation landscape / SSM-specific tooling) converged on Path 2 with refined framing.
**This is a design doc, not a code commit.** Session 15 produces this document + architecture memo + CLAUDE.md positioning update. Implementation starts Session 16.
## 1. Problem
Current Noos (post-Phase 14):
- **Downstream signals are fine** — `body_budget`, `conservation`, `confidence`, `strategy`, `recent_quality` all have working infrastructure, documented operating envelopes, test coverage.
- **Input adapter is wrong** — signals driven by regex on user text (emotional lexicon, topic extraction, structural pattern matching). v3 diagnostic (2026-04-15) confirmed:
- `body_budget` stuck at ~1.0 across 20-turn workload (regex doesn't produce depletion)
- `confidence` stuck at 0.5 on gibberish (regex can't detect lexical OOD)
- `strategy` fires only when warmup > MODERATE_MIN_COUNT (not a bug but undocumented user pitfall)
The signals themselves are valuable (research confirms: MAST NeurIPS 2025, Chroma Context Rot, Agent Drift 2601.04170, Abstention TACL 2025). What Noos reads to build them is the wrong input modality.
## 2. Core change
Rewrite the input adapter to read **LLM operation events** instead of **user text patterns**.
```
┌─────────────────────────────────────────────────────────────────┐
│ BEFORE (Path 1) │
├─────────────────────────────────────────────────────────────────┤
│ user text → regex/lexicon → CognitiveState → downstream signals │
└─────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ AFTER (Path 2) │
├─────────────────────────────────────────────────────────────────┤
│ LLM events (tokens, logprobs, corrections, cost, timing) → │
│ input adapter → CognitiveState → downstream signals │
└─────────────────────────────────────────────────────────────────┘
```
`CognitiveState` is the fixed intermediate. `CognitiveSignals` keeps its shape. Only the input pathway changes. **Tầng 2 delta modulation continues unchanged** — still reads `CognitiveState`, still outputs `DeltaModulation`, still useful for SSM/Mamba deployments as a specialized feature.
## 3. New public API surface
### 3.1 `Regulator` — the new top-level entry point
```rust
pub struct Regulator {
session: CognitiveSession, // wraps existing, stays for backcompat
user_id: String, // for cross-session procedural memory
token_stats: TokenStatsAccumulator, // rolling entropy window
scope_tracker: ScopeTracker, // task vs response token overlap
cost_tracker: CostAccumulator, // tokens/wallclock/call counts
correction_memory: CorrectionStore, // per-user procedural preferences
}
impl Regulator {
/// New regulator for a specific user. Loads persistent state if exists.
pub fn for_user(user_id: impl Into<String>) -> Self { ... }
/// Ingest one LLM event. Updates internal state, does not emit decision.
pub fn on_event(&mut self, event: LLMEvent) { ... }
/// Query current regulatory decision. Call after a turn completes.
pub fn decide(&self) -> Decision { ... }
/// Export procedural memory for persistence across process restarts.
/// Includes the wrapped session's LearnedState plus correction patterns.
pub fn export(&self) -> RegulatorState { ... }
pub fn import(state: RegulatorState) -> Self { ... }
/// Escape hatch: expose wrapped session for users who want raw signals.
pub fn session(&self) -> &CognitiveSession { &self.session }
pub fn session_mut(&mut self) -> &mut CognitiveSession { &mut self.session }
}
```
### 3.2 `LLMEvent` — input stream
```rust
pub enum LLMEvent {
/// New turn starts. User's message.
TurnStart {
user_message: String,
},
/// One token emitted by the LLM with its logprob.
/// For streaming API clients: emit per-token.
/// For non-streaming: emit once at TurnComplete with aggregate.
Token {
token: String,
logprob: f64,
index: usize,
},
/// Turn response complete. Full text.
TurnComplete {
full_response: String,
},
/// Cost accounting. Emit after TurnComplete.
Cost {
tokens_in: u32,
tokens_out: u32,
wallclock_ms: u32,
/// Optional provider tag for multi-provider agents.
provider: Option<String>,
},
/// User corrected the last response. Used for procedural learning.
/// `corrects_last` = true means "this user message is a correction of the
/// previous response", not a new independent query.
UserCorrection {
correction_message: String,
corrects_last: bool,
},
/// Low-level: ground truth about response quality if available.
/// E.g., user gave thumbs-up/down, task completed, etc.
QualityFeedback {
quality: f64, // [0, 1]
/// Optional: which fragment(s) of the response triggered the feedback.
fragment_spans: Option<Vec<(usize, usize)>>,
},
}
```
### 3.3 `Decision` — output
```rust
pub enum Decision {
/// Continue normally. No intervention required.
Continue,
/// Stop the agent loop. Further LLM calls unlikely to help.
CircuitBreak {
reason: CircuitBreakReason,
suggestion: String, // human-readable, e.g. "ask user to clarify scope"
},
/// Response drifted beyond task scope. App can accept or re-prompt.
ScopeDriftWarn {
drift_tokens: Vec<String>,
drift_score: f64, // [0, 1] — how far out of scope
task_tokens: Vec<String>, // original task keywords for reference
},
/// Specific response fragments have low confidence. App can highlight to
/// user for review or re-generate those spans.
LowConfidenceSpans {
spans: Vec<ConfidenceSpan>,
},
/// Apply learned procedural pattern before next generation.
/// E.g., "user_123 + refactor cluster → don't expand to error handling".
ProceduralWarning {
patterns: Vec<CorrectionPattern>,
},
}
pub enum CircuitBreakReason {
/// Budget cap reached with response quality still poor.
CostCapReached {
tokens_spent: u32,
tokens_cap: u32,
mean_quality_last_n: f64,
},
/// Quality trending down across N consecutive turns, no recovery.
QualityDeclineNoRecovery {
turns: usize,
mean_delta: f64,
},
/// Repeated failure on same topic cluster.
RepeatedFailurePattern {
cluster: String,
failure_count: usize,
},
}
pub struct ConfidenceSpan {
pub start_char: usize,
pub end_char: usize,
pub confidence: f64, // [0, 1]
pub mean_token_logprob: f64,
}
pub struct CorrectionPattern {
pub user_id: String,
pub topic_cluster: String,
pub pattern_name: String, // e.g. "do_not_add_error_handling"
pub learned_from_turns: usize,
pub confidence: f64,
}
```
## 4. Usage examples
### 4.1 Minimal agent loop with Anthropic SDK
```rust
use noos::regulator::{Regulator, LLMEvent, Decision, CircuitBreakReason};
let mut regulator = Regulator::for_user("user_123");
let client = anthropic_sdk::Client::new();
let user_msg = "Refactor this function to be async.";
regulator.on_event(LLMEvent::TurnStart { user_message: user_msg.into() });
let response = client
.messages()
.model("claude-sonnet-4-6")
.user(user_msg)
.send()
.await?;
// Stream tokens into regulator (or emit single Token at end for non-streaming)
for (i, t) in response.content_blocks().iter().enumerate() {
regulator.on_event(LLMEvent::Token {
token: t.text.clone(),
logprob: t.logprob.unwrap_or(0.0),
index: i,
});
}
regulator.on_event(LLMEvent::TurnComplete {
full_response: response.text(),
});
regulator.on_event(LLMEvent::Cost {
tokens_in: response.usage.input_tokens,
tokens_out: response.usage.output_tokens,
wallclock_ms: response.elapsed_ms,
provider: Some("anthropic".into()),
});
match regulator.decide() {
Decision::Continue => send_to_user(&response.text()),
Decision::ScopeDriftWarn { drift_tokens, drift_score, .. } if drift_score > 0.5 => {
warn_user(&format!(
"Response added unexpected topics: {:?}. Continue?",
drift_tokens
));
}
Decision::CircuitBreak { reason: CircuitBreakReason::RepeatedFailurePattern { .. }, suggestion } => {
return_error(&suggestion);
}
other => send_to_user_with_annotation(&response.text(), &other),
}
// If user corrects next turn:
let next_msg = "Don't add logging — I specifically said refactor only.";
regulator.on_event(LLMEvent::UserCorrection {
correction_message: next_msg.into(),
corrects_last: true,
});
// Regulator now has procedural pattern persisted for this user.
```
### 4.2 Local Mamba agent (keeps Tầng 2 bonus)
```rust
use noos::regulator::{Regulator, LLMEvent};
use noos::inference::mamba::CognitiveMambaModel;
use noos::inference::cognitive_model::CognitiveModel;
let mut regulator = Regulator::for_user("user_456");
let mut model = CognitiveMambaModel::from_pretrained("state-spaces/mamba-130m-hf", config)?;
let user_msg = "I'm so stressed about this deadline.";
regulator.on_event(LLMEvent::TurnStart { user_message: user_msg.into() });
// Path 2 signals drive Tầng 2 delta modulation
let delta_mod = regulator.session().current_delta_modulation(model.num_layers());
// Generate with delta modulation (unchanged Tầng 2 path — SSM bonus feature)
for i in 0..tokens.len() {
let result = model.forward_cognitive(&[tokens[i]], i, &delta_mod)?;
// ... emit tokens, update regulator per above
}
```
### 4.3 Backwards-compatible existing code
```rust
// Existing Path 1 users — this still works unchanged
use noos::session::CognitiveSession;
let mut session = CognitiveSession::new();
let turn = session.process_message("Hello");
let sampling = turn.sampling;
let delta = turn.delta_modulation;
// ... existing flow unchanged
```
`CognitiveSession::process_message(text)` continues to work. It's equivalent to `Regulator::on_event(TurnStart + TurnComplete)` but with text-regex-derived signals. New users use `Regulator`; old users keep their code.
## 5. Signal source migration
| `arousal` | `emotional.rs` English lexicon | Per-token entropy trajectory (rising entropy = confusion) + `UserCorrection` frequency | High |
| `gate_type` | `thalamic_gate.rs` text patterns | Response structure (completion vs refusal vs clarification) from `TurnComplete` | Medium |
| `body_budget` depletion | `sensory_pe` + arousal | `Cost` events (tokens, wallclock) + quality-trend | High |
| `signals.confidence` | Fixed 0.5 base | Per-fragment logprob analysis from `Token` stream | High |
| `signals.strategy` | Regex on response text | Extracted correction patterns from `UserCorrection` events | Medium |
| `signals.conservation` | `body_budget` + arousal | Unchanged formula; driven by new body_budget source | Medium |
| `signals.recent_quality` | EMA of `quality` passed to `process_response` | EMA of `QualityFeedback.quality` + inferred from corrections | Low |
| `signals.salience` | Arousal + gate | Response-entropy spike + correction frequency | Low |
**Deprecation plan**:
- `emotional.rs` regex arousal → deprecated in 0.2.0, removed in 0.3.0
- `thalamic_gate.rs` text patterns → deprecated in 0.2.0, removed in 0.3.0
- `cognition/detector.rs` keyword extraction → retained for topic clustering (backwards compat with LearnedState), but not used for cognitive-state building
## 6. Practitioner vocabulary
Public-facing documentation (README, crate docs, integration guides) uses practitioner vocabulary. Internal docs (`docs/theories.md`, `docs/brain-map.md`, module `//!` comments) retain biological citations where they inspired the mechanism.
**Translation table**:
| Allostatic controller | Reliability layer |
| Body budget / resource pressure | Cost tracker / budget envelope |
| Conservation signal | Cost-overrun prevention |
| Convergence failure | Loop-spiral detection |
| Scope drift | Spec drift defense |
| Procedural memory | Repeated-mistake learning |
| Metacognitive abstention | Low-confidence fragment flagging |
| LC-NE gain mode / arousal | Output-entropy trajectory |
| Gate classification | Response-type inference |
| Cognitive signals | Regulatory signals |
**Tagline candidates for 1.0 release**:
- "Reliability infrastructure for Rust LLM agents"
- "Cost + scope + calibration sidecar for agent loops"
- "The regulation layer between your agent and your LLM"
## 7. Three flagship demos (Sessions 21-23)
Each demo closes a loop that Mem0, Letta, LangChain Memory, Langfuse, Arize, OPA demonstrably CANNOT close in one call. This is the articulation strategy (research agent 3 flagged articulation risk).
### Demo 1 — Scope drift intercept (`regulator_scope_drift_demo.rs`)
**Setup**: Agent asked to refactor a function. LLM response expands to add error handling, logging, telemetry.
**Baseline agent** (no regulator): sends entire response to user. User now has to manually identify what wasn't asked for.
**Regulator agent**: `ScopeDriftWarn` emitted with drift_tokens listing non-task additions. App can either auto-strip, ask user, or accept.
**What no competitor does**: Mem0 stores the interaction. Langfuse logs the trace. OPA checks allow/deny. **None detect semantic drift from task to response in real time.**
### Demo 2 — Cost circuit break (`regulator_cost_break_demo.rs`)
**Setup**: Agent given ambiguous task. LLM generates long response, user corrects, LLM retries, cost accumulates.
**Baseline agent**: runs until token cap, returns best-effort. Cost wasted on repeated low-quality attempts.
**Regulator agent**: After 3 turns without quality improvement + cost > threshold, `CircuitBreak { CostCapReached }` emitted with suggestion "ask user to clarify scope". Agent halts loop, returns suggestion to user.
**What no competitor does**: Portkey has provider-level circuit-breakers (e.g., retry on 429). **None halt on quality+cost compound signals.**
### Demo 3 — Procedural correction memory (`regulator_correction_memory_demo.rs`)
**Setup**: Over 5 sessions, user_123 corrects agent 3 times: "don't add docstrings to refactor output". Correction text varies across turns.
**Baseline agent (Mem0 style)**: stores each correction message verbatim. Next session: retrieves similar past corrections by semantic search. Effectiveness depends on retrieval quality.
**Regulator agent**: extracts pattern `{user: user_123, topic: refactor, rule: no_new_docstrings}`. Persists as `CorrectionPattern`. Session 6: emits `ProceduralWarning` BEFORE generation. Pattern is structural, not text-based.
**What no competitor does**: Memory systems store CONTENT. **None extract behavioral PATTERNS from corrections and apply them prospectively.** This is the clearest differentiation.
## 8. Migration path (backwards compatibility)
**0.1 → 0.2 (post-MVP)**:
- All Path 1 APIs stay callable
- `Regulator` added as new top-level surface
- `emotional.rs` regex still used by default (via `CognitiveSession::process_message`), with `#[deprecated]` attribute pointing to `Regulator`
- Tầng 2 delta modulation unchanged
**0.2 → 0.3 (post-demo validation)**:
- `Regulator` is the primary documented API
- `CognitiveSession::process_message(text)` still works but docs recommend `Regulator`
- SSM users: `Regulator::for_user(...)` + `current_delta_modulation()` hook provides Tầng 2
**0.3 → 1.0 (post-real-LLM eval)**:
- Remove `emotional.rs` regex default path (apps can implement their own via `Regulator::session_mut().model.belief.affect.arousal = _`)
- API stability guarantee
## 9. Implementation plan (Sessions 16-25)
| 16 | `src/regulator/mod.rs` skeleton: `Regulator`, `LLMEvent`, `Decision`, empty event-handling dispatch | All existing 324 tests still pass + 5 new unit tests on event dispatch |
| 17 | `TokenStatsAccumulator` + entropy-based confidence. Replaces fixed-0.5 `signals.confidence` base. | Confidence responds to low/high logprob runs; gibberish text produces low confidence |
| 18 | `ScopeTracker` + keyword-overlap scope-drift detector (MVP). Emits `ScopeDriftWarn`. | Demo: task "refactor function" vs response "add logging + error handling" → drift_score > 0.3 |
| 19 | `CostAccumulator` + circuit-break rules. Emits `CircuitBreak`. | Demo: 3-turn quality decline + cost > 1000 tokens → CircuitBreak fires |
| 20 | `CorrectionStore` + procedural pattern extraction from `UserCorrection` events. Extends `LearnedState`. | Demo: 3 similar corrections in different words → single `CorrectionPattern` extracted |
| 21 | `examples/regulator_scope_drift_demo.rs` with anthropic_sdk | End-to-end demo showing scope drift detected on real Claude output |
| 22 | `examples/regulator_cost_break_demo.rs` | End-to-end demo on real LLM |
| 23 | `examples/regulator_correction_memory_demo.rs` | End-to-end demo with persistent state across 2 runs |
| 24 | Real-LLM reliability eval: Regulator-enabled agent vs Regulator-disabled on 50-query benchmark with cost cap | Quality, cost, scope-compliance metrics; publish numbers |
| 25 | Docs: README, `app-contract.md` rewrite, `regulator-guide.md`, practitioner vocabulary migration | Public docs ready for crates.io publish |
**Total**: 10 sessions after Session 15 design. MVP end at Session 23, validated 24, shipped 25.
## 10. Explicitly out of scope (for now)
- **Multi-provider event correlation** (tracking same conversation across Claude + GPT calls) — v1 assumes single provider per Regulator instance
- **Streaming token-by-token intervention** — v1 collects all events, decides post-turn. Per-token intervention (stop mid-stream) deferred
- **Cross-user pattern aggregation** — v1 keeps each user's patterns isolated. Anonymized cross-user learning deferred
- **Hybrid-SSM-aware Tầng 2 extensions** — v1 Tầng 2 works on pure SSM only. Hybrid delta modulation (Jamba/Granite 4.0) deferred pending demand signal
- **Non-Rust bindings (Python/TS)** — v1 is Rust-native only. FFI deferred
## 11. Open questions (resolve during implementation)
1. **Token logprob availability**: Anthropic API does not expose per-token logprobs by default. How to handle? Options:
- (a) Require log probability enablement in API call (when available)
- (b) Fall back to confidence-from-response-structure (entropy of top-k alternatives not available)
- (c) Document Regulator works best with local inference where logprobs are free
→ Decision owed by Session 17.
2. **Scope-drift MVP detection**: keyword overlap is naive. When does it need embeddings?
- Acceptable for Demo 1 if drift_score > 0.3 is interpretable by user
- Embedding-based detection deferred to v0.3 if demo feedback suggests insufficient precision
3. **Correction pattern extraction**: how to differentiate "don't do X" (rule) vs "prefer Y over X" (preference)?
- v1 treats all as rules; preference-vs-rule deferred
- Pattern naming: extract from correction text ("don't add X" → `no_new_X`) or require explicit tagging?
4. **RegulatorState serialization format**: JSON via serde (matches existing LearnedState) or custom binary? JSON for v0.1.
5. **Thread safety**: `Regulator` is `!Send + !Sync` in v1. Multi-user server apps use one Regulator per user per task. Async-safe `Regulator` deferred.
## 11a. Implementation notes (post-ship)
This design was the Session 15 forward-looking spec. Sessions 16-20 shipped
the infrastructure phase; the notes below record resolved open questions and
the design-vs-implementation deltas future readers should be aware of. The
design itself is not rewritten — deviations are preferred at this
boundary so the original intent is still reviewable.
### §11.1 — logprob availability (Session 17)
Resolved as **hybrid** per option (a) + (b) + (c) compromise. Primary path
uses mean-NLL over a rolling window when the provider exposes logprobs
(OpenAI, vLLM, local candle). Fallback path is a language-neutral structural
heuristic (length + `?` density, P9b-compliant). `LOGPROB_UNAVAILABLE = 0.0`
sentinel per §3.2 contract; non-finite / spurious positive values fail-open to
"unavailable". Both paths composed via `confidence_with_fallback`. See
`src/regulator/token_stats.rs` module doc.
### §11.2 — scope-drift detection (Session 18)
Resolved as **keyword-overlap with decision checkpoint**. Metric `|response \
Resolved as **structural, opaque, language-neutral**. Rejected approach (a):
English-regex "don't X" → `no_new_X` rule extraction (P9b red flag — duplicates
cortical work). Shipped approach: `pattern_name = format!("corrections_on_{cluster}")`;
raw `example_corrections` texts stored for app / LLM to interpret at retrieval
time. Differentiation vs Mem0/Letta preserved: Noos surfaces patterns
PROACTIVELY via `Decision::ProceduralWarning` once count threshold trips,
instead of requiring semantic search per turn.
**Deviation from §3.1 and architecture plan**: `correction_patterns` lives on
`RegulatorState` (`src/regulator/state.rs`), NOT extended into
`LearnedState`. Rationale: keeps Layer-0 `types/world.rs` free of Path 2
types; avoids serde tuple-key JSON workarounds; Regulator is 1:1 with user so
`user_id` is implicit and the key can be a plain `String` (topic_cluster).
Plan intent preserved: patterns survive process restart, count ≥ 3 threshold,
cluster-identity shared with strategy learning via
`detector::build_topic_cluster` (moved public in Session 20's P3 refactor).
### §11.4 — `RegulatorState` serialization (Session 20)
JSON via serde (matches `LearnedState`). Extended schema adds
`correction_patterns` with `#[serde(default)]` so pre-Session-20 snapshots
still load. Test `pre_session_20_snapshot_deserialises_with_empty_patterns`
(in `src/regulator/state.rs`) locks the backcompat.
### §11.5 — `Decision` priority ordering (P10, Sessions 19-20)
Design doc §3.3 lists Decision variants without priority. Implementation
locks an explicit order in `Regulator::decide`:
```
1. CircuitBreak(CostCapReached) — hard stop, cost + quality
2. CircuitBreak(QualityDeclineNoRecovery) — hard stop, quality trend
3. ScopeDriftWarn — semantic warning
4. ProceduralWarning — historical-pattern advisory
5. Continue — fallthrough
```
Rationale: urgent stops dominate semantic warnings dominate advisories.
Tests `decide_priority_circuit_break_dominates_scope_drift` (Session 19) and
`decide_priority_scope_drift_dominates_procedural_warning` (Session 20)
encode two adjacent-pair priority assertions. `LowConfidenceSpans` will slot
between `ProceduralWarning` and `Continue` when Session 22+ work surfaces it
(span-local advisory comes last among advisories).
### §11.6 — thread safety (resolved)
Unchanged from §11 point 5: `Regulator` remains `!Send + !Sync` in v1.
Multi-user server apps use one Regulator per user per task.
## 12. References
- Internal memos archived in the project's private `.claude/memory/`
directory record the pivot decision, vocabulary discipline rule, and
v3 diagnostic that surfaced the adapter gap. Those memos are the
authoritative history; this design doc is the public distillation.
- Research agent 3 report (this session transcript) — "cognitive-signal + active intervention" market gap
- MAST NeurIPS 2025 (arxiv 2503.13657) — 14 failure modes this addresses
- Chroma Context Rot (trychroma.com research) — context degradation problem
- Agent Drift (arxiv 2601.04170) — scope drift problem named
- Abstention survey TACL 2025 — confidence calibration gap