kb://architecture/interaction-modes-companion-agent-2026-05-31active2026-06-17

Interaction Modes: Companion (Chat) vs Worker (Agent)

openalicedesignalice-core

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
... ×5

Two independent gates suppressed the content:

  1. rest_client.rs:868 — a group voice without a wake-word ("alice/алиса") AND not a reply-to-Alice is skipped (crosstalk reduction).
  2. host.rs:1247any 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)**
IngestAddressee-gated (current)Everything: every speaker, forward, file, voice→transcript, PDF→text
RespondWhen addressedWhen a task is actionable / completed
SpeakersPer-speaker lanes + AddresseeAnchorOne shared feed; speaker-labeled but not lane-separated
AutonomyOne reply per turnMulti-step, work-until-done (drives /goal stack)
Use caseCompanion, 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 existing ChatMode enum (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; → IDLE

Guarantee: 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)

  1. ⛔ Termination ceiling. Pure quiescence never terminates in a busy group → no reply + unbounded token burn. A time | steps | tokens budget forces a checkpoint reply (partial result + "продолжаю…") and a yield. Liveness > purity.
  2. 💸 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 to gpt-5.4-mini, escalate on need. A hard kill-switch per task.
  3. 🍴 Fork-bomb guard. Sub-agents spawning sub-agents → enforce depth + concurrency caps (reuse depth-cap probes). Worker mode does not get unbounded recursion.
  4. 💾 Durable resume. Long tasks must survive an alice restart (we redeploy often). Wire worker working-state into durable execution + auto-resume (8FF-5) + LanePersist (SQLite WAL).
  5. 🧠 Memory attribution preserved. The reply is speaker-agnostic, but content is still attributed to its speaker for dossiers/memory. SharedFeed disables AddresseeAnchor for the reply, not speaker tagging for memory writes.
  6. 👀 Discoverability. /mode reports 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.

FlagCodePrompt
ingest_allinbound gate (host.rs) admits or drops the message— (dropped messages are never seen)
respondreply stage holds vs emits"worker doctrine" block: work then report
speakerslane key per-(chat,sender) vs per-(chat)AddresseeAnchor on (Companion) / off + [speaker, kind]: feed labels (Worker)
autonomyone 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:

  • /goal decomposition (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

NeedExisting primitive
Mid-flight absorption/steer mid-run injection (Phase 8 / G8 #235)
Queue drain post-turnpending_after_current (8YYY)
Settle-window quiescenceWaiting-TTL queue accumulator v2
"All asks answered?"ANSWER_COMPLETENESS scorer (8XXX M4/M5)
Cancelturn-cancel (8KK)
Multi-step turnrun_agentic_loop
Per-chat sticky settingmemory_preload/mode.rs settings infra
Global default configunified ConfigLoader (Phase 9 / 8KKKK)
Cost capsWave-1 worker cap enforcement
Task decomposition / sub-agents / durablePhase 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_date into 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.

  • ModeProfile struct + COMPANION/WORKER presets; /mode command; per-chat sticky setting + config default. Companion preset = byte-identical current behavior (regression-guarded).

P2 — Worker ingest + SharedFeed.

  • ingest_all branch in the inbound gate; chat-level lane re-key; speaker attribution retained for memory; AddresseeAnchor off in SharedFeed.

P3 — Worker autonomy loop.

  • RespondPolicy::OnQuiescence + Autonomy::DrainLoop over agentic_loop; termination ceiling; typing-heartbeat; completeness-gated finalization; cancel.

P4 — `/goal` wiring + safety.

  • work_step delegates 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.