Skip to content
Closed
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ loosely while pre-1.0 (breaking changes can land on minor bumps).

## [Unreleased]

- **Local label filter for presence silence and no-cue intent.**
With `ARBITER_DECISION_MODEL` set to an `ollama/…` id, a closed-set
next-token distribution can skip a presence review on an extreme
`silent`, or route a no-cue utterance without the advisor completion.
Both margins default to off. The filter cannot write a note, a brief
beyond the canned route line, or a halt. See
[Presence](docs/concepts/presence.md#silence-filter) and
[Intent](docs/concepts/intent.md#hybrid-classify).

## [0.13.11] — 2026-09-21

- **Fleet dashboard pane (Phase C1).** The TUI consumes fleet SSE (`stream_id` +
Expand Down
32 changes: 32 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ set(ARBITER_SOURCES
src/constitution.cpp
src/orchestrator.cpp
src/intent.cpp
src/label_score.cpp
src/reconcile.cpp
src/advisor_gate.cpp
src/advisor.cpp
Expand Down Expand Up @@ -560,6 +561,7 @@ if(INDEX_BUILD_TESTS)
add_executable(unit_api_client
tests/test_api_client.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand All @@ -582,6 +584,7 @@ if(INDEX_BUILD_TESTS)
add_executable(unit_api_client_pool
tests/test_api_client_pool.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand All @@ -603,6 +606,7 @@ if(INDEX_BUILD_TESTS)
add_executable(unit_api_client_pool_cancel
tests/test_api_client_pool_cancel.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand Down Expand Up @@ -846,6 +850,7 @@ if(INDEX_BUILD_TESTS)
src/atomic_file.cpp
src/json.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/tui/opentui/pane_input_editor.cpp
Expand Down Expand Up @@ -969,6 +974,7 @@ if(INDEX_BUILD_TESTS)
src/workspace_root.cpp
src/json.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/styled_text.cpp
Expand Down Expand Up @@ -1015,6 +1021,7 @@ if(INDEX_BUILD_TESTS)
src/workspace_root.cpp
src/json.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/styled_text.cpp
Expand Down Expand Up @@ -1048,6 +1055,7 @@ if(INDEX_BUILD_TESTS)
src/workspace_root.cpp
src/json.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/styled_text.cpp
Expand Down Expand Up @@ -1086,6 +1094,7 @@ if(INDEX_BUILD_TESTS)
src/workspace_root.cpp
src/json.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/styled_text.cpp
Expand Down Expand Up @@ -1390,6 +1399,7 @@ if(INDEX_BUILD_TESTS)
src/event_routing.cpp
src/constitution.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand Down Expand Up @@ -1614,6 +1624,7 @@ if(INDEX_BUILD_TESTS)
src/a2a/sse_reader.cpp
src/constitution.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand Down Expand Up @@ -1655,6 +1666,7 @@ if(INDEX_BUILD_TESTS)
tests/test_constitution.cpp
src/constitution.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand All @@ -1680,6 +1692,7 @@ if(INDEX_BUILD_TESTS)
src/model_context.cpp
src/constitution.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand Down Expand Up @@ -1707,6 +1720,7 @@ if(INDEX_BUILD_TESTS)
src/model_context.cpp
src/constitution.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand All @@ -1731,6 +1745,7 @@ if(INDEX_BUILD_TESTS)
src/model_catalog.cpp
src/model_context.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand Down Expand Up @@ -1772,6 +1787,7 @@ if(INDEX_BUILD_TESTS)
src/advisor_gate.cpp
src/advisor.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand All @@ -1794,6 +1810,7 @@ if(INDEX_BUILD_TESTS)
tests/test_presence.cpp
src/presence.cpp
src/api_client.cpp
src/label_score.cpp
src/circuit_breaker.cpp
src/metrics.cpp
src/json.cpp
Expand All @@ -1815,13 +1832,28 @@ if(INDEX_BUILD_TESTS)
add_executable(unit_intent
tests/test_intent.cpp
src/intent.cpp
src/label_score.cpp
src/json.cpp
)
target_include_directories(unit_intent PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/third_party/doctest
)
add_test(NAME unit_intent COMMAND unit_intent)

# Closed-set label distribution: renormalize, missing-label fail-open,
# OpenAI logprob body parse, margin floors. No provider client.
add_executable(unit_label_score
tests/test_label_score.cpp
src/label_score.cpp
src/json.cpp
)
target_include_directories(unit_label_score PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/third_party/doctest
)
add_test(NAME unit_label_score COMMAND unit_label_score)

# Intent reconcile: admit, contract compile, observe ΔS, JIT ensure
# waves, verification mandate, implement stub, snapshot rollback.
# No provider client.
Expand Down
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ calendar commitments.
- [ ] **Delegation policies-** Consitutions declare max depth, allowed callees, budget caps (tokens/$)
- [ ] ~~**Workflow recipes-** Checked-in “crews” (JSON): ordered/parallel graphs of agents + shared todo board~~
- [ ] **Advisor policy packs-** Reusable gate profiles (strict / coding / research)
- [ ] **Decision-filter measurement-** Refit `ARBITER_PRESENCE_SILENCE_MARGIN` and `ARBITER_INTENT_ROUTE_MARGIN` from `filter_*` / `decision_*` traces. The advisor gate stays off this path until those floors exist.

**Acceptance criteria:**
- [x] A user can watch and steer a 5-agent job.
Expand Down
3 changes: 2 additions & 1 deletion docs/api/intent.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ curl http://127.0.0.1:8080/v1/intent \
| Field | Meaning |
|-------|---------|
| `kind` | Closed taxonomy: `research` \| `review` \| `write` \| `ops` \| `frontend` \| `backend` \| `plan` \| `market` \| `social` \| `multi` \| `unknown`. |
| `source` | `heuristic` \| `llm` \| `explicit` \| `event` \| `none`. |
| `source` | `heuristic` \| `llm` \| `decision` \| `explicit` \| `event` \| `none`. `decision` is a peaked local label distribution on a no-cue utterance. It carries the canned brief and no seeds. |
| `decision_peak`, `decision_margin`, `decision_label` | Present when the local filter ran, including when it fell through to the LLM. Omitted when the filter is off or the heuristic already routed. |
| `target_agent` | Suggested specialist, or `""` if index should handle it. |
| `applied` | True when `apply_routing` would rewrite an `index` ingress to `target_agent` **and** that id is in the roster used for this request. This endpoint never dispatches. [`POST /v1/orchestrate`](orchestrate.md) additionally requires the agent to be loaded on the request orch (API: tenant catalog; TUI: `agents_dir`). |
| `todo_seeds` / `plan_seeds` | Optional decomposition hints. Not persisted. |
Expand Down
3 changes: 3 additions & 0 deletions docs/cli/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ See [`docs/cli/connect.md`](connect.md). Local provider keys (`OPENROUTER_API_KE
| `ARBITER_CIRCUIT_COOLDOWN_SECONDS` | Cooldown in Open before a half-open probe. Default `30` (clamped ≥0). |
| `ARBITER_INTENT_LLM_RATE_PER_MIN` | Extra per-tenant token bucket for `POST /v1/intent` hybrid/llm classify. Default `20`. `0` disables. Independent of `ARBITER_TENANT_RATE_PER_MIN`. See [Operations → Per-tenant rate / concurrency limiting](../concepts/operations.md#per-tenant-rate--concurrency-limiting). |
| `ARBITER_INTENT_LLM_RATE_BURST` | Burst for the intent LLM bucket. Default `5`. |
| `ARBITER_DECISION_MODEL` | Local `ollama/…` id for the closed-set label filter. Unset, empty, or any non-`ollama/` id leaves the filter off. Hosted models are not called. |
| `ARBITER_PRESENCE_SILENCE_MARGIN` | Margin (peak minus runner-up) required before an extreme `silent` skips a presence review. `0` or unset disables that skip. Clamped to `[0, 1]`. A skip also requires peak ≥ 0.9. See [Presence](../concepts/presence.md#silence-filter). |
| `ARBITER_INTENT_ROUTE_MARGIN` | Margin required before a no-cue utterance routes from the label filter without the advisor completion. `0` or unset disables that route. Clamped to `[0, 1]`. A route also requires peak ≥ 0.9. See [Intent](../concepts/intent.md#hybrid-classify). |

## TUI session durability

Expand Down
4 changes: 4 additions & 0 deletions docs/concepts/intent.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ The executor still speaks [writs](writ.md). Intent never becomes a writ.
```text
explicit non-index agent → source=explicit (no reroute)
heuristic unique match → source=heuristic, maybe reroute
else if no cue and a local label distribution is peaked
→ source=decision, canned brief, no seeds
else if mode=hybrid|llm → one LLM call, tagged <intent> reply
else → fall through to index
```

The decision step is off unless `ARBITER_DECISION_MODEL` is an `ollama/…` id and `ARBITER_INTENT_ROUTE_MARGIN` is greater than 0. It runs only when the heuristic kind is `unknown` (no cue hit). A multi-cue result, a confident cue, and `mode=heuristic` never consult it. The model scores single-letter labels, including `unknown`, and the runtime renormalizes over those letters alone. A route is taken only when the peak is at least 0.9, the margin (peak minus the runner-up) clears the env floor, the winner is a real kind, and that kind maps to an agent on the roster. The brief is the canned `Route to <agent>` line. Todo and phase seeds still come only from the tagged completion, which runs for every other case. A missing label in the provider's top logprobs, a flat distribution, or a transport error leaves routing on the existing path. The margin is a local gate to refit from `decision_peak` / `decision_margin` / `decision_label` on the intent event. It is not a calibrated probability. See [Environment](../cli/environment.md).

Closed `kind` values: `research`, `review`, `write`, `ops`, `frontend`, `backend`, `plan`, `market`, `social`, `multi`, `unknown`. They map onto the personal starter callsigns under `agents/*.json` (`scout`, `vera`, `quill`, `forge`, `loom`, `nexus`, `compass`, `beacon`, `echo`). `multi` means "needs decomposition"; seed slots may be filled.

LLM replies use a tagged grammar (same idea as the [advisor gate](advisor.md)):
Expand Down
6 changes: 6 additions & 0 deletions docs/concepts/presence.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ The review is history-less — one snapshot in, one signal out — matching the

`SILENT` is the default. Surrounding prose is tolerated. Missing `<signal>`, an unknown token, or `CONTEXT` without `<note>` is **malformed Silent** (fail-open). Presence cannot `HALT` or `REDIRECT` — that remains the advisor's job.

## Silence filter

When `ARBITER_DECISION_MODEL` is an `ollama/…` id and `ARBITER_PRESENCE_SILENCE_MARGIN` is greater than 0, each checkpoint scores a short snapshot before `run_presence_review`. The snapshot is the first line of the task (240 bytes) plus tool names. Arguments, result bodies, recent assistant text, and watcher rules are not in it.

The labels are `silent` and `context`. An extreme `silent` — peak at least 0.9 and margin at least the env floor — skips the review and emits `presence` with `kind=silent`, `detail=filter`, plus `filter_peak`, `filter_margin`, and `filter_label`. A peaked `context`, a flat distribution, a missing label, or a transport error still runs the review, which is the only place a note can be written. The filter cannot inject `[PRESENCE: …]`. The margin is a local gate to refit from those fields. It is not a calibrated probability. See [Environment](../cli/environment.md).

## Runtime control flow

Where presence fires inside the dispatch loop:
Expand Down
4 changes: 2 additions & 2 deletions docs/concepts/sse-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Every event on the `/v1/orchestrate` stream has an `event:` line and a `data:` l
| Event | When | Fields |
|-------|------|--------|
| `request_received` | Exactly once, first event on the stream. | `agent`, `tenant`, `tenant_id`, `message` (first 200 chars, ellipsis added if truncated), `channel?` (`"voice"` when the request asked for spoken output). |
| `intent` | Pre-dispatch classify/route on **fresh** ingress (`original_query` omitted) when the ingress agent's `intent.mode` is not `off`. After `request_received`, before `stream_start`. See [Intent](intent.md). | `kind`, `confidence`, `source`, `target_agent`, `applied`, `requested_agent`, `applied_agent`, `brief?`, `todo_seed_count`, `plan_seed_count`, `llm_used?`, `malformed?`. |
| `intent` | Pre-dispatch classify/route on **fresh** ingress (`original_query` omitted) when the ingress agent's `intent.mode` is not `off`. After `request_received`, before `stream_start`. See [Intent](intent.md). | `kind`, `confidence`, `source`, `target_agent`, `applied`, `requested_agent`, `applied_agent`, `brief?`, `todo_seed_count`, `plan_seed_count`, `llm_used?`, `malformed?`, `decision_peak?`, `decision_margin?`, `decision_label?`. |
| `reconcile.progress` | Phase note on [`POST /v1/reconcile`](../api/reconcile.md). | `request_id`, `phase`, `detail?`. |
| `reconcile.delta` | Contract observation: residual vs held clauses. Emitted after the initial observe and after every ensure wave. | `residual`, `held`, `empty`, `wave`. |
| `agent.spawned` | JIT ephemeral clone created for a ΔS cover (`mode=ensure` waves). | `agent`, `clone_id`, `clauses`. |
Expand All @@ -23,7 +23,7 @@ Every event on the `/v1/orchestrate` stream has an `event:` line and a `data:` l
| `sub_agent_response` | After a delegated turn completes (depth > 0). The full turn body in one payload — useful for consumers that don't want to reconstruct from deltas. | `agent`, `stream_id`, `depth`, `content`. |
| `token_usage` | After each turn completes. | `agent`, `stream_id`, `depth`, `model`, `input_tokens`, `output_tokens`, `cache_read_tokens?`, `cache_create_tokens?`. |
| `advisor` | Every advisor interaction. Independent of `tool_call` — fires for the runtime's gate decisions (which the executor never invokes directly) and for the executor's own `/advise` consults. See [Advisor concept](advisor.md). | `agent`, `stream_id`, `kind`, `detail?`, `preview?`, `malformed?`. |
| `presence` | Always-on watcher consult after a peer's tool batch. Fires for `SILENT` and `CONTEXT`. See [Presence](presence.md). | `watcher`, `agent` (the working agent), `stream_id`, `kind` (`silent` \| `context`), `detail?`, `malformed?`. |
| `presence` | Always-on watcher consult after a peer's tool batch. Fires for `SILENT` and `CONTEXT`. See [Presence](presence.md). | `watcher`, `agent` (the working agent), `stream_id`, `kind` (`silent` \| `context`), `detail?`, `malformed?`, `filter_action?`, `filter_peak?`, `filter_margin?`, `filter_label?`. |
| `escalation` | Out-of-band advisor halt. Fires before the corresponding `stream_end` (which arrives with `ok: false`). Only fires at the originating depth — sub-agent halts bubble up via the parent's response, not via duplicate escalation events. | `agent`, `stream_id`, `reason`. |
| `stream_end` | Closes each turn. Any remaining partial line is flushed before this fires, so no `text` events arrive with this `stream_id` after. | `agent`, `stream_id`, `ok`. |
| `error` | Recoverable errors during the request (transient upstream issue). The stream continues or terminates depending on severity. | `message`, plus optional context fields (e.g. `reason`). |
Expand Down
5 changes: 5 additions & 0 deletions include/api_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ struct ApiRequest {
int max_tokens = 1024;
double temperature = 0.3;
bool include_temperature = true;
// Decision-filter only. Ollama's OpenAI-compatible endpoint. Left
// false on every generative call so older Ollama builds never see
// the fields.
bool logprobs = false;
int top_logprobs = 0;
};

struct ApiResponse {
Expand Down
21 changes: 17 additions & 4 deletions include/intent.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// Distinct from Constitution::MemoryConfig::intent_routing, which only
// boosts /mem search types.

#include "label_score.h"

#include <cstddef>
#include <functional>
#include <string>
Expand Down Expand Up @@ -65,13 +67,20 @@ struct Intent {
// Closed taxonomy aligned with starter agents, plus multi/unknown.
std::string kind; // research|review|write|ops|frontend|backend|plan|market|social|multi|unknown
double confidence = 0; // 0..1
std::string source; // heuristic|llm|explicit|event|none
std::string source; // heuristic|llm|decision|explicit|event|none
std::string target_agent; // empty = do not reroute
std::string brief;
std::vector<IntentSeedTodo> todo_seeds;
std::vector<IntentSeedPhase> plan_seeds;
bool llm_used = false;
bool malformed = false;
// Set when the local label scorer ran on a no-cue utterance. peak and
// margin are the renormalized distribution, even when the route was
// not taken and source stayed heuristic/llm.
bool decision_consulted = false;
double decision_peak = 0;
double decision_margin = 0;
std::string decision_label;
};

struct IntentInput {
Expand Down Expand Up @@ -105,11 +114,15 @@ bool intent_should_apply(const IntentConfig& cfg,
bool fresh_ingress = true);

// Hybrid classify: explicit specialist short-circuits; heuristic when
// confident; LLM when mode is hybrid/llm and heuristic is below threshold.
// llm may be null — hybrid then degrades to heuristic-only.
// confident; a peaked local label distribution when the heuristic found
// no cue and `decision_margin` > 0; otherwise the LLM. llm and scorer
// may be null — hybrid then degrades to heuristic-only. decision_margin
// <= 0 leaves the scorer unconsulted.
Intent resolve_intent(const IntentInput& in,
const IntentConfig& cfg,
const IntentLlmFn& llm = nullptr);
const IntentLlmFn& llm = nullptr,
const LabelScorer& scorer = nullptr,
double decision_margin = 0);

// One-line (plus optional GOAL) preamble injected ahead of the user text.
// Empty when there is nothing useful to show the executor.
Expand Down
Loading
Loading