# Interactions — plan approval & Q&A

The `present_plan` and `ask_user` tools let the agent pause for explicit
user input before continuing. Both are wired in the TUI by default; SDK
consumers opt in via `createInteractionTools()`.

## How it works

The data model is "the call IS the persisted state": both tools land in
`session.turns` as regular `tool_call` blocks. When the user responds,
the matching `tool_result` block follows in the next user turn. There is
no parallel store, no migration, no extra schema. Resume-from-disk is
just `pendingInteractionsFromTurns(session.turns)`.

```
agent calls present_plan / ask_user
        │
        ▼
tool_call block lands in session.turns
        │
        ▼ (live: tool's Promise awaits;
        │  resumed: queue rebuilt from session.turns on reopen)
        │
user responds via the InteractionBlock UI
        │
        ▼
tool_result block written; agent loop continues
```

## `present_plan`

```json
{
  "title": "Refactor the agent loop",
  "plan": "Markdown body of the plan...",
  "steps": [
    { "id": "s1", "title": "Extract X", "description": "..." },
    { "id": "s2", "title": "Move Y" }
  ]
}
```

User decides:
- `approve` — agent continues, no comment.
- `revise` / `reject` — optional free-text comment, agent sees both.

## `ask_user`

```json
{
  "intro": "Optional Markdown context above the questions.",
  "questions": [
    { "id": "name", "prompt": "Your name?", "type": "text", "required": true },
    { "id": "notes", "prompt": "Notes?", "type": "textarea", "default": "..." },
    {
      "id": "fw", "prompt": "Pick", "type": "select",
      "choices": [{ "id": "react", "label": "React" }, ...]
    },
    {
      "id": "go", "prompt": "Proceed?", "type": "confirm",
      "affirmLabel": "do it", "denyLabel": "wait"
    }
  ]
}
```

Four question `type`s: `text`, `textarea`, `select`, `confirm`. Batch
related questions into one call — every call pauses the conversation.

Response shape:

```json
{ "answers": { "name": "Jane", "notes": "...", "fw": "react", "go": true } }
```

Missing keys = question was skipped (only possible when `required:
false`). Empty-string answers are dropped at serialization.

## Keyboard

In the question wizard (multi-question batches):

- `↑/↓` (or `j/k`) — navigate options
- `↵` — submit the active question / pick the focused option
- `shift+↹` — back one question (preserves your answers, pre-fills the
  input with the prior value)
- `esc` — leave the form
  - Live (agent waiting): aborts the run
  - Resumed (you reopened the session): leaves the form, the session
    re-enqueues the same interaction on next activation

In the plan picker:

- Decision picker → `revise` / `reject` → comment textarea
- `shift+↹` from the comment stage → back to the decision picker
- `esc` — abort the run

## Persistence guarantees

- **Live tool call**: the tool's `execute()` awaits a Promise resolved
  by the UI. Standard agent-loop path; the `tool_result` is persisted
  by the loop's own `tool-results:after` hook.
- **Hard-killed mid-prompt**: the assistant turn with the unresolved
  `tool_call` is on disk. On reload, `pendingInteractionsFromTurns`
  surfaces it and the wizard re-prompts. When the user resolves,
  `buildResumedToolResultsTurn` writes ONE batched user turn with one
  `tool_result` per pending call, in pending order — load-bearing for
  Anthropic's history validator.
- **Graceful abort (esc on live)**: the tool's Promise rejects, the
  `tool:error` hook produces a `Tool error: Interaction cancelled`
  tool_result, the session persists a complete history. Does NOT
  re-enqueue on next open.
- **"Leave for later" (esc on resumed)**: the queue drains, the session
  keeps the unresolved `tool_call` intact, the next activation
  re-enqueues. Partial within-entry answers are NOT preserved across
  navigation — known limitation, documented.

## Smoke-test checklist

Run these manually before cutting a release that touches interaction
behavior. Automated coverage is in `test/chat/interactions.test.ts`.

1. **Single plan approval** — model emits `present_plan`, approve →
   agent continues; revise with comment → agent sees comment; reject
   → run unwinds. Esc on the picker → run aborts.
