Interaction Modes: Companion (Chat) vs Worker (Agent)
Status: DRAFT — pending NAO review Author: Norbert Wiener (alice-core) Date: 2026-05-31 Scope: openalice core — connector inbound gating, coalesce/lane FSM, agentic loop, config.
1. Motivation
1.1 The triggering bug (Katya / diploma chat)
On 2026-05-30 11:34–11:35, Katya forwarded ~5 voice messages into the group chat telegram--5220582659 ("diploma"). Live logs show every voice was successfully transcribed (ElevenLabs Scribe v2, transcripts 980–1248 chars each) and then every one was dropped:
telegram: voice message transcribed (text_len=980)
telegram: voice in group without wake-word and not reply-to-Alice, skipping
... ×5Two independent gates suppressed the content:
rest_client.rs:868— a group voice without a wake-word ("alice/алиса") AND not a reply-to-Alice is skipped (crosstalk reduction).host.rs:1247— any forwarded message is dropped regardless of mention/wake/reply.
The transcription/ingest machinery works perfectly. The failure is the addressee gate: a heuristic that is correct for ambient group chatter but wrong for content a user deliberately hands to Alice by forwarding. Forward fields (forward_origin/forward_date) are not even parsed today.
1.2 The deeper gap
Alice has exactly one interaction model: an addressee-aware companion that responds when addressed and tracks who said what via per-(chat,sender) lanes + AddresseeAnchor. That model is the source of a long bug saga (8LLL/8MMM/8JJJ multi-speaker confusion) and it suppresses forwarded/voice/file content. There is no "ingest everything and work the task" model — no agent surface.
2. The reframe — two modes, same soul
| 🗣 **Companion (Chat)** | 🤖 **Worker (Agent)** | |
|---|---|---|
| Ingest | Addressee-gated (current) | Everything: every speaker, forward, file, voice→transcript, PDF→text |
| Respond | When addressed | When a task is actionable / completed |
| Speakers | Per-speaker lanes + AddresseeAnchor | One shared feed; speaker-labeled but not lane-separated |
| Autonomy | One reply per turn | Multi-step, work-until-done (drives /goal stack) |
| Use case | Companion, stream, group banter (speaker-awareness IS the feature) | "Собери диплом из этих 5 голосовых + PDF" — the chat is a workspace |
Agent mode dissolves both problems at once: it ingests everything by design, and it is speaker-agnostic — eliminating the entire multi-speaker-confusion bug class for task-chats (it needs what, not who).
3. Architecture — modes as a profile, not two code paths
The two modes are named presets of four orthogonal toggles. This is the central simplification: one pipeline, four switches, not two branches.
pub struct ModeProfile {
/// Companion: addressee-gated. Worker: absorb every inbound.
pub ingest_all: bool,
/// Companion: reply-when-addressed. Worker: reply-when-actionable-or-done.
pub respond: RespondPolicy, // WhenAddressed | OnQuiescence
/// Companion: per-(chat,sender) lanes + AddresseeAnchor.
/// Worker: one (chat) lane, shared feed, speaker attribution kept for memory only.
pub speakers: SpeakerModel, // PerSpeaker | SharedFeed
/// Companion: 1 reply = 1 turn. Worker: drain-until-quiescent multi-step.
pub autonomy: Autonomy, // SingleTurn | DrainLoop
}
pub const COMPANION: ModeProfile = /* gated · WhenAddressed · PerSpeaker · SingleTurn */;
pub const WORKER: ModeProfile = /* all · OnQuiescence · SharedFeed · DrainLoop */;Benefits: a third preset (Hybrid) is trivial; tests target the four flags, not "modes"; no flag-soup exposed to users — only the two named presets are public API.
3.1 Mode selection
- Config default in
oa.yaml/alice.yaml(global) — reuses the unified ConfigLoader (Phase 9 / 8KKKK). - Per-chat override (sticky) stored at
memory/{agent}/chat/{scope}/settings/interaction_mode— reuses the existing per-chat settings infra (memory_preload/mode.rs). - Command:
/mode chat·/mode agent·/mode(show current). Distinct from the existingChatModeenum (Roleplay/Coding/…) which only sizes memory budget.
4. Worker-mode execution: work-conserving drain-until-quiescent loop
The agent is a long-running consumer over a live append-only chat stream, not a request→response bot.
on first inbound (WORKER):
enter WORKING; emit typing-indicator (sendChatAction); optional single "принял, работаю 🔄" on long tasks
loop:
step = work_step(feed, tools) # one reasoning/tool step; delegates to /goal stack for big tasks
drain injected inbound → append to feed # mid-flight absorption (steer)
if cancel_requested: abort; emit "отменено"; → IDLE
if budget_exceeded(time|steps|tokens): # ⛔ termination ceiling — see §4.1
emit checkpoint reply ("...вот пока что, продолжаю"); continue or pause
if subtask_ready and worth_delivering(chunk): # incremental checkpoint delivery — see §4.2
emit chunk (partial answer); mark its asks addressed; respect message-rate cap
if step.task_done:
wait settle_window (~coalesce TTL)
if new inbound during settle: continue
else: break
completeness = score(all_user_asks, delivered) # ANSWER_COMPLETENESS over the WHOLE ask-set
if not complete: continue loop
optional consolidated wrap; stop typing; → IDLEGuarantee: every inbound is accounted for (the Katya-style drop is structurally impossible here). Delivery is incremental: Alice emits each answer-chunk at a meaningful checkpoint, keeps absorbing new input and working the rest, and addresses everything — with an optional consolidated wrap at the end. Always responds; never one-and-done; never spam (checkpoint-batched + message-rate capped).
4.1 Critical-review additions (things the naive design forgot)
- ⛔ Termination ceiling. Pure quiescence never terminates in a busy group → no reply + unbounded token burn. A
time | steps | tokensbudget forces a checkpoint reply (partial result + "продолжаю…") and a yield. Liveness > purity. - 💸 Cost cap + kill-switch. Multi-step + sub-agents burn tokens. Reuse Wave-1 worker cap enforcement (
max_tokens_per_run/max_tokens_per_day). Default sub-steps togpt-5.4-mini, escalate on need. A hard kill-switch per task. - 🍴 Fork-bomb guard. Sub-agents spawning sub-agents → enforce depth + concurrency caps (reuse depth-cap probes). Worker mode does not get unbounded recursion.
- 💾 Durable resume. Long tasks must survive an
alicerestart (we redeploy often). Wire worker working-state into durable execution + auto-resume (8FF-5) +LanePersist(SQLite WAL). - 🧠 Memory attribution preserved. The reply is speaker-agnostic, but content is still attributed to its speaker for dossiers/memory. SharedFeed disables
AddresseeAnchorfor the reply, not speaker tagging for memory writes. - 👀 Discoverability.
/modereports the chat's current mode; long-running worker tasks surface progress in the mission cockpit (8FF-2).
4.2 Response model — incremental checkpoint delivery (chosen)
Refined per NAO (2026-05-31). Not "one final message" — Alice delivers answers incrementally: as each sub-task/question resolves she emits that chunk (a partial answer), then continues the rest and absorbs newly-arrived messages, emitting more as they resolve, until the completeness gate confirms every ask is addressed. This beats the competitor norm (one final per turn) — responsive and never drops input.
Anti-spam (the one risk): emit only at meaningful checkpoints (a sub-task is done), never per micro-step; enforce a message-rate cap; let the agent judge "is this chunk worth sending now?".
Final report — resolved (NAO 2026-05-31): YES, task-adaptive.
- Multi-goal / long task → incremental parts plus a consolidated final report: per-goal result (✓ / ✗ / blocked) + artifacts produced. Generated from the completeness-gate result, so it doubles as closure + audit trail ("what's done, what's pending"). This is what makes it read as a real agent.
- Trivial single-chunk task → the answer is the report; the separate wrap is skipped (it would be noise).
- The agent decides by sub-task/goal count.
4.3 Toggle mechanism — code, not prompt
Each ModeProfile flag is a deterministic code-level behavior switch, reinforced (where the model's behavior matters) by which prompt sections get assembled. A mode is never "just a prompt" — a prompt cannot drop an inbound, hold a reply, re-key a lane, or loop execution.
| Flag | Code | Prompt |
|---|---|---|
ingest_all | inbound gate (host.rs) admits or drops the message | — (dropped messages are never seen) |
respond | reply stage holds vs emits | "worker doctrine" block: work then report |
speakers | lane key per-(chat,sender) vs per-(chat) | AddresseeAnchor on (Companion) / off + [speaker, kind]: feed labels (Worker) |
autonomy | one pass vs drain-loop wrapper around agentic_loop | "you may take multiple steps until done" |
This is the reliability edge over prompt-only mode-switching (which the model can ignore).
5. /goal integration — agent mode IS the chat front-end to the 8FF stack
The biggest reuse insight: worker mode is not a new agent engine. Its work_step delegates a large task into the machinery already built in Phase 8FF:
/goaldecomposition (8FF-1) — split a big task into sub-tasks.- Recursive sub-agents (Phase 7 Wave T) — fan sub-tasks to sub-agents (with §4.1 caps).
- Kanban + heartbeats + retry budgets (8FF-4) — track sub-task state.
- Durable execution + auto-resume (8FF-5) — survive restarts.
- Mission-cockpit TUI (8FF-2) — operator view of the running mission.
The chat is the I/O surface of the mission cockpit. We connect existing Lego; we do not pour new infrastructure.
6. Reuse map
| Need | Existing primitive |
|---|---|
| Mid-flight absorption | /steer mid-run injection (Phase 8 / G8 #235) |
| Queue drain post-turn | pending_after_current (8YYY) |
| Settle-window quiescence | Waiting-TTL queue accumulator v2 |
| "All asks answered?" | ANSWER_COMPLETENESS scorer (8XXX M4/M5) |
| Cancel | turn-cancel (8KK) |
| Multi-step turn | run_agentic_loop |
| Per-chat sticky setting | memory_preload/mode.rs settings infra |
| Global default config | unified ConfigLoader (Phase 9 / 8KKKK) |
| Cost caps | Wave-1 worker cap enforcement |
| Task decomposition / sub-agents / durable | Phase 8FF + Phase 7 T |
Genuinely new (thin wiring): forward-field parsing; ingest_all branch in the inbound gate; RespondPolicy::OnQuiescence (suppress intermediate emits); Autonomy::DrainLoop wrapper over agentic_loop; chat-level lane re-key for SharedFeed; typing-heartbeat; ModeProfile + /mode command.
7. Safety / permissions (gov-grade, per project bar)
Worker mode = elevated power (bash, file writes, web, sub-agents, long loops). Boundary:
- Allowlist of who may set a chat to agent mode (reuse internal-auth + per-chat settings).
- Per-task token budget cap + kill-switch (Wave-1 caps).
- HITL approval for dangerous actions (rm / money / external publication) — reuse existing HITL gate + Constitutional Critic.
- Depth + concurrency caps on sub-agents.
- Companion mode is unchanged — no new powers, no new risk surface.
8. Phased implementation plan
P0 — Forward-fix (ships independently, un-breaks Katya today).
- Parse
forward_origin/forward_dateinto the TG inbound structs. - Treat a forwarded message as explicit intent: bypass both suppression gates. - Companion mode: ingest + respond. - (Worker mode lands later; until then forwards behave as "addressed" in Companion.)
- TDD: failing test reproducing the group-forward-voice drop → fix → green.
- Branch + lab smoke before mvp.
- Constraint (NAO 2026-05-31): Katya's live chat is reference-only — build + test the capability against fixtures; do NOT action her existing messages. No deploys during this work (dev branch + lab only).
P1 — ModeProfile + selection.
ModeProfilestruct +COMPANION/WORKERpresets;/modecommand; per-chat sticky setting + config default. Companion preset = byte-identical current behavior (regression-guarded).
P2 — Worker ingest + SharedFeed.
ingest_allbranch in the inbound gate; chat-level lane re-key; speaker attribution retained for memory;AddresseeAnchoroff in SharedFeed.
P3 — Worker autonomy loop.
RespondPolicy::OnQuiescence+Autonomy::DrainLoopoveragentic_loop; termination ceiling; typing-heartbeat; completeness-gated finalization; cancel.
P4 — `/goal` wiring + safety.
work_stepdelegates large tasks to the 8FF stack; cost caps; depth/concurrency caps; HITL on dangerous ops; durable resume.
Each phase: tests + doc comments + feature-registry entries; branch + lab acceptance before mvp; no prod (dev-only per current directive).
9. Non-goals (YAGNI)
- No cross-chat agent missions (one chat = one feed) in v1.
- No autonomous self-initiated work (worker reacts to chat input; Voyager stays separate).
- No exposed 4-flag matrix UI — only the two named presets are public.
- No new STT/file infra — transcription + inbox extraction already exist; we only stop dropping their output.