# Telling an agent's answer from its thinking

> An ACP agent streams prose, thoughts and tool calls into one turn, and nothing on the wire says which prose is the reply. Cydonia's first rule split each turn at the last tool call or thought. Three weeks later it split turns into runs instead, and later still thoughts folded to one line.

A coding agent's turn is a stream. It thinks, says a sentence, reads a file, runs a command, says another sentence, edits something, and ends with a paragraph addressed to you. A transcript that draws all of it at the same weight buries the paragraph you wanted under everything the agent did to get there. A transcript that hides too much hides the reply. This is the rule Cydonia used to tell the two apart, the case it got wrong, and what it does now.

## What arrives

Cydonia speaks the Agent Client Protocol. During a turn the agent sends `session/update` notifications, and the session folds them into a flat list of items:

| Update | Item |
| --- | --- |
| `AgentMessageChunk` | `Agent(text)`, appended to the last item if that is already `Agent` |
| `AgentThoughtChunk` | `Thinking { text, done }`, appended to the last item if that is an unfinished thought |
| `ToolCall` | a new `Tool` item; any open thought is marked done |
| `ToolCallUpdate` | updates the `Tool` with the same id, found by `rposition` |

So prose is not one item per turn. Every tool call or thought between two stretches of prose ends one `Agent` item and the next chunk starts another. A turn that thinks, explains, calls three tools and sums up is something like:

```
User · Thinking · Agent · Tool · Tool · Tool · Agent
```

Nothing in the protocol marks the last `Agent` as the answer. A model's thinking out loud arrives as the same `AgentMessageChunk` as its reply. Whatever separates them has to be a rule over the order of items, which is why [PR #2](https://github.com/crabtalk/cydonia/pull/2) routed every agent-side event through one channel: a turn's final updates reach the UI before its stop reason, in wire order.

Turns themselves are easy. A turn starts at every `User` item. The chunk before the first question is a turn of its own, since a connection that fails before the first prompt still has something to show.

## The first rule: the prose after the last tool call

PR #2 rebuilt the client on bezel and cacp, and `transcript.rs` was the one file that grew. Its rule, as the PR put it: _turns split into zones by an `rposition` on the last tool call or thought — the prose after it is the answer, everything before is interim work._

```rust
let interim =
    |item: &ChatItem| matches!(item, ChatItem::Tool { .. } | ChatItem::Thinking { .. });
let answer_from = items[start..ix]
    .iter()
    .rposition(interim)
    .map_or(start, |last| start + last + 1);
```

Everything between the question and `answer_from` went under one collapsible header, _Worked · 3 steps_, or _Thought_ when there were no tool calls. Inside it, adjacent tool calls were boxed together, and thoughts and interim prose were drawn small and muted between the boxes. Everything from `answer_from` to the end of the turn was drawn as the reply, in full Markdown.

For the example above that gives:

```
User │ Thinking · Agent · Tool · Tool · Tool │ Agent
     │ work                                  │ answer
```

The header followed the turn: open while the turn was running, closed once it finished, and from the first press on, whatever the person chose. That is bezel's `Takeover`, an `Option<bool>` that answers the automatic value until it is toggled and the user's value after.

## Where it broke

The rule assumes the turn ends on prose. When it does not, `rposition` lands on the last item, the answer is empty, and every sentence the agent wrote is inside the collapsed work header, drawn as muted interim text. A turn whose last item was a tool call showed nothing but a header.

It also put all the work in one place. A turn that alternated, explaining a step and then doing it, had all its explanation folded into the same box as its tool calls.

## Runs instead of a split

[PR #34](https://github.com/crabtalk/cydonia/pull/34), on 2026-09-19, replaced the split with runs. The `Turn` lost its `answer_from`, and the transcript's module doc now reads:

> A zone is a run split: **prose the agent addressed to you is the answer wherever it falls, and tool calls and thoughts are the work.** Each run of work collapses under its own header, in the place it happened, so a turn that ends on a tool call still shows the prose before it as prose.

The body of a turn is cut with `chunk_by` on one predicate:

```rust
fn interim(item: &ChatItem) -> bool {
    matches!(item, ChatItem::Tool { .. } | ChatItem::Thinking { .. })
}

for run in chat.items[body.clone()].chunk_by(|a, b| interim(a) == interim(b)) {
    // prose: drawn in place as Markdown
    // work: a header, and the run under it when open
}
```

The same turn now reads:

```
User │ Thinking │ Agent │ Tool · Tool · Tool │ Agent
     │ work     │ prose │ work               │ prose
```

Every `Agent` item is drawn as prose, at full weight, wherever it falls. Notices and process output sit in the prose runs too, notices at reduced opacity. Each run of work gets its own header, labelled by the tool calls in that run alone, and its open state is keyed by the run's first item rather than the turn's. Only the run the turn is still inside follows the turn: it is open while the agent is working in it and closes when the agent moves on. Earlier runs are closed unless someone opened them.

Inside a run, tool calls are still boxed by adjacency, so a thought between two calls breaks the box where it falls. Interim prose no longer appears inside a run, because prose is never interim now.

## Thoughts on one line

A run that is mostly thinking could still be long once opened. [PR #46](https://github.com/crabtalk/cydonia/pull/46), on 2026-09-24, folded each thought to a single line: its text with all whitespace collapsed, truncated to the width, behind a disclosure. Pressing it shows the whole thought below, indented, with a rule at its left. A thought's open state is per item, so opening one does not open the others.

## What is still true

The distinction is the same one PR #2 drew: tool calls and thoughts are work, and the agent's prose is what it said to you. What changed is where the line falls. It used to be one position per turn, found from the end. Now it is every boundary between work and prose, found in order, and the answer is whatever the agent wrote in the gaps.

The turn split has since moved into the `artifact` crate, `artifact::session::chat::turns`, so turn _n_ on the transcript's rail is turn _n_ for the session tools. It skips runs that are nothing but the agent process's stderr, which are not turns.

The code is in [`crates/gui/src/view/component/transcript.rs`](https://github.com/crabtalk/cydonia/blob/main/crates/gui/src/view/component/transcript.rs) and [`crates/gui/src/model/session.rs`](https://github.com/crabtalk/cydonia/blob/main/crates/gui/src/model/session.rs).
