- Pass 1, stitch: one
Noulquestion (a yes/no question whose answer is the probability that yes is correct) per adjacent pair of lines, asking whether the line break split a sentence across the two. All the pairs go in a single request, and lines that continue a split sentence get merged back into blocks. - Pass 2, classify: one
Choicequestion (pick one option from a list, with a probability for every option) per merged block, choosing among heading, paragraph, list item, quote, code, or callout (a note, tip, or warning set apart from the main text). The blocks only exist once pass 1 has answered, so this is a second request; it also carries companion questions for every block (heading level, step order, callout kind) whose answers are read only when the block’s type makes them relevant. - Direct evidence stays in code. Blank lines and explicit markers (
-,1.,#) are read in code, never sent to the model to reconsider; this memo kept its blank lines but lost every marker. The model gets only the questions code cannot answer from the text.
classify_questions. The rest of the code is plumbing around them. The cost and latency
numbers are in the appendix: two round trips, 10,211 tokens, 0.8s, $0.0015 for this
memo.
Setup
TYPESAFE_API_KEY. Every API call is cached in json_cache.json, which ships
with the cookbook, so re-rendering replays the published numbers without calling the API.
Delete that file to re-run everything live.
The document: a team memo that lost its formatting
The test document is a memo about a build-system migration, in the state it arrives in a plain-text inbox: paragraphs hard-wrapped mid-sentence, a shell command sitting on a bare line, two lists with no bullets or numbers, a warning with nothing marking it as one. The text is fetched from a pinned gist so the cookbook’s numbers stay reproducible.L014| ); the ids are ordinary text the model reads as part of
the state, and questions and answers refer to lines by these ids (the same scheme as the
semantic search cookbook).
Pass 1: stitching split sentences
One Noul question per adjacent pair of lines, all in one request; pairs separated by a blank line are skipped. The question is deliberately narrow (“does this line pick up mid-sentence?”), which is close to an objective fact about the text. The appendix covers both the wording choice and how the merge thresholds were derived.. ! ? : ;), the cutoff rises to 0.5. The
appendix walks through the probabilities behind the two numbers.
Pass 2: classifying blocks
Each stitched block gets aChoice question: what kind of content is this? These
three
dicts, plus the step question’s true/false criteria inside classify_questions below,
are the entire specification of the classifier. There is no other logic. To adapt the
pipeline to your own documents, edit these descriptions.
heading, the renderer needs a heading level; if list_item,
whether order matters; if callout, which kind. The types are not known yet, and waiting
for them would mean a third round trip, so the companion questions are asked up front in
the same request. Most of these answers are never read: the step probability of a paragraph
means nothing and is simply ignored. An extra question adds little, since the state is
most of the tokens and is sent once either way, while an extra round trip adds a full
request of latency.
warning. The appendix looks at the one block the model was unsure
about.
Rendering
Code assembles the page from the judgments. Consecutive list items become one list, numbered when the mean of the items’ step probabilities is at least 0.5. That threshold is a group-level decision no single question asked directly.Open it in the playground
This share link holds the stitched blocks and the full pass-2 question set. Open it to re-run the classification live.Appendix
Cost and latency
Where the join thresholds come from
The per-line join probabilities from pass 1:- After a dangling line (one with no sentence-ending punctuation), anything at 0.2 or
above counts as a continuation. True continuations score as low as 0.39 here (
L004| make the switch for real.), so a single cautious cutoff at 0.5 would break up healthy paragraphs. - After terminal punctuation (a character that ends a sentence or clause:
.!?:;), the cutoff rises to 0.5. The memo’s team list shows why:L015| The platform teamfollows a colon and scores 0.22. That is a low but nonzero “this continues the sentence” signal, and it would clear the 0.2 cutoff and merge the list into the sentence introducing it. No single threshold works for both cases; once code checks the punctuation first, the two bands separate.

