---
title: Patterns
description: "How to structure your agent loop with beliefs: single-turn, multi-turn, streaming, tool-aware, multi-agent, and the smaller patterns that compose with them."
---

Every agent using beliefs follows the same `before → act → after` cycle. The difference is how you arrange that cycle for your use case.

<Callout type="info" title="Scope choice matters">
These patterns assume you already chose an appropriate scope. For copy-paste examples, `writeScope: 'space'` is the simplest starting point. For chat apps, bind `writeScope: 'thread'` with `thread` or `beliefs.withThread(threadId)`. See [Scoping](/dev/sdk/scoping).
</Callout>

<Callout type="info" title="`callLLM` is a stand-in">
Snippets below use `callLLM(systemPrompt, userMessage)` as a stand-in for your model. Replace it with whichever framework you ship on (Vercel AI, Anthropic SDK, OpenAI, plain fetch; see the [Hack Guide](/dev/tutorial/hack-guide) for working recipes). The point of these examples is the belief flow.
</Callout>

## Choosing a loop pattern

```
┌─ Is this a single request/response? ──→ Single-turn
│
├─ Does the agent use tools? ──→ Tool-aware
│
├─ Does the agent stream output? ──→ Streaming
│
├─ Should the agent loop until confident? ──→ Multi-turn
│
└─ Do multiple agents collaborate? ──→ Multi-agent
```

Most production agents combine patterns: a multi-turn loop with streaming and tool use. Start with the simplest pattern that fits, then layer in complexity.

---

## Single-Turn

The simplest integration. One `before`, one agent call, one `after`.

```ts
async function answer(question: string) {
  const context = await beliefs.before(question)
  const result = await callLLM(context.prompt, question)
  const delta = await beliefs.after(result)

  return result
}
```

**When to use:** Chatbots, Q&A, any request/response flow where you want to accumulate knowledge across interactions but don't need to loop within a single request.

**What you get:** Beliefs accumulate across calls within the same scope (the same `thread` if you're thread-scoped, or the same `namespace` if you're space-scoped). The second time the user asks about a topic, `before()` returns richer context with existing beliefs, gaps, and moves.

---

## Multi-Turn (Clarity-Driven)

Loop until the agent has enough confidence to act. Use `clarity` as the stopping condition.

```ts
async function research(question: string) {
  await beliefs.add(question, { type: 'goal' })

  for (let turn = 0; turn < 10; turn++) {
    const context = await beliefs.before(question)

    // Stop when clarity is high enough
    if (context.clarity > 0.7) {
      return {
        beliefs: context.beliefs,
        clarity: context.clarity,
        gaps: context.gaps,
      }
    }

    // Follow the highest-value move
    const focus = context.moves[0]?.target ?? question
    const result = await callLLM(context.prompt, focus)
    const delta = await beliefs.after(result)

    console.log(
      `Turn ${turn + 1}: clarity ${delta.clarity.toFixed(2)}, ` +
      `${delta.changes.length} changes`
    )
  }

  // Hit turn limit: return what we have
  return await beliefs.read()
}
```

**When to use:** Research agents, fact-checkers, decision support: any task where the agent should investigate until it has enough information.

**Key decisions:**
- **Clarity threshold:** `0.7` is a good starting point. Lower for exploratory tasks, higher for critical decisions.
- **Turn limit:** always set a hard cap to prevent infinite loops.
- **Move routing:** use `context.moves[0]` to direct the next investigation. The move with the highest `value` has the most expected information gain.

---

## Streaming

Accumulate the full response, then call `after()` once when the stream completes.

```ts
import { streamText } from 'ai'
import { anthropic } from '@ai-sdk/anthropic'

async function researchStream(question: string) {
  const context = await beliefs.before(question)

  const result = streamText({
    model: anthropic('claude-sonnet-4-20250514'),
    system: context.prompt,
    prompt: question,
  })

  let fullText = ''
  for await (const chunk of result.textStream) {
    process.stdout.write(chunk)
    fullText += chunk
  }

  // Call after() once with the complete text
  const delta = await beliefs.after(fullText)
  return { text: fullText, delta }
}
```

In a Next.js route handler, use `onFinish`:

```ts
export async function POST(req: Request) {
  const { messages } = await req.json()
  const lastMessage = messages[messages.length - 1]?.content ?? ''
  const context = await beliefs.before(lastMessage)

  const result = streamText({
    model: anthropic('claude-sonnet-4-20250514'),
    system: context.prompt,
    messages,
    onFinish: async ({ text }) => {
      await beliefs.after(text)
    },
  })

  return result.toDataStreamResponse()
}
```

<Callout type="warning" title="One after() per turn">
Call `after()` exactly once per turn, after the stream completes. Do not call it on partial chunks. Each `after()` triggers full extraction and fusion against incomplete text, which produces duplicate beliefs, spurious contradictions, and ledger churn that's hard to clean up later. If you need live UI feedback during a stream, use `subscribe()` for projection updates instead. Let the final `after()` do the actual extraction.
</Callout>

---

## Tool-Aware

When your agent uses tools, feed each tool result separately so beliefs update as evidence arrives mid-turn.

```ts
const context = await beliefs.before(question)

const message = await client.messages.create({
  model: 'claude-sonnet-4-20250514',
  system: context.prompt,
  messages: [{ role: 'user', content: question }],
  tools: myTools,
})

// Feed each tool result. Source is tracked per-belief for traceability.
for (const block of message.content) {
  if (block.type === 'tool_use') {
    const result = await executeTool(block.name, block.input)
    await beliefs.after(JSON.stringify(result), {
      tool: block.name,
      source: `tool:${block.name}`,
    })
  } else if (block.type === 'text') {
    await beliefs.after(block.text)
  }
}
```

