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 anIntentfor an unambiguous message, elseNone("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).