thinkn
  • Product
    Manifesto
    The reason we exist
    Founder Studioprivate beta
    Make better product decisions faster
    Belief SDKinvite only
    Add belief states to your AI system
    Request Access →Join the private beta waitlist
  • Docs
  • Pricing
  • FAQ
  • Docs
  • Pricing
  • FAQ
Sign In
Welcome
  • Start Here
  • Install
  • Quickstart
  • FAQ
  • Why beliefs
  • Core API
  • Patterns
  • Scope reads
  • Moves & Forecast
  • Trust & Tools
  • Streaming
  • Scoping
  • Auth
  • Errors
sdk/patterns.mdx

Patterns

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.

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.

`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 for working recipes). The point of these examples is the belief flow.

Choosing a loop 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-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.

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: 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.

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

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


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

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}

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:

1const context = await beliefs.before(input)
2
3if (context.clarity < 0.3) {
4  await runResearch(context.gaps)
5} else if (context.clarity > 0.7) {
6  await draftRecommendations(context.beliefs)
7} else {
8  await investigateGaps(context.gaps)
9}

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:

1const world = await beliefs.read()
2
3const strong = world.beliefs.filter(b => b.confidence > 0.7)
4const weak = world.beliefs.filter(b => b.confidence <= 0.7)
5
6// Use strong beliefs in the response
7// 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:

1const context = await beliefs.before(input)
2
3for (const gap of context.gaps) {
4  const result = await searchTool.run(gap)
5  await beliefs.after(result, { tool: 'search' })
6}

Custom assertion with evidence

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

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

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

Inspecting the trace

Use the trace to debug belief transitions:

1const history = await beliefs.trace()
2
3for (const entry of history) {
4  console.log(`${entry.timestamp} | ${entry.action}`)
5  if (entry.confidence) {
6    console.log(`  ${entry.confidence.before} → ${entry.confidence.after}`)
7  }
8  if (entry.reason) console.log(`  reason: ${entry.reason}`)
9}

For replay-shaped reads ("what did the world look like at time T?"), use beliefs.stateAt({ asOf }) instead.

Scoping

Namespace, thread, and agent isolation patterns.

Learn more

Core API

Full method reference.

Learn more
PreviousCore API
NextScope reads

On this page

  • Choosing a loop pattern
  • Single-Turn
  • Multi-Turn (Clarity-Driven)
  • Streaming
  • Tool-Aware
  • Multi-Agent
  • Combining Patterns
  • Smaller patterns
  • Clarity-driven routing
  • Confidence gating
  • Gap-driven research
  • Custom assertion with evidence
  • Inspecting the trace