Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,23 @@ Composite scores are always in [0,1]. Typical production floor: 0.3-0.7.

### Brain pack — 15 verbs (`brain.` prefix)

| Verb | What it does | When to use |
| ------------------------ | ---------------------------------------------------------------------- | ---------------------------------------------------------- |
| `brain.event_counts` | Windowed event counts by kind/actor/verb (+ feedback by_profile split) | Flywheel metrics, feedback-coverage reporting |
| `brain.profiles` | List profiles (optionally filtered by lifecycle) | See what profiles exist |
| `brain.profile` | Full detail: metadata, snapshot, state summary | Inspect a specific profile |
| `brain.create_profile` | Create a new profile with optional seed priors | Custom tuning for a new consumer |
| `brain.resolve` | Which profile serves a given consumer context? | Before recall — check active tuning |
| `brain.activate` | Start live update loop for a profile | Enable feedback-driven tuning |
| `brain.deactivate` | Stop live updates, retain state | Pause tuning without losing progress |
| `brain.archive` | Read-only, audit-retained | Retire a profile permanently |
| `brain.reset` | Reset posteriors to priors (preserves event history) | Start tuning fresh |
| `brain.feedback` | Emit explicit feedback event | Rate a recall result as useful/not_useful/wrong |
| `brain.auto_feedback` | Emit implicit feedback for recall results | Convenience: agents call after memory.recall |
| `brain.bind` | Bind a profile to an actor + consumer | Route a specific caller to a specific profile |
| `brain.unbind` | Remove a binding | Stop routing |
| `brain.bindings` | List binding rows | Audit profile routing |
| `brain.register_adapter` | Register an adapter integrity record | Gate adapter composition to the active base-model revision |
| Verb | What it does | When to use |
| ------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `brain.event_counts` | Windowed event counts by kind/actor/verb (+ feedback by_profile split, cost_unit totals) | Flywheel metrics, feedback-coverage reporting, cost attribution |
| `brain.profiles` | List profiles (optionally filtered by lifecycle) | See what profiles exist |
| `brain.profile` | Full detail: metadata, snapshot, state summary | Inspect a specific profile |
| `brain.create_profile` | Create a new profile with optional seed priors | Custom tuning for a new consumer |
| `brain.resolve` | Which profile serves a given consumer context? | Before recall — check active tuning |
| `brain.activate` | Start live update loop for a profile | Enable feedback-driven tuning |
| `brain.deactivate` | Stop live updates, retain state | Pause tuning without losing progress |
| `brain.archive` | Read-only, audit-retained | Retire a profile permanently |
| `brain.reset` | Reset posteriors to priors (preserves event history) | Start tuning fresh |
| `brain.feedback` | Emit explicit feedback event | Rate a recall result as useful/not_useful/wrong |
| `brain.auto_feedback` | Emit implicit feedback for recall results | Convenience: agents call after memory.recall |
| `brain.bind` | Bind a profile to an actor + consumer | Route a specific caller to a specific profile |
| `brain.unbind` | Remove a binding | Stop routing |
| `brain.bindings` | List binding rows | Audit profile routing |
| `brain.register_adapter` | Register an adapter integrity record | Gate adapter composition to the active base-model revision |

### Comm pack — 7 verbs (`comm.` prefix)

