Skip to content

agents.ambassador

agents.ambassador

The Ambassador — the conversational front door (Track B / the Voice).

A reasoning agent that is computationally light: it reasons about intent, answers from already-grounded material on the pinned tier, reads its own operational state, captures the dialogue as authored-dialogue, and delegates heavy work to the async scheduler — read + propose, never write + act. See agent.py for the full design.

AMBASSADOR_ROLE = "You are the Ambassador of the owner's sealed personal mind-palace — the voice they talk to. You are a mirror onto their own mind, not an oracle (Constitution §III.2): reflect what they actually wrote, hold interpretive claims loosely, and let them draw the conclusions — the judgment is theirs.\n\nGround every substantive claim in the retrieved notes below and cite them by title in double brackets, e.g. [[note title]]. Never invent notes, quotes, facts, or citations. When their notes don't cover something, say so plainly instead of guessing.\n\nBe concise and warm; surface options and tradeoffs, not directives. On health, financial, or legal matters be substantive and honest about uncertainty, but defer the final decision to the owner and a qualified professional (Constitution §III.3)." module-attribute

AMBASSADOR_TEMPLATE = RoleTemplate(name='ambassador', prompt_fragment=AMBASSADOR_ROLE, default_tier='router', scope=(frozenset())) module-attribute

Ambassador dataclass

server instance-attribute

librarian instance-attribute

ops_view instance-attribute

budgeter instance-attribute

dreams_view = None class-attribute instance-attribute

tier = 'router' class-attribute instance-attribute

capture_sink = None class-attribute instance-attribute

attestor = None class-attribute instance-attribute

delegate = None class-attribute instance-attribute

pending_results = None class-attribute instance-attribute

verdict_transport = None class-attribute instance-attribute

interruption = field(default_factory=InterruptionPolicy) class-attribute instance-attribute

role_prompt = AMBASSADOR_ROLE class-attribute instance-attribute

history_max_turns = 6 class-attribute instance-attribute

respond(text, *, conversation='default')

handler(text)

The CoreInbox Handler: text -> reply text (single default conversation).

transport_verdict(signed)

Carry a signed owner verdict inbound to the verify+apply seam — TRANSPORT ONLY.

The Ambassador holds no verdict authority: it never signs, verifies, or applies a verdict (each is outside its read+propose scope). It only forwards the signed artifact to the injected receiver, which verifies against the owner PUBLIC key and applies it. A compromised Ambassador can thus drop/reorder but never forge a verdict (verdict-authority.md §4). Raises if no transport is wired — the Ambassador cannot manufacture verdict authority it lacks.

DeliveredResult dataclass

A completed delegated task's result, ready to surface as an EXPECTED update.

ref instance-attribute

topic instance-attribute

text instance-attribute

Turn dataclass

reply instance-attribute

intent instance-attribute

sources = () class-attribute instance-attribute

check = None class-attribute instance-attribute

Intent

Bases: StrEnum

RETRIEVE = 'retrieve' class-attribute instance-attribute

EXPLAIN = 'explain' class-attribute instance-attribute

STATUS = 'status' class-attribute instance-attribute

DREAMS = 'dreams' class-attribute instance-attribute

TASK = 'task' class-attribute instance-attribute

CAPTURE = 'capture' class-attribute instance-attribute

InterruptionPolicy dataclass

Whether the Ambassador may speak UNPROMPTED. The "would the owner want this raised?" judgment is the model-advised part; this policy is the code that acts on it, and the owner-tunable default is EARNED_ONLY (note §3 — "not a silent oracle, not a nag").

A single dial (off / earned-only / verbose) is shipped; per-category sensitivity is left a documented future extension (note §7 leaves it genuinely open — avoid the premature- abstraction trap on an axis nothing uses yet). Expected updates (a result the owner asked for) are NOT governed by this — those are always delivered; this gates only the unprompted.

sensitivity = Sensitivity.EARNED_ONLY class-attribute instance-attribute

admits(worth_raising)

Sensitivity

Bases: StrEnum

OFF = 'off' class-attribute instance-attribute

EARNED_ONLY = 'earned_only' class-attribute instance-attribute

VERBOSE = 'verbose' class-attribute instance-attribute

classify(text, *, chat=None)

Floor first; earn the model only for the ambiguous rest (note §5).

classify_floor(text)

The deterministic floor: an Intent for an obvious message, else None (escalate).

narrate_effort(topic='')

Plain-language "I need to go work on this" — the RIGHT register (note §4): honest about the wait + the rough plan, never the plumbing. A pure function from "what was delegated" to one plain sentence (unit-tested to leak no internal noun).

build_ambassador(config=None, *, delegate=None, pending_results=None, server=None, embedder=None, store=None, drift=None, derived=None)

Wire an Ambassador against the real configured stores + models.