2. **Single ask_user, one question** — text / textarea / select /
   confirm each render correctly. `↵` submits, the answer reaches the
   model.
3. **Multi-question batch** — 3+ questions of mixed types. `shift+↹`
   goes back, prior answer is pre-filled. Final `↵` flushes one
   `answers: {...}` payload.
4. **Long batch + scroll** — 6+ questions. Form caps at ~60% terminal
   height. Active question auto-scrolls into view on advance.
5. **Hard-kill mid-batch** — open a long batch, answer Q1, `kill -9`
   the process. Reopen the session. Wizard restarts on the current
   request (not Q1 of the whole batch — only the current request's
   activeIndex resets). Answer through; the session resumes.
6. **Parallel interaction calls** — if a model emits both
   `present_plan` and `ask_user` in one turn (rare), reopen mid-batch
   and resolve both. Verify the resumed user turn contains TWO
   `tool_result` blocks in the same order as the assistant turn's
   `tool_call`s. (Automated in `test/chat/interactions.test.ts`'s
   `resumed-flow end-to-end` suite — manual check confirms the visual.)
7. **Subagent emitting an interaction** — a spawned child agent calls
   `ask_user` or `present_plan`. The parent's `InteractionBlock`
   shows; the user responds; the child receives the answer and its
   loop continues. _Automated_ — see `test/chat/interactions.test.ts`'s
   `subagent-emitted interactions` suite, which exercises the full
   `createSpawnTool({ persist: true })` pipeline against a real
   `createMemoryStore` and verifies: (a) the queue receives requests
   tagged with the CHILD's runId, (b) the child's `tool_result` is
   persisted under the child's runId, (c) the child run is linked to
   its parent via `parentRunId`, (d) queue rejections surface as a
   child-side `Tool error` rather than crashing the parent. Manual
   check is still useful for visual confirmation (the `InteractionBlock`
   doesn't distinguish parent vs subagent origin in the title).
8. **Live + nav-away** — start a live interaction (busy=true, pending),
   press esc. Run aborts, `Tool error` written, footer returns to
   normal hints.
9. **Resumed + provider switch** — reopen a session with a pending
   interaction. Open `ctrl+m` model picker, switch providers. The
   teardown drains the queue; `activateSession` re-enqueues from the
   reloaded session. User answers, the new provider receives the
   response.
10. **Model picker / settings / session details mid-resumed** —
    confirm all overlays open cleanly. They steal focus; the
    `OptionList` / textarea inside the InteractionBlock goes inert via
    `useModalAwareFocus`. Closing the overlay restores input.

## Public API (for GUI consumers)

Re-exported from `zidane/chat`:

- `createInteractionTools({ requestInteraction })` — builds the two
  `ToolDef`s. `requestInteraction` resolves with the user's response.
- `pendingInteractionsFromTurns(turns)` — derive pending list from a
  session.
- `buildResumedToolResultsTurn(requests, responses, opts)` — produce a
  protocol-valid user turn closing a resumed batch.
- `serializeInteractionResponse(response)` — string the model sees.
- `InteractionsProvider` + `useInteractionsQueue` + `useInteractionsActions`
  + `makeRequestInteraction(actions)` — React queue + tool binding.
  GUIs that don't use React can ignore these and wire their own
  resolver to `requestInteraction` directly.
- Types: `InteractionRequest`, `InteractionResponse`, `PlanPayload`,
  `QuestionPayload`, `Question`, `TextQuestion`, `SelectQuestion`,
  `ConfirmQuestion`, `AnswerValue`, ...

A renderer-agnostic GUI just needs to:

1. Mount `<InteractionsProvider>` (or its own queue).
2. Inject the tools at agent build time:
   `createInteractionTools({ requestInteraction: makeRequestInteraction(actions) })`.
3. Render the head of the queue with its own UI.
4. On resume, call `pendingInteractionsFromTurns` and enqueue resumed
   entries whose resolver writes a `buildResumedToolResultsTurn` to
   the session.