Expand Down
18 changes: 18 additions & 0 deletions crates/khive-db/src/writer_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ async fn run_writer_task(
mod tests {
use super::*;
use crate::pool::PoolConfig;
use serial_test::serial;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
Expand All @@ -432,7 +433,14 @@ mod tests {
ConnectionPool::new(cfg).expect("pool open")
}

// `#[serial(tx_registry)]`: `run_writer_task` registers a `writer_task_tx`
// handle in the process-wide `tx_registry` singleton for the life of each
// `BEGIN IMMEDIATE`. Tests that observe the registry (the checkpoint
// `tx_age_sweep_*` group) read `tx_registry::oldest()`; an un-serialized
// spawning test here would leak a longer-lived `writer_task_tx` into that
// read and make the sweep name the wrong transaction. Share the key.
#[tokio::test]
#[serial(tx_registry)]
async fn begin_immediate_failure_replies_error_without_running_op() {
// Real lock contention, not a simulation: hold the database-level
// write lock from the pool's own writer connection (the unmigrated
Expand Down Expand Up @@ -504,7 +512,11 @@ mod tests {
);
}

// `#[serial(tx_registry)]`: shares the key with the checkpoint
// `tx_age_sweep_*` tests — see the note on
// `begin_immediate_failure_replies_error_without_running_op`.
#[tokio::test]
#[serial(tx_registry)]
async fn writer_task_executes_op_and_commits() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("writer_task_commit.db");
Expand Down Expand Up @@ -627,7 +639,13 @@ mod tests {
first.abort();
}

// `#[serial(tx_registry)]`: this test deliberately keeps a request (and
// thus its `writer_task_tx` registry handle) alive past a timeout, so it is
// the worst polluter of the checkpoint `tx_age_sweep_*` reads if left
// un-serialized. Shares the key — see the note on
// `begin_immediate_failure_replies_error_without_running_op`.
#[tokio::test]
#[serial(tx_registry)]
async fn send_with_timeout_returns_op_result_when_op_outlives_the_timeout() {
// `send_with_timeout`'s timeout must bound ONLY the enqueue step —
// never the reply-wait. An accepted request (channel not full) must
Expand Down
12 changes: 11 additions & 1 deletion crates/khive-mcp/src/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5048,11 +5048,21 @@ backend = "kg-backend"
);

use clap::Parser;
let args = Args::parse_from(["mcp", "--config", config_path.to_str().expect("utf8 path")]);
let args = Args::parse_from([
"mcp",
"--config",
config_path.to_str().expect("utf8 path"),
"--no-embed",
]);

let (server, schedule_rt) = build_server(&args).expect("build_server must succeed");
// No `[packs.schedule]` entry above, so it defaults to "main".
let rt = schedule_rt.expect("schedule pack is loaded by default");
assert!(
rt.config().embedding_model.is_none()
&& rt.config().additional_embedding_models.is_empty(),
"the backend-routing fixture must remain independent of external embedding models"
);

let marker = "adr106-multi-backend-dispatch-marker";
let action_dsl = format!("create(kind=\"observation\", content=\"{marker}\")");
Expand Down
67 changes: 52 additions & 15 deletions crates/khive-pack-brain/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ pub(crate) static BRAIN_HANDLERS: &[HandlerDef] = &[
plane (ADR-103 Stage 1, #724 Ask A); feedback_explicit events additionally split by \
served_by_profile_id; events carrying a work_class (today: phase_started / \
phase_completed / phase_cancelled payloads, checked before any future \
payload.resource.work_class) split by counts_by_work_class. cost_unit is not \
surfaced: it does not exist on any event payload yet (ADR-103 Stage 0 design-only) \
and will be added once a resource payload carries it.",
payload.resource.work_class) split by counts_by_work_class. Events carrying \
payload.resource.cost_unit (ADR-103 Amendment 1; stamped on every successful verb \
dispatch since PR #927) sum into total_cost_unit and cost_unit_by_verb; events with \
no resource.cost_unit (pre-Amendment-1 events, or errored/denied dispatches) simply \
do not contribute. Both fields are omitted, not zero-filled, when no event in the \
window carries cost_unit. When truncated=true, both sums are over the fetched page \
only, same as the other counts_by_* fields.",
visibility: khive_types::Visibility::Verb,
category: khive_types::VerbCategory::Assertive,
params: &[
Expand Down Expand Up @@ -680,14 +684,16 @@ impl BrainPack {
// where a generic audit row carries a `resource` sub-object (ADR-103
// Stage 1's "resource payload enrichment", still open). Events with
// neither path present are simply not counted in the split.
// `cost_unit` is NOT surfaced: as of this PR no event payload carries it
// anywhere in the codebase (ADR-103 Stage 0 defines it; Stage 1's
// resource-payload work has not landed a `cost_unit` field on any
// producer yet). Surfacing a field that no event can ever populate today
// would be a documentation lie dressed as a response key; once a
// producer emits `cost_unit` (under `payload.resource.cost_unit`, by the
// same convention as `work_class`), this verb should sum it the same way
// `counts_by_work_class` groups `work_class`.
// `cost_unit` (ADR-103 Amendment 1, `payload.resource.cost_unit`,
// stamped on every successful verb dispatch since PR #927 via
// `khive_runtime::cost_unit::resource_payload`) sums into
// `total_cost_unit` and `cost_unit_by_verb`, both omitted rather than
// zero-filled when no event in the window carries it. Amendment 1's
// "absence has exactly two meanings" rule means a missing
// `resource.cost_unit` is either a pre-Amendment-1 event or an
// errored/denied dispatch — this verb does not distinguish the two, it
// simply excludes the event from the sum, matching the `work_class`
// split's existing exclusion convention.
//
// Named `brain.event_counts` rather than the literal
// `brain.events(...)` shape sketched on the issue — that name is already
Expand Down Expand Up @@ -798,6 +804,9 @@ impl BrainPack {
std::collections::BTreeMap::new();
let mut counts_by_work_class: std::collections::BTreeMap<String, u64> =
std::collections::BTreeMap::new();
let mut total_cost_unit: i64 = 0;
let mut cost_unit_by_verb: std::collections::BTreeMap<String, i64> =
std::collections::BTreeMap::new();

for event in &page.items {
*counts_by_kind
Expand All @@ -819,6 +828,11 @@ impl BrainPack {
.entry(work_class.to_string())
.or_insert(0) += 1;
}
if let Some(cost_unit) = event_cost_unit(&event.payload) {
total_cost_unit = total_cost_unit.saturating_add(cost_unit);
let entry = cost_unit_by_verb.entry(event.verb.clone()).or_insert(0);
*entry = entry.saturating_add(cost_unit);
}
}

let mut result = json!({
Expand All @@ -839,6 +853,10 @@ impl BrainPack {
if !counts_by_work_class.is_empty() {
result["counts_by_work_class"] = json!(counts_by_work_class);
}
if !cost_unit_by_verb.is_empty() {
result["total_cost_unit"] = json!(total_cost_unit);
result["cost_unit_by_verb"] = json!(cost_unit_by_verb);
}
Ok(result)
}

Expand Down Expand Up @@ -2299,10 +2317,10 @@ fn parse_rfc3339_micros(field: &'static str, value: &str) -> Result<i64, Runtime
/// Checks two paths, phase-payload first:
/// 1. `payload.work_class` — the shape `PhaseStarted`/`PhaseCompleted`/`PhaseCancelled`
/// events use today (`khive_storage::telemetry::{PhaseStartedPayload, ...}`).
/// 2. `payload.resource.work_class` — the not-yet-emitted shape a generic audit row would
/// use once ADR-103 Stage 1's "resource payload enrichment" lands `work_class` on
/// non-phase events too. No producer writes this today; the fallback exists so this
/// verb does not need a second change when one starts.
/// 2. `payload.resource.work_class` — the shape successful-dispatch audit rows write:
/// `khive_runtime::cost_unit` constructs the resource object with `work_class` and the
/// runtime persists it on successful audit rows. Phase payloads keep precedence when
/// both paths are present.
///
/// Returns `None` (silently excluded from the split, not an error) when neither path is
/// present — the overwhelming majority of event kinds today.
Expand All @@ -2318,6 +2336,25 @@ fn event_work_class(payload: &Value) -> Option<&str> {
})
}

/// Extract `cost_unit` from an event payload for `brain.event_counts`'
/// `total_cost_unit` / `cost_unit_by_verb` aggregation (ADR-103 Amendment 1).
///
/// Reads `payload.resource.cost_unit` only — unlike `work_class`, `cost_unit`
/// has no top-level payload shape: it is emitted exclusively by
/// `khive_runtime::cost_unit::resource_payload` on the per-dispatch audit
/// row's `resource` sub-object, never by the Phase* background-work events.
///
/// Returns `None` (silently excluded from the sum, not an error or a zero)
/// when the field is absent, which per Amendment 1's "absence has exactly
/// two meanings" rule is either a pre-Amendment-1 event or an
/// errored/denied dispatch.
fn event_cost_unit(payload: &Value) -> Option<i64> {
payload
.get("resource")
.and_then(|resource| resource.get("cost_unit"))
.and_then(Value::as_i64)
}

// ── lattice-router seam (#345 M1 / #352 M2) ──────────────────────────────────

/// Context-vector dimension for the lattice-fann router seam.
Expand Down
Loading
Loading