Delegation (the gate→queue seam + completed-result surfacing) is INJECTED by the scheduler-layer wiring (scheduler/interface.py), which owns the queue — the Ambassador never imports the scheduler, so this base build leaves delegate/pending_results as passed (None → TASK still attests + narrates effort, just enqueues nothing).

server/embedder/store are injectable so the offline CLI + tests can swap the two model-backed pieces for deterministic stand-ins without touching the real machinery. The ONE vector store is shared by the librarian (reads) and the dialogue capture (writes) so a captured turn is immediately retrievable. drift is an optional bound DriftReport reading for the ops-view (None per-chat — drift is measured by a separate job, not on every turn).

agent

The Ambassador — the conversational front door (Track B; ambassador-as-reasoning-agent.md).

A reasoning agent that is computationally light (note §0): it genuinely reasons about each message (the intent step), but holds no heavy work inline — it answers from already-grounded material on the always-warm pinned tier, and delegates anything expensive to the async scheduler. "Wide window, narrow hands": it can read the authored mirror, the curated self-knowledge graph, and the operational state, but its authority is read + propose, never write + act (nervous-system-and-ambassador.md §4). Every step is attested.

The five paths (ambassador-interpretation-and-flow.md §3, split by which graph retrieval reads): RETRIEVE → the authored mirror → grounded answer (inline). [B2, B5] EXPLAIN → the CURATED self-knowledge graph → grounded answer (inline). [B2, B4] STATUS → the read-only ops-view, narrated in plain language (inline). [B2, B3] TASK → compose a scoped task → gate → queue; narrate the effort. [B2] CAPTURE → (the owner's message is stored as authored-dialogue) → ack. [B1, B2]

Context for the inline answers is assembled through the §13 budgeter (B5): the Ambassador chooses what to put in ContextParts (which chunks, how much history); the budgeter is the one thing that enforces the window — "agent-judged retrieval within the budgeter ceiling".

AMBASSADOR_ROLE = "You are the Ambassador of the owner's sealed personal mind-palace — the voice they talk to. You are a mirror onto their own mind, not an oracle (Constitution §III.2): reflect what they actually wrote, hold interpretive claims loosely, and let them draw the conclusions — the judgment is theirs.\n\nGround every substantive claim in the retrieved notes below and cite them by title in double brackets, e.g. [[note title]]. Never invent notes, quotes, facts, or citations. When their notes don't cover something, say so plainly instead of guessing.\n\nBe concise and warm; surface options and tradeoffs, not directives. On health, financial, or legal matters be substantive and honest about uncertainty, but defer the final decision to the owner and a qualified professional (Constitution §III.3)." module-attribute

AMBASSADOR_TEMPLATE = RoleTemplate(name='ambassador', prompt_fragment=AMBASSADOR_ROLE, default_tier='router', scope=(frozenset())) module-attribute

DeliveredResult dataclass

A completed delegated task's result, ready to surface as an EXPECTED update.

ref instance-attribute
topic instance-attribute
text instance-attribute

Turn dataclass

reply instance-attribute
intent instance-attribute
sources = () class-attribute instance-attribute
check = None class-attribute instance-attribute

ChatServer

Bases: Protocol

The Ambassador's real dependency on ModelServer (core/models/server.py): "chat at a tier, get text back." A Protocol rather than the concrete class so test fakes (bare .chat(tier, messages, ...) -> str stand-ins) satisfy the type structurally, without inheriting from or importing the real ModelServer.

chat(tier, messages, **kwargs)

Ambassador dataclass

server instance-attribute
librarian instance-attribute
ops_view instance-attribute
budgeter instance-attribute
dreams_view = None class-attribute instance-attribute
tier = 'router' class-attribute instance-attribute
capture_sink = None class-attribute instance-attribute
attestor = None class-attribute instance-attribute
delegate = None class-attribute instance-attribute
pending_results = None class-attribute instance-attribute
verdict_transport = None class-attribute instance-attribute
interruption = field(default_factory=InterruptionPolicy) class-attribute instance-attribute
role_prompt = AMBASSADOR_ROLE class-attribute instance-attribute
history_max_turns = 6 class-attribute instance-attribute
respond(text, *, conversation='default')
handler(text)

The CoreInbox Handler: text -> reply text (single default conversation).

transport_verdict(signed)

Carry a signed owner verdict inbound to the verify+apply seam — TRANSPORT ONLY.

The Ambassador holds no verdict authority: it never signs, verifies, or applies a verdict (each is outside its read+propose scope). It only forwards the signed artifact to the injected receiver, which verifies against the owner PUBLIC key and applies it. A compromised Ambassador can thus drop/reorder but never forge a verdict (verdict-authority.md §4). Raises if no transport is wired — the Ambassador cannot manufacture verdict authority it lacks.

intent

Intent reasoning for the Ambassador (Track B / B2).

The authoritative note (§5) is explicit: the intent step is a reasoning step, not a bucket classifier — but a deterministic floor handles the cheap, obvious cases (a clear retrieval, a clear status query) before the model is earned for the ambiguous/compound rest. "Floor for the obvious; mind for the rest."

So this module is two separately-testable units
  • classify_floor — pure, deterministic; returns an Intent for an unambiguous message, else None ("not obvious — earn the model").
  • classify_model — the model-earned fallback; asks the pinned model to reason about intent, parses forgivingly, and degrades to a safe heuristic if the model is unavailable/unparseable.

The five intents map to the conversation loop's paths (ambassador-interpretation-and-flow.md §3), with retrieval split by which graph it reads — the mirror (authored) vs. the curated self-knowledge graph — so the firewall (curated ∉ mirror) is honored at the intent boundary.

CLASSIFIER_ROLE = "Classify the owner's message into exactly ONE intent and reply with only that single word.\n- retrieve: they want to recall or find something from their OWN notes/journals.\n- explain: they're asking how YOU (the system) work — your design or guarantees.\n- status: they're asking what you've been doing, or whether you are healthy.\n- dreams: they're asking what PATTERNS, themes, or insights you've noticed across their notes.\n- task: they're asking you to go look into / research / investigate something.\n- capture: they're telling you something (a thought, a journal entry) rather than asking.\nReply with one word: retrieve, explain, status, dreams, task, or capture." module-attribute

Intent

Bases: StrEnum

RETRIEVE = 'retrieve' class-attribute instance-attribute
EXPLAIN = 'explain' class-attribute instance-attribute
STATUS = 'status' class-attribute instance-attribute
DREAMS = 'dreams' class-attribute instance-attribute
TASK = 'task' class-attribute instance-attribute
CAPTURE = 'capture' class-attribute instance-attribute

classify_floor(text)

The deterministic floor: an Intent for an obvious message, else None (escalate).

heuristic_fallback(text)

Model-free default for ambiguous messages: a question is a retrieval (blast radius = a wrong query, which is read-only and recoverable — note §1b); a statement is a capture.

classify_model(text, chat)

The model-earned step. chat(text) -> reply asks the pinned model to name the intent; parse the first intent word it returns. Forgiving: any unparseable/empty reply degrades to the deterministic heuristic, so a missing model never breaks the conversation.

classify(text, *, chat=None)

Floor first; earn the model only for the ambiguous rest (note §5).

policy

The two cross-cutting build deltas (authoritative note §5): effort narration + the earned-interruption policy.

Both follow "model advises, code acts": the judgment ("is this worth raising?", "what to say") may be model-advised, but the policy (whether to deliver an unprompted message at all, the default-off knob) and the register (plain language, never internals) are plain, testable code.

LEAKY_NOUNS = ('synthesis', 'tier', 'queue', 'job', 'token', 'accessor', 'signature', 'sqlite', 'lancedb', 'embedding', 'vector', 'attestation', 'ledger', 'podman', 'ollama', 'qwen') module-attribute

TASK_PREFIXES = ('look into whether', 'look into', 'dig into', 'dig through', 'investigate', 'find out whether', 'find out', 'figure out', 'research whether', 'do some research on', 'do some research', 'can you research', 'go research', 'explore whether', 'look at whether', 'cross-check') module-attribute

Sensitivity

Bases: StrEnum

OFF = 'off' class-attribute instance-attribute
EARNED_ONLY = 'earned_only' class-attribute instance-attribute
VERBOSE = 'verbose' class-attribute instance-attribute

InterruptionPolicy dataclass

Whether the Ambassador may speak UNPROMPTED. The "would the owner want this raised?" judgment is the model-advised part; this policy is the code that acts on it, and the owner-tunable default is EARNED_ONLY (note §3 — "not a silent oracle, not a nag").

A single dial (off / earned-only / verbose) is shipped; per-category sensitivity is left a documented future extension (note §7 leaves it genuinely open — avoid the premature- abstraction trap on an axis nothing uses yet). Expected updates (a result the owner asked for) are NOT governed by this — those are always delivered; this gates only the unprompted.

sensitivity = Sensitivity.EARNED_ONLY class-attribute instance-attribute
admits(worth_raising)

topic_of(text)

A short topic for a delegated request — strip the delegation verb, cap the length. Shared by the effort narration and the delegated-result surfacing so they read consistently.

narrate_effort(topic='')

Plain-language "I need to go work on this" — the RIGHT register (note §4): honest about the wait + the rough plan, never the plumbing. A pure function from "what was delegated" to one plain sentence (unit-tested to leak no internal noun).

parse_sensitivity(value)

Tolerant config parse — an unknown value degrades to the safe default (earned-only).