Skip to content

21. Agents inside documents

Design only

This chapter describes functionality that is not in the current release. It is written as if it existed: these programs do not run against the current binary, and names, types and outputs may change before it ships.

Chapter 20 ran an agent from a program. This chapter runs one from inside the document that asks the question. The knowledge, the prompt, the program that assembles the prompt and the answer then sit in one file, each visible, each editable, each a value a later cell can read. The document is examples/notebook/next-meeting.hmd, rendered against the tutorial vault like the reading list of chapter 19.

The document is the harness

Two bindings reach a cell from the host. cell is the cell being rendered, whose logs channel the logging page wrote to. this is the card the cell is written in, as it stands when the render starts: its header, its prose, and every output a previous render saved into it.

The prose of a cell's section, from the nearest heading above the cell down to the cell itself, is the default instructions of every Prompt the cell builds. The question is written once, as prose a reader reads, and the cell below it carries only the program that gathers the evidence.

hql render --emit after --in-place writes each cell's output back into the document, between the markers of chapter 19. The opening marker's hash covers the cell's code and the code of every cell it reads; a cell that reads this also covers the document's text, so its output is stale after every edit of the prose around it. That is the loop this chapter is about: edit the draft, render, read the answer, edit again.

The document

---
title: Next meeting
---
# Next meeting

Notes for the club's next meeting, built from the shelf with
`hql --vault examples/tutorial render`.

```hql id=books hidden
cards | filter(c => c.metadata.category == "book")
```

## Candidates

The books nobody has finished.

```hql id=unread
books
| filter(c => c.metadata.status == "todo")
| sort(by = c => c.title)
| map(c => c.title)
```

## Evidence

What the shelf holds on the question the club keeps returning to.

```hql id=evidence hidden
import semantic
cards | semantic("which unread book suits a reading club") | take(3) | Context("evidence")
```

## What should we read next?

Pick one unread book for the next meeting, and say why. The candidates and
the evidence are in this draft; rely on them and on the shelf.

```hql#eval id=pick
import ai

fn books_by(author: String) : List<String> =
    cards | filter(c => c.metadata.author == author) | map(c => c.title) | List

[this]
| Context("draft")
| concat(evidence)
| run(@[[librarian]] | Agent, tools = [Tool(books_by, "The titles on the shelf by one author")])
```

## What happened

```hql#eval id=trace
pick | trace | map(e => e.kind)
```

## Remembered

```hql id=note hidden
pick | remember("next-meeting-pick")
```

```hql#eval
cards | filter(c => c.path | starts_with("memory/")) | map(c => c.title)
```

The first two cells run against the current binary; they are the reading list of chapter 19 again. The rest is the subject of this chapter.

Evidence by meaning

The evidence cell is a ranking of chapter 18 turned into a context. Each hit becomes one item, and the item keeps what the hit knew: its score, and the retrieval that produced it, in its provenance. Evidence that reaches a model says how it was found:

evidence | map(i => {source: i.provenance.name, score: i.score})
[{score: 0.3127, source: "the-common-reader"}, {score: 0.2904, source: "club"}, {score: 0.2851, source: "leaves-of-grass"}]

The question is a literal because the vault's index answers the questions it was built for, as chapter 18 explains; a question the index lacks is a diagnostic naming the build command, not an empty context.

The agent reads the draft

[this] | Context("draft") is a context of one item: the document as it stands, written as Markdown, with the card's name and path as its provenance. concat(evidence) appends the ranking. The run step builds the prompt from the section's prose and both contexts, and the rendered prompt opens with the question the reader just read:

Pick one unread book for the next meeting, and say why. The candidates and
the evidence are in this draft; rely on them and on the shelf.

## draft

Source: next-meeting

# Next meeting
…

## evidence

Source: the-common-reader
…

The run's result is written after the cell, and the next cell reads the run itself, not its text:

```hql#eval id=pick
…
```
<!-- hql:output id=pick hash=3b91c0e4d2a7 status=ok -->
Start with The Common Reader. It is the only unread book by Virginia Woolf,
the club has read A Room of One's Own and is reading Mrs Dalloway, and the
draft lists it among the candidates.
<!-- /hql:output -->
["prompt", "request", "response", "tool_call", "request", "response", "stop"]

Change the prose above the cell, "say why" to "say why, in one sentence", and the cell's hash no longer matches its saved output. An editor shows the answer as stale without running anything; the next render asks again, with the new instructions and the draft as it now stands, which includes the previous answer. The agent reads what it wrote last time, as a value among the others.

Memory is cards

remember(name) writes a run's result as a card in the vault's [ai.memory] directory, here memory/next-meeting-pick.hmd, with the run's id in its header so the card names the run that wrote it. Memory is cards: writing it is one explicit step, and reading it is a query like any other, which is what the last cell does. On the next render, cards holds the memory card and this holds the saved pick, so the document reads what it wrote.

In replay mode, the default, remember succeeds only when the card already holds the same content, so a committed vault renders without writing. Under --ai record or --ai live it writes.

The editor loop

A request a run makes can be answered by three answerers, and every answer is a recording under the hash of the request, so a document rendered later reads all three alike. A provider answers over HTTP, as chapter 20 configured. A headless session of an agent host answers when the model entry names one. And under --ai host, a request with no recording is not sent anywhere: it waits, and whoever is driving the program answers it. In an editor that is the agent the user is already chatting with.

$ hql --vault examples/tutorial --ai host render --emit after --in-place examples/notebook/next-meeting.hmd
$ hql --vault examples/tutorial ai pending
5e10ae23…  messages  next-meeting.hmd:31  pick
$ hql --vault examples/tutorial ai answer 5e10ae23… --text "Start with The Common Reader. …"
$ hql --vault examples/tutorial render examples/notebook/next-meeting.hmd

hql ai pending lists each waiting request with its key, the wire format its answer is written in, and the request itself: the agent's instructions, the conversation, and the tools offered with their schemas. hql ai answer writes the answer as a recording marked answered_by: host, so a reader of the document can tell a provider's answer from one a person's chat gave. The second render replays it. Committed recordings make the document a procedure the chat executed once and anyone can read again.

edit the draft ──► render ──► request waits ──► chat answers ──► recording
      ▲                                                             │
      └───────────────── read the saved output ◄────────────────────┘

What is next

Every card in this chapter was in one vault. Chapter 22 asks what the same steps are over a web of them.