# Distill — sessions into reusable procedures

A session records work that succeeded. `read_conversation` exposes that record to Programs and user-authored skills. OpenProgram no longer ships a default distillation skill.

The retained product capability is the transcript renderer. A product-specific reusable workflow should be implemented as a Program; users may independently author a skill if they want model instructions rather than product behavior.

## Components

| Piece | Where | What it does |
|---|---|---|
| Transcript renderer | `openprogram/store/session/transcript.py` | `render_read_conversation()` — one branch of a session as LLM-readable plain text |
| Model-facing tool | `openprogram/programs/tools/knowledge/read_conversation/` | `read_conversation` — the renderer as a `@function`, defaulting to the current session |
| Product page | `docs/capabilities/distill.md` | User-facing documentation |

## The transcript renderer

`get_branch` returns the conversational chain — user and assistant nodes linked by `predecessor`. Tool and function-call nodes are not on that chain; they hang off the assistant turn that issued them via `caller` ([DAG overview](../dag/overview.md)). The renderer joins the two: walk the branch, and under each turn print the calls whose `caller` points at it. This is the same edge `graph_builder` follows for the web DAG, read for prose instead of coordinates.

Signature:

```python
render_read_conversation(
    session_id,
    head_id=None,               # default: the session's active head
    start_turn=0,               # 1-based, inclusive; 0 = first, negative = from the end
    end_turn=0,                 # 1-based, inclusive; 0 = last, -1 = last turn
    include_function_calls=True,
    max_chars=60_000,
    store=None,                 # default: agent.session_db.default_db()
) -> str
```

`start_turn`/`end_turn` select a closed range of turns by the same 1-based `[N]` numbers the transcript prints, Python-slice style with negatives counting from the end (`start_turn=-10` is the last 10 turns). The slice keeps global turn numbers, so pages read in sequence stay aligned; a sub-range is announced in the header as `turns 37-52 of 120`, and an empty range returns a one-line notice instead of an empty transcript.

The store comes from `default_db()` rather than a hardcoded `~/.openprogram` path, so a project-bound session resolves through the same locator as everything else. The `store` parameter exists for tests.

Each call prints its name, a success or `FAILED` marker, clipped arguments, and a clipped result. Failures are kept rather than filtered: a failed call and its correction is what a trap looks like in the record, and it is the highest-value material in a distillation.

Two nodes get explicit labels rather than passing as ordinary turns, because in both cases the reader would otherwise draw a wrong conclusion from what is present:

- **Compaction summaries** (`context/summary`) stand in for a collapsed range. Labeled so the reader knows detail was dropped there, not that the session was thin.
- **Spawn branch roots** open a sub-branch. Labeled so a nested agent's work is not read as the main thread.

Truncation is layered. Per-field caps stop one runaway tool result (a large file read) from evicting the reasoning around it; the total budget cuts at the last whole turn that fits and names the first dropped turn (`re-read with start_turn=N to continue`), so a truncated transcript never looks like a complete one and the reader can page on from where it stopped.

`scripts/dag_dump.py` remains the debugging view — node ids, lanes, tiers. The two do not overlap.

## Discovery reuses existing tools

`list_agents` (`programs/tools/send_message/`) already enumerates session ids and `SID:HEAD` branch tips. Those are exactly the arguments `read_conversation` takes, so no listing tool is added. The tool accepts a whole `SID:HEAD` string in `session_id` and splits it, since that is the form `list_agents` prints.

## The skill carries the judgment

Distillation is a judgment task: deciding what in a transcript generalizes and what was incidental to that day. Nothing about it is deterministic, so there is no `distill()` function — the skill body tells the agent what to extract (goal, preconditions, steps, decision points, traps) and it writes the output file with its own `Write` tool.

This follows the precedent set when the `create()` / `edit()` / `improve()` wrappers were removed from agentic programming: a function whose entire body is one LLM call plus a file write is a layer the agent does not need.

The skill also decides the output form. A procedure requiring runtime judgment becomes a `SKILL.md`; a mechanical one becomes an `Agent` method written per the [agentic-programming](../../function/calling-unification.md) conventions.

## Output lands in the existing skill pipeline

A user-authored skill is written to `~/.openprogram/skills/<name>/` or `<cwd>/skills/<name>/` — two of the four sources the loader merges. It is live without a restart (the watcher hot-reloads) and automatically available as `/<name>` (`commands/_skill_adapter.py` projects every discovered skill into the slash-command registry).

That is the reason the feature needs no subsystem of its own: the storage, the discovery, the reload, and the invocation path all exist. Distillation only had to produce a file in the right place.

## Revision closes the loop

Distillation covers refinement, not only first creation. The skill body directs the agent, before writing, to look for an existing skill on the same topic — same goal and preconditions, not merely a similar name — and to revise a match in place: keep what held, replace what the new session disproved, merge new decision points and traps into the flow. A distilled `Agent` method follows the same rule and is edited rather than duplicated, keeping its name stable for callers.

This is the refine step of the record → replay → refine loop. A skill that fails in use is corrected through the same path — the failing run is itself a session to distill — and the complaint form ("that skill didn't work, update it with what we learned") is among the triggers in the skill's description, so it routes here without a separate mechanism.

Revision adds no machinery. Finding the existing skill uses the loader's lookup locations and `openprogram skills list`; editing uses the agent's own tools; history lives in git, and the skill body forbids changelogs in the prose. Like distillation itself, revision is judgment carried by the skill, not code.

## Naming

The product word is **distill**, not "workflow". In this repository "workflow" already denotes an agentic program — a complete pre-built agent harness, documented under `docs/capabilities/workflows/`. A distilled procedure is a smaller thing: knowledge extracted from one session, not a shipped program. Reusing the word would have merged two concepts that appear side by side in the same documentation tab.

## Implementation status

Implemented as described.
