Self-review (draft, check, keep)
A maker-checker loop over transient sends — draft a reply without keeping it, check the draft, and keep only the accepted one with `addMessages(user, reply)`, memory work included. Covers passing a reply back in `messages` (the check sees what the draft wrote to memory), a generate → check → retry loop against a separate checker agent, 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.
import { z, type UserMessage } from '@aichatgames/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.'),
})
const user: UserMessage = { role: 'user', content: input }
const review: UserMessage = { role: 'user', content: '[System] Check your last reply against everything established so far. Does it contradict anything?' }
// Draft — keeps nothing.
const draft = await gm.send(`draft-${turn}`, { messages: [user], transient: true })
// Check — the agent reads its own draft and judges it. Keeps nothing.
const { data: verdict } = await gm.send(`check-${turn}`, {
messages: [user, draft, review],
responseFormat: Verdict,
transient: true,
})
// Keep the exchange: the player's message and the accepted draft.
if (verdict.consistent) gm.addMessages(user, draft)- A reply can be passed back in
messages. The check posts the draft as it came back — its text, tool calls and memory work — so the check sees the memory the draft opened or wrote, exactly as the draft left it. - Keeping the draft keeps its memory work; dropping it drops that too (see
agent-memory). - 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.
Retrying until it passes
When a failed check should feed a retry, loop: draft, check, and on failure pass the
checker's issues back. Nothing is kept until the loop settles. A separate checker agent keeps
authoring and rule-keeping in different minds; give it the canon it checks against in its
system prompt, or feed it the story with addMessages as it happens.
let issues: string[] = []
for (let attempt = 1; ; attempt++) {
const fix: UserMessage[] = issues.length
? [{ role: 'user', content: `[System] Your last draft had these problems:\n- ${issues.join('\n- ')}` }]
: []
const draft = await gm.send(`draft-${turn}-${attempt}`, { messages: [user, ...fix], transient: true })
const { data: verdict } = await io.agents.continuity.send(`check-${turn}-${attempt}`, {
messages: [{ role: 'user', content: `Check this reply:\n\n${draft.text}` }],
responseFormat: Verdict,
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 }
issues = verdict.issues
}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 check is worth it; the conversation itself belongs in kept history
(see hinting).