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.
| 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/functions/vanilla/read_conversation/ |
read_conversation — the renderer as a @function, defaulting to the current session |
| Product page | docs/capabilities/distill.md |
User-facing documentation |
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). 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:
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()
) -> strstart_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.
list_agents (programs/functions/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.
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 @agentic_function written per the agentic-programming conventions.
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.
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 @agentic_function 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.
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.
Implemented as described.