Agent Runtime & Reasoning
Build agents with pluggable reasoning strategies, handoffs-as-tools for multi-agent collaboration, and a composable workflow system — all streaming-first via iter.Seq2.
Overview
The Beluga AI agent runtime is the central building block for creating autonomous AI systems in Go. At its core, a BaseAgent wraps an LLM with tools, memory, a persona, and a planner to produce a typed event stream via iter.Seq2[Event, error]. Every interaction — tool calls, reasoning steps, partial responses — flows through this single streaming interface, giving you full visibility and control over agent behavior.
The design philosophy is composability over configuration. Instead of a monolithic agent class, you assemble capabilities through functional options: pick a reasoning strategy, attach tools, wire in memory, define a persona, and set up handoffs to other agents. Each piece is independently testable and replaceable. The agent runtime sits in the Capability Layer of the architecture, consuming LLM providers and tools from below while being orchestrated by workflow patterns from above.
Whether you need a simple ReAct loop for straightforward tasks or a Monte Carlo Tree Search planner that achieves 94.4% on HumanEval, the agent runtime gives you a consistent API. Multi-agent collaboration happens through handoffs-as-tools — agent transfers are modeled as ordinary tool calls, meaning the LLM decides when to hand off and the framework handles the rest, with zero additional boilerplate.
Capabilities
BaseAgent
The core agent type wraps an LLM with tools, memory, a persona, and a planner. It produces typed event streams — every tool call, reasoning step, and partial response is an Event yielded through iter.Seq2. Construct agents with functional options for a clean, composable API.
agent := agent.New("researcher",
agent.WithLLM(model),
agent.WithTools(webSearch, calculator),
agent.WithMemory(mem),
agent.WithPlanner(planner.New("react")),
) Persona Engine
Define agent identity separately from agent behavior. A persona encapsulates system prompts, behavioral constraints, response style, and domain expertise. This separation means you can swap personas without touching tools or planners — the same agent runtime can behave as a formal analyst or a casual assistant.
persona := agent.Persona{
Name: "Financial Analyst",
Instructions: "You are a senior financial analyst...",
Style: "formal, data-driven, cite sources",
Constraints: []string{"Never provide investment advice"},
}
agent := agent.New("analyst", agent.WithPersona(persona)) Pluggable Planners
Seven reasoning strategies, selectable at construction time. Each planner implements the same interface, so switching strategies is a single line change. Choose based on your task requirements:
- ReAct — Observe-think-act loop. The baseline for most tasks.
- Reflexion — Verbal reinforcement learning with episodic memory for self-improvement.
- Self-Discover — Compute-efficient JSON reasoning, up to 32% improvement over Chain-of-Thought with 10-40x fewer LLM calls.
- Tree-of-Thought — Parallel branching exploration for complex problem-solving.
- Graph-of-Thought — Non-linear reasoning DAGs for interconnected problem spaces.
- LATS — Monte Carlo Tree Search achieving 94.4% on HumanEval.
- Mixture-of-Agents — Multi-model ensemble combining outputs from diverse LLMs.
// Switch planners with a single option
agent := agent.New("solver",
agent.WithLLM(model),
agent.WithPlanner(planner.New("lats")), // or "react", "reflexion", "tot", etc.
agent.WithTools(tools...),
) Handoffs-as-Tools
Multi-agent transfers are modeled as tool calls. When you register handoffs, the framework auto-generates transfer_to_{name} tools. The LLM decides when to hand off based on the tool descriptions. Context filtering via HandoffInputData controls what conversation history flows to the target agent, keeping context focused and token-efficient.
triage := agent.New("triage",
agent.WithLLM(model),
agent.WithHandoffs(
agent.Handoff{Agent: billingAgent, Description: "Billing questions"},
agent.Handoff{Agent: shippingAgent, Description: "Shipping inquiries"},
),
)
// LLM sees: transfer_to_billing, transfer_to_shipping as available tools Workflow Agents
For deterministic pipelines where you do not want LLM-driven routing, workflow agents compose agents into fixed execution patterns. SequentialAgent runs agents in order, passing output as input. ParallelAgent runs agents concurrently and merges results. LoopAgent repeats an agent until a condition is met.
pipeline := workflow.Sequential(
researchAgent,
analysisAgent,
summaryAgent,
)
for event, err := range pipeline.Stream(ctx, "Analyze market trends") {
// Events from each agent in sequence
} Dynamic Tool Selection
When agents have large tool inventories (50+ tools), sending all tool schemas to the LLM wastes context and degrades performance. Dynamic tool selection filters tools per turn using either LLM-based relevance scoring or embedding-based similarity matching, presenting only the most relevant tools for each interaction.
agent := agent.New("assistant",
agent.WithLLM(model),
agent.WithTools(allTools...), // 100+ tools registered
agent.WithToolSelector(
tool.EmbeddingSelector(embedder, tool.WithTopK(10)),
),
) Architecture
Full Example
A complete example creating a research agent with the ReAct planner, tools, memory, and streaming output:
package main
import (
"context"
"fmt"
"github.com/lookatitude/beluga-ai/agent"
"github.com/lookatitude/beluga-ai/llm"
"github.com/lookatitude/beluga-ai/memory"
"github.com/lookatitude/beluga-ai/agent/planner"
"github.com/lookatitude/beluga-ai/tool"
)
func main() {
ctx := context.Background()
// Create an LLM provider
model, _ := llm.New("openai", llm.ProviderConfig{Model: "gpt-4o"})
// Define tools
webSearch := tool.NewFuncTool("web_search", "Search the web", searchFunc)
calculator := tool.NewFuncTool("calculator", "Evaluate math", calcFunc)
// Create agent with ReAct planner, tools, and semantic memory
researcher := agent.New("researcher",
agent.WithLLM(model),
agent.WithPlanner(planner.New("react")),
agent.WithTools(webSearch, calculator),
agent.WithMemory(memory.NewSemantic(embedder, store)),
agent.WithPersona(agent.Persona{
Name: "Research Assistant",
Instructions: "You are a thorough research assistant.",
}),
)
// Stream events — tool calls, reasoning, partial responses
for event, err := range researcher.Stream(ctx, "What were the key AI breakthroughs in 2025?") {
if err != nil {
fmt.Printf("Error: %v\n", err)
break
}
switch e := event.(type) {
case *agent.TextEvent:
fmt.Print(e.Text)
case *agent.ToolCallEvent:
fmt.Printf("\n[Tool: %s]\n", e.Name)
}
}
}