# PLAN — Agent task routing accuracy and empty post-tool replies

## Purpose & context

Operators have observed Telegram flows where:

1. **False-positive agent tasks** — The micro-brain sets `route_agent_task: true` for messages that describe *future* or *hedged* work the *human* might do (for example: automating an app feature, plugin ideas, “I’ll see about…”), not a request for Furoshiki to run a long-horizon background job *now*. That short-circuits the listener before `generate_reply`, creates an `agent_tasks` row, and sends the “queued as a background task” reply — wrong UX and wrong workload.

2. **Empty user-visible text after tools** — The listener shows: “my tools ran but I didn’t get text to send.” That path means at least one tool ran (`skill_results` non-empty) but the final assistant `content` was empty after stripping. Users get a fallback string instead of a substantive reply; follow-up turns can feel incoherent (topic drift, ignored short requests).

This plan addresses **routing accuracy** (prompt + deterministic guards) and **reply completeness** (one recovery LLM call before falling back to the canned message).

**Related design:** Agent task creation from Telegram is specified in `docs/PLAN-agent-tasks-and-tool-runtime.md` (section 3.7–3.8). Implementation lives in `scripts/micro_brain.py` (`route_agent_task`, `apply_fallbacks`) and `scripts/telegram_listener.py` (early `agent_tasks.create` + `generate_reply` tool loop).

---

## Problem summary (architecture)

```text
micro_brain (MODEL_MICRO JSON) → apply_fallbacks → telegram_listener
                                                      │
                    route_agent_task true ────────────┼──► agent_tasks.create → early return
                                                      │
                    else ─────────────────────────────┼──► generate_reply → call_llm_chat_with_tool_loop
                                                      │
                    empty text + skill_results ───────┼──► _TELEGRAM_EMPTY_REPLY_FALLBACK_AFTER_TOOLS
```

**False agent tasks:** `route_agent_task` is taken from parsed JSON with default `false`, but when the LLM returns `true` there is **no** second-stage veto comparable to `route_state_introspection`, which is cleared unless regex/vocab confirms (see `apply_fallbacks` in `scripts/micro_brain.py`).

**Empty reply after tools:** Not primarily a plugin-manifest issue. `scripts/model_tools.py` `TOOL_USAGE_INSTRUCTION` already constrains plugin callables to explicit user requests. The failure is either (a) final message with empty `content` after tool rounds, or (b) content stripped to empty by `strip_leaked_tool_markup_from_reply`, or (c) tool loop exhaustion + failed plain completion (`scripts/llm.py` `call_llm_chat_with_tool_loop`).

---

## Goals

- **G1.** Do not create listener-originated agent tasks for hedged planning, thanks + idea, “maybe I’ll build”, or other non-delegation phrasing unless the user clearly asks Furoshiki to run sustained autonomous work *in this chat turn*.

- **G2.** When tools ran but the main model returned no visible text, attempt **one** no-tools recovery completion that summarizes tool results and answers the user before using the Telegram empty-reply fallback.

- **G3.** Keep behavior explainable: log when heuristics override the LLM on `route_agent_task`; use a distinct `source=` on recovery calls for `llm_call_log` filtering.

---

## Non-goals (this plan)

- Changing the “use inner-state tools actively” guidance in `TOOL_USAGE_INSTRUCTION` (possible follow-up if recovery metrics still show empty replies).

- New dashboard knobs unless heuristics need operator tuning without deploy (defer).

---

## Phase 1 — Micro-brain: prompt + deterministic suppression

### 1.1 Prompt updates (`scripts/micro_brain.py`, `_INBOUND_SYSTEM`)

Extend the `route_agent_task` bullet with:

- **Explicit FALSE examples:** hedged future work (“I’ll see about”, “maybe I can”, “I could build”, “plugin idea”, “thinking about automating”), brainstorming mixed with thanks, anything that describes the *user’s* future work without asking Furoshiki to execute a multi-step background job *now*.

- **Explicit TRUE examples:** Align with `PLAN-agent-tasks-and-tool-runtime.md` — e.g. research and report back, multi-step inbox/code work, explicit request to run work in the background / autonomously *through Furoshiki*.

- Reinforce: **default false**; true only when the user is delegating **to Furoshiki** a sustained task, not when they narrate their own plans.

### 1.2 Heuristic veto (after `rat = _parse_bool(...)`, before `return TurnSignals`)

Add a helper, e.g. `_agent_task_suppressed_by_heuristic(text: str) -> bool`, using regex lists in the same style as `_THINK_PHRASES_RE` / `_INTROSPECTION_VOCAB_RE`:

- If the helper matches **suppression** patterns, set `rat = False`.
- `log.info` a stable, grep-friendly line when suppression fires.

Optional refinement (only if tests show lingering false positives): require a **positive** signal for `rat` to remain true when the LLM said true (e.g. imperative delegation or explicit background/autonomous wording). Start with negative-only suppression unless needed.

### 1.3 Tests

Add tests (new file e.g. `tests/test_micro_brain_agent_task_route.py` or extend `tests/test_anti_repeat_comprehensive.py`):

- Synthetic `parsed` with `route_agent_task: true` + user text with hedge phrases → `route_agent_task` is **false** after `apply_fallbacks`.
- Control: explicit long-horizon delegation phrasing → remains **true** (if positive gate exists, align cases).

### 1.4 Documentation

Update the micro-brain / Telegram routing section in `docs/SELF-AWARENESS-DESIGN.md` to describe `route_agent_task` and the new heuristic veto. After substantive doc edits, operators run `furoshiki design update` per repo rules.

---

## Phase 2 — Listener: empty-reply recovery after tool loop

### 2.1 Where (`scripts/telegram_listener.py`, `generate_reply`)

After `call_llm_chat_with_tool_loop` returns and optional `skill_results` processing, **before** deciding on the empty-reply fallback:

- If `(reply or '').strip()` is empty **and** `skill_results` is non-empty:

  - Issue **one** `call_llm_chat` (no tools) with messages that include the user turn and a short system or user instruction: produce visible prose that summarizes what tools did and responds to the user (1–3 short paragraphs as appropriate).

  - Use a distinct `source` string (e.g. `telegram_listener_tool_recovery`) for observability.

- If recovery still yields empty text, keep existing behavior: `_TELEGRAM_EMPTY_REPLY_FALLBACK_AFTER_TOOLS`, existing `log.warning`, and optional reaction behavior unchanged.

### 2.2 Optional diagnostics (`scripts/llm.py`)

If useful for ops: when `_assistant_message_text` is empty on a no-tool completion after tool rounds, log `finish_reason` at warning level once (no behavior change).

---

## Verification

- **Unit:** New/updated micro-brain tests pass; full `pytest` for touched modules.
- **Manual / trace:** A hedge-heavy user message should **not** trigger `agent_tasks.create` from the listener; logs should show heuristic suppression when applicable. After implementation, `furoshiki trace --simulate` can be used where the harness exercises `generate_reply` (per `CLAUDE.md` prompt pipeline audit).

---

## File touch list (implementation checklist)

| Area | Files |
|------|--------|
| Routing prompt + heuristic | `scripts/micro_brain.py` |
| Recovery completion | `scripts/telegram_listener.py` |
| Optional finish_reason log | `scripts/llm.py` |
| Tests | `tests/test_micro_brain_agent_task_route.py` (or extend existing) |
| Design doc | `docs/SELF-AWARENESS-DESIGN.md` |
| Chroma / snapshot | Run `furoshiki design update` after design doc edits |

---

## References (code)

- Empty-reply fallbacks: `scripts/telegram_listener.py` — `_TELEGRAM_EMPTY_REPLY_FALLBACK_AFTER_TOOLS`, `generate_reply` empty check after tool pipeline.
- Agent task short-circuit: `scripts/telegram_listener.py` — `route_agent_task` block before NL tasks / `generate_reply`.
- Tool loop: `scripts/llm.py` — `call_llm_chat_with_tool_loop`.
- Tool usage vs plugins: `scripts/model_tools.py` — `TOOL_USAGE_INSTRUCTION`.
