Every agent using beliefs follows the same before → act → after cycle. The difference is how you arrange that cycle for your use case.
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).
Choosing a Pattern
1┌─ Is this a single request/response? ──→ Single-turn
2│
3├─ Does the agent use tools? ──→ Tool-aware
4│
5├─ Does the agent stream output? ──→ Streaming
6│
7├─ Should the agent loop until confident? ──→ Multi-turn
8│
9└─ Do multiple agents collaborate? ──→ Multi-agentMost 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.
1async function answer(question: string) {
2 const context = await beliefs.before(question)
3 const result = await callLLM(context.prompt, question)
4 const delta = await beliefs.after(result)
5
6 return result
7}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: Even in single-turn, beliefs accumulate across calls. The second time the user asks about the same 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.
1async function research(question: string) {
2 await beliefs.add(question, { type: 'goal' })
3
4 for (let turn = 0; turn < 10; turn++) {
5 const context = await beliefs.before(question)
6
7 // Stop when clarity is high enough
8 if (context.clarity > 0.7) {
9 return {
10 beliefs: context.beliefs,
11 clarity: context.clarity,
12 gaps: context.gaps,
13 }
14 }
15
16 // Follow the highest-value move
17 const focus = context.moves[0]?.target ?? question
18 const result = await callLLM(context.prompt, focus)
19 const delta = await beliefs.after(result)
20
21 console.log(
22 `Turn ${turn + 1}: clarity ${delta.clarity.toFixed(2)}, ` +
23 `${delta.changes.length} changes`
24 )
25 }
26
27 // Hit turn limit — return what we have
28 return await beliefs.read()
29}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.7is 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 highestvaluehas the most expected information gain.
Streaming
Accumulate the full response, then call after() once when the stream completes.
1import { streamText } from 'ai'
2import { anthropic } from '@ai-sdk/anthropic'
3
4async function researchStream(question: string) {
5 const context = await beliefs.before(question)
6
7 const result = streamText({
8 model: anthropic('claude-sonnet-4-20250514'),
9 system: context.prompt,
10 prompt: question,
11 })
12
13 let fullText = ''
14 for await (const chunk of result.textStream) {
15 process.stdout.write(chunk)
16 fullText += chunk
17 }
18
19 // Call after() once with the complete text
20 const delta = await beliefs.after(fullText)
21 return { text: fullText, delta }
22}In a Next.js route handler, use onFinish:
1export async function POST(req: Request) {
2 const { messages } = await req.json()
3 const lastMessage = messages[messages.length - 1]?.content ?? ''
4 const context = await beliefs.before(lastMessage)
5
6 const result = streamText({
7 model: anthropic('claude-sonnet-4-20250514'),
8 system: context.prompt,
9 messages,
10 onFinish: async ({ text }) => {
11 await beliefs.after(text)
12 },
13 })
14
15 return result.toDataStreamResponse()
16}One after() per turn
Call after() exactly once per turn, after the stream completes. Each call triggers extraction and fusion — calling it on partial chunks creates duplicate beliefs from incomplete text.
Tool-Aware
When your agent uses tools, feed each tool result separately so beliefs update as evidence arrives mid-turn.
1const context = await beliefs.before(question)
2
3const message = await client.messages.create({
4 model: 'claude-sonnet-4-20250514',
5 system: context.prompt,
6 messages: [{ role: 'user', content: question }],
7 tools: myTools,
8})
9
10// Feed each tool result — source is tracked per-belief for traceability
11for (const block of message.content) {
12 if (block.type === 'tool_use') {
13 const result = await executeTool(block.name, block.input)
14 await beliefs.after(JSON.stringify(result), {
15 tool: block.name,
16 source: `tool:${block.name}`,
17 })
18 } else if (block.type === 'text') {
19 await beliefs.after(block.text)
20 }
21}With the Vercel AI SDK and maxSteps:
1const { text, toolResults } = await generateText({
2 model: anthropic('claude-sonnet-4-20250514'),
3 system: context.prompt,
4 prompt: question,
5 tools: myTools,
6 maxSteps: 5,
7})
8
9// Feed tool results individually
10for (const result of toolResults) {
11 await beliefs.after(JSON.stringify(result.result), { tool: result.toolName })
12}
13
14// Then feed the final text
15await 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. Feeding them individually lets the system detect when a tool result contradicts an existing belief or resolves a gap. If you batch everything, these relationships can be 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.
1const researcher = new Beliefs({
2 apiKey,
3 agent: 'researcher',
4 namespace: 'market-analysis',
5 writeScope: 'space',
6})
7
8const critic = new Beliefs({
9 apiKey,
10 agent: 'critic',
11 namespace: 'market-analysis',
12 writeScope: 'space',
13})
14
15// Researcher gathers evidence
16const researchContext = await researcher.before('AI tools market size')
17const findings = await callLLM(researchContext.prompt, 'Research AI tools market')
18await researcher.after(findings)
19
20// Critic challenges the findings
21const criticContext = await critic.before('Challenge these market findings')
22const critique = await callLLM(criticContext.prompt, 'Find weaknesses')
23await critic.after(critique)
24
25// Both see the same world state
26const world = await researcher.read()
27console.log(`Contradictions: ${world.contradictions.length}`)
28console.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:
1async function deepResearch(question: string) {
2 await beliefs.add(question, { type: 'goal' })
3
4 for (let turn = 0; turn < 5; turn++) {
5 const context = await beliefs.before(question)
6 if (context.clarity > 0.8) break
7
8 const { text, toolResults } = await generateText({
9 model: anthropic('claude-sonnet-4-20250514'),
10 system: context.prompt,
11 prompt: context.moves[0]?.target ?? question,
12 tools: myTools,
13 maxSteps: 3,
14 })
15
16 for (const result of toolResults) {
17 await beliefs.after(JSON.stringify(result.result), { tool: result.toolName })
18 }
19 await beliefs.after(text)
20 }
21
22 return await beliefs.read()
23}