Self-review (draft, check, keep)
A maker-checker loop over transient sends — draft a reply without keeping it, check it with a separate structured checker agent, and keep only the accepted one with `addMessages(user, reply)`, topic opens included. Covers why the verdict needs its own agent (one agent, one format), a generate → check → revise loop that passes the draft back in `messages` (the revision sees the topics the draft opened), and what re-posting a draft costs.
A reply that has to keep rules — continuity, secrets, what a character can know — is easier to get right in two passes: draft it, then check it. Both passes are transient; only the accepted draft is kept, so a rejected one never enters the agent's history.
The check is a separate checker agent with a fixed structured responseFormat: the
maker stays a prose agent, and each agent's config — its schema included — stays the same on
every send, so both keep their prompt caches (see
structured-output). It also keeps authoring and rule-keeping in
different minds. Give the checker the canon it checks against in its system prompt, or feed
it the story with addMessages as it happens.
import { z, type AgentMessage, type UserMessage } from '@omorashi/sdk'
const Verdict = z.object({
consistent: z.boolean().describe('True only when nothing is violated.'),
issues: z.array(z.string()).describe('Each specific inconsistency, naming what it contradicts. Empty when consistent.'),
})
// Created once, at the start: io.createAgent('continuity', { systemPrompt: CONTINUITY_PROMPT, responseFormat: Verdict })
const continuity = io.getAgent('continuity', Verdict)
const user: UserMessage = { role: 'user', content: input }
// Draft — keeps nothing.
const draft = await gm.send(`draft-${turn}`, { messages: [user], transient: true })
// Check — the checker judges the draft's text. Keeps nothing.
const { data: verdict } = await continuity.send(`check-${turn}`, {
messages: [{ role: 'user', content: `Check this reply:\n\n${draft.text}` }],
transient: true,
})
// Keep the exchange: the player's message and the accepted draft.
if (verdict.consistent) gm.addMessages(user, draft)- Keeping the draft keeps its topic opens; dropping it drops them too (see
agent-topics). - Transient sends on one agent may run concurrently (
Promise.allover candidates is fine); a kept send may not overlap any other send or anaddMessageson the same agent. - Don't give the maker a structured format just for the check: an agent has one format, and
changing it (an
io.updateAgentwith a different schema) re-reads its whole history at full price. - A rule code can check alone (a mark's syntax, a name from a list) needs no checker: put it
in the agent's schema as a
.refine(), and a reply that fails it is retried with a correction automatically (seestructured-output).
Retrying until it passes
When a failed check should feed a retry, loop: draft, check, and on failure hand the maker its own draft back with the checker's issues. Nothing is kept until the loop settles.
let revise: AgentMessage[] = []
for (let attempt = 1; ; attempt++) {
const draft = await gm.send(`draft-${turn}-${attempt}`, { messages: [user, ...revise], transient: true })
const { data: verdict } = await continuity.send(`check-${turn}-${attempt}`, {
messages: [{ role: 'user', content: `Check this reply:\n\n${draft.text}` }],
transient: true,
})
// Keep the accepted draft — or, out of attempts, the last one (one possible fallback).
if (verdict.consistent || attempt === 3) { gm.addMessages(user, draft); break }
revise = [draft, { role: 'user', content: `[System] Your last reply had these problems:\n- ${verdict.issues.join('\n- ')}\nWrite it again.` }]
}- A reply can be passed back in
messages. The revision posts the draft as it came back — its text, tool calls and topic opens — so the maker sees the topics the draft opened, exactly as the draft left it.
Suffix each callId with the attempt so every send replays stably.
Cost
Messages passed in messages can cost up to 10× the same content in kept history. A
draft re-posted for one revision is worth it; the conversation itself belongs in kept history
(see hinting).