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.

ts
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.all over candidates is fine); a kept send may not overlap any other send or an addMessages on 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.

ts
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).