With the Vercel AI SDK and `maxSteps`:

```ts
const { text, toolResults } = await generateText({
  model: anthropic('claude-sonnet-4-20250514'),
  system: context.prompt,
  prompt: question,
  tools: myTools,
  maxSteps: 5,
})

// Feed tool results individually
for (const result of toolResults) {
  await beliefs.after(JSON.stringify(result.result), { tool: result.toolName })
}

// Then feed the final text
await beliefs.after(text)
```

**When to use:** Agents that call external APIs, search the web, query databases, or use any tools that return factual data.

**Why per-tool?** Each tool result is a distinct observation with its own provenance. Feeding them individually lets the system attribute claims to the right tool, detect when a tool result *contradicts* an existing belief, and notice when a tool result *resolves* a gap. If you concatenate everything into one `after()` call, those per-source contradictions and gap-resolutions get smeared together and the relationships are missed.

---

## Multi-Agent

Multiple agents contribute to the same shared belief state. They share a `namespace` and `writeScope: 'space'`, but use different `agent` identifiers so contributions are attributed.

```ts
const researcher = new Beliefs({
  apiKey,
  agent: 'researcher',
  namespace: 'market-analysis',
  writeScope: 'space',
})

const critic = new Beliefs({
  apiKey,
  agent: 'critic',
  namespace: 'market-analysis',
  writeScope: 'space',
})

// Researcher gathers evidence
const researchContext = await researcher.before('AI tools market size')
const findings = await callLLM(researchContext.prompt, 'Research AI tools market')
await researcher.after(findings)

// Critic challenges the findings
const criticContext = await critic.before('Challenge these market findings')
const critique = await callLLM(criticContext.prompt, 'Find weaknesses')
await critic.after(critique)

// Both see the same world state
const world = await researcher.read()
console.log(`Contradictions: ${world.contradictions.length}`)
console.log(`Total beliefs: ${world.beliefs.length}`)
```

**When to use:** Debate systems, red-team/blue-team, supervisor/worker patterns, any architecture with multiple agents reasoning about the same domain.

**How it works:** All agents in the same namespace with `writeScope: 'space'` share one authoritative state. When the critic adds beliefs that contradict the researcher's findings, the system detects the contradiction automatically. If you want private agent memory plus shared background, switch to `writeScope: 'agent'`.

---

## Combining Patterns

Most production agents combine patterns. Here's a multi-turn streaming agent with tool use:

```ts
async function deepResearch(question: string) {
  await beliefs.add(question, { type: 'goal' })

  for (let turn = 0; turn < 5; turn++) {
    const context = await beliefs.before(question)
    if (context.clarity > 0.8) break

    const { text, toolResults } = await generateText({
      model: anthropic('claude-sonnet-4-20250514'),
      system: context.prompt,
      prompt: context.moves[0]?.target ?? question,
      tools: myTools,
      maxSteps: 3,
    })

    for (const result of toolResults) {
      await beliefs.after(JSON.stringify(result.result), { tool: result.toolName })
    }
    await beliefs.after(text)
  }

  return await beliefs.read()
}
```

---

## Smaller patterns

Once the loop is in place, these are the moves and accessors you'll reach for most.

### Clarity-driven routing

Branch on `context.clarity` to decide what to do next:

```ts
const context = await beliefs.before(input)

if (context.clarity < 0.3) {
  await runResearch(context.gaps)
} else if (context.clarity > 0.7) {
  await draftRecommendations(context.beliefs)
} else {
  await investigateGaps(context.gaps)
}
```

For coarser routing, `delta.readiness` returns `'low' | 'medium' | 'high'`: a categorical projection of the underlying 0–1 clarity score, useful when you want simple branching without picking your own thresholds.

### Confidence gating

Only act on beliefs above a confidence threshold:

```ts
const world = await beliefs.read()

const strong = world.beliefs.filter(b => b.confidence > 0.7)
const weak = world.beliefs.filter(b => b.confidence <= 0.7)

// Use strong beliefs in the response
// Flag weak beliefs for further investigation
```

### Gap-driven research

Read open gaps and use them to drive the next research action. The agent's next move is shaped by what it doesn't know, not just what the user asked:

```ts
const context = await beliefs.before(input)

for (const gap of context.gaps) {
  const result = await searchTool.run(gap)
  await beliefs.after(result, { tool: 'search' })
}
```

### Custom assertion with evidence

When you have domain-specific knowledge, assert it directly with evidence and supersession:

```ts
await beliefs.add('Market is $6.8B', {
  confidence: 0.92,
  evidence: 'IDC Q4 2025 tracker, 2400 enterprise survey',
  supersedes: 'Market is $4.2B',
})
```

Explicit assertions take precedence over auto-extracted beliefs when they conflict.

### Inspecting the trace

Use the trace to debug belief transitions:

```ts
const history = await beliefs.trace()

for (const entry of history) {
  console.log(`${entry.timestamp} | ${entry.action}`)
  if (entry.confidence) {
    console.log(`  ${entry.confidence.before} → ${entry.confidence.after}`)
  }
  if (entry.reason) console.log(`  reason: ${entry.reason}`)
}
```

For replay-shaped reads ("what did the world look like at time T?"), use [`beliefs.stateAt({ asOf })`](/dev/sdk/core-api) instead.

<CardGroup cols={2}>
  <DocsCard title="Scoping" description="Namespace, thread, and agent isolation patterns." href="/dev/sdk/scoping" />
  <DocsCard title="Core API" description="Full method reference." href="/dev/sdk/core-api" />
</CardGroup>
