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
  • Hack Guide
  • Introduction
  • Install
  • Quickstart
  • FAQ
  • The Problem
  • Memory vs Beliefs
  • Drift
  • Examples
  • Overview
  • Core API
  • Loop Patterns
  • Scoping
  • Patterns
  • Adapters
sdk/loops.mdx

Loop Patterns

How to structure your agent loop with beliefs — single-turn, multi-turn, streaming, tool-aware, and multi-agent.

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

Scoping

Namespace, thread, and agent isolation patterns.

Learn more

Core API

Full method reference.

Learn more
PreviousCore API
NextScoping

On this page

  • Choosing a Pattern
  • Single-Turn
  • Multi-Turn (Clarity-Driven)
  • Streaming
  • Tool-Aware
  • Multi-Agent
  • Combining Patterns