reactree

package
v0.1.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 1, 2026 License: Apache-2.0 Imports: 35 Imported by: 0

README

pkg/reactree

A thin wrapper around trpc-agent-go's graph.StateGraph that maps Behavior Tree (BT) semantics — Sequence, Fallback, and Parallel control flow — onto state graph primitives.

ReAcTree is inspired by the ReAcTree paper, which introduces a tree-structured reasoning framework for LLM agents that combines subgoal decomposition with episodic memory.


Architecture

┌─────────────────────────────────────────────────────────────┐
│                      TreeExecutor                           │
│  Builds a graph.StateGraph, compiles, runs graph.Executor   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐   ┌──────────────┐   ┌──────────────────┐ │
│  │  node.go     │   │ agent_node.go│   │ control_flow.go  │ │
│  │             │   │              │   │                  │ │
│  │ NodeStatus  │   │ AgentNodeFunc│   │ BuildSequence    │ │
│  │ StateKeys   │   │ (NodeFunc)   │   │ BuildFallback    │ │
│  │ StateSchema │   │              │   │ BuildParallel    │ │
│  └──────┬──────┘   └──────┬───────┘   └────────┬─────────┘ │
│         │                 │                     │           │
│         └────────┬────────┴─────────────────────┘           │
│                  ▼                                           │
│         graph.StateGraph (trpc-agent-go)                    │
│         graph.Executor   (trpc-agent-go)                    │
├─────────────────────────────────────────────────────────────┤
│                      memory/                                │
│  ┌──────────────────┐  ┌─────────────────────────────────┐  │
│  │  working.go       │  │  episodic.go                    │  │
│  │  WorkingMemory    │  │  EpisodicMemory (interface)     │  │
│  │  (KV scratchpad)  │  │  → serviceEpisodicMemory        │  │
│  │                   │  │    (delegates to memory.Service) │  │
│  │                   │  │  → noOpEpisodicMemory            │  │
│  └──────────────────┘  └─────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
Package Layout
pkg/reactree/
├── node.go              # NodeStatus enum, StateKeys, NewReAcTreeSchema()
├── agent_node.go        # NewAgentNodeFunc() → graph.NodeFunc wrapping expert.Expert
├── control_flow.go      # BuildSequence / BuildFallback / BuildParallel
├── tree.go              # TreeExecutor interface + default implementation
├── control_flow_test.go # Graph compilation & execution tests
├── init_test.go         # Ginkgo test suite bootstrap
├── memory/
│   ├── working.go       # WorkingMemory — thread-safe KV shared across nodes
│   ├── episodic.go      # EpisodicMemory interface + memory.Service delegate
│   ├── memory_test.go   # Memory tests (working, episodic, service-backed)
│   └── init_test.go     # Memory test suite bootstrap
└── reactreefakes/       # counterfeiter-generated fakes

Key Concepts

Control Flow → Graph Primitive Mapping
BT Pattern ReAcTree Builder Graph Primitive Semantics
Sequence BuildSequence(sg, nodeIDs) AddConditionalEdges success→next, failure→END (AND)
Fallback BuildFallback(sg, nodeIDs) AddConditionalEdges success→END, failure→next (OR)
Parallel BuildParallel(sg, nodeIDs, aggID) AddJoinEdge + aggregator Majority vote (fan-out/fan-in)
State Graph Schema

All nodes share state via graph.State using these keys:

Key Type Purpose
reactree_goal string The current task goal
reactree_node_status NodeStatus Last node's Success/Failure/Running
reactree_output string Text output from the last node
reactree_working_memory map[string]any Shared observations across nodes
Memory System
Component Type Backend Purpose
WorkingMemory Concrete struct In-memory KV Ephemeral scratchpad for a single tree run
EpisodicMemory Interface memory.Service Long-term subgoal experience storage & retrieval

Usage

Basic: Single-Goal Execution
import (
    "github.com/stackgenhq/genie/pkg/reactree"
    "github.com/stackgenhq/genie/pkg/reactree/memory"
)

// Create memory instances
wm := memory.NewWorkingMemory()
ep := memory.NewNoOpEpisodicMemory() // or NewServiceEpisodicMemory(cfg)

// Create executor
executor := reactree.NewTreeExecutor(myExpert, wm, ep, reactree.DefaultTreeConfig())

// Run
result, err := executor.Run(ctx, reactree.TreeRequest{
    Goal:      "Analyze the Terraform configuration and suggest improvements",
    EventChan: eventCh,
})
With memory.Service-Backed Episodic Memory
import (
    "github.com/stackgenhq/genie/pkg/reactree/memory"
    "trpc.group/trpc-go/trpc-agent-go/memory/inmemory"
)

svc := inmemory.NewMemoryService()
defer svc.Close()

ep := memory.EpisodicMemoryConfig{
    Service: svc,
    AppName: "my-app",
    UserID:  "user-123",
}.NewServiceEpisodicMemory()

executor := reactree.NewTreeExecutor(myExpert, nil, ep, reactree.DefaultTreeConfig())
Building Custom Graph Topologies
schema := reactree.NewReAcTreeSchema()
sg := graph.NewStateGraph(schema)

// Add agent nodes
sg.AddNode("analyze", reactree.NewAgentNodeFunc(analyzeConfig))
sg.AddNode("fix",     reactree.NewAgentNodeFunc(fixConfig))
sg.AddNode("verify",  reactree.NewAgentNodeFunc(verifyConfig))
sg.SetEntryPoint("analyze")

// Wire as sequence: analyze → fix → verify (fail-fast on any failure)
reactree.BuildSequence(sg, []string{"analyze", "fix", "verify"})

// Compile & execute
compiled, _ := sg.Compile()
executor, _ := graph.NewExecutor(compiled)
events, _   := executor.Execute(ctx, graph.State{
    reactree.StateKeyGoal: "Fix all lint errors",
}, invocation)

Configuration

TreeConfig
reactree.TreeConfig{
    MaxDepth:            3,   // Max tree depth for recursive expansion
    MaxDecisionsPerNode: 10,  // Max LLM calls per agent node
    MaxTotalNodes:       20,  // Max total nodes (maps to graph.WithMaxSteps)
}

Use reactree.DefaultTreeConfig() for sensible defaults.


Runbook

Running Tests
# Run all reactree tests
go test -v -count=1 ./pkg/reactree/...

# Run memory sub-package tests only
go test -v -count=1 ./pkg/reactree/memory/...

# Run with race detector
go test -race -count=1 ./pkg/reactree/...
Regenerating Fakes
go generate ./pkg/reactree/...
go generate ./pkg/reactree/memory/...

This regenerates counterfeiter fakes for:

  • TreeExecutorreactreefakes/fake_tree_executor.go
  • EpisodicMemorymemory/memoryfakes/fake_episodic_memory.go (after running generate)
Adding a New Control Flow Pattern
  1. Create a Build<Pattern>(sg, nodeIDs, ...) *graph.StateGraph function in control_flow.go
  2. Use graph.AddConditionalEdges, graph.AddEdge, or graph.AddJoinEdge to wire the topology
  3. The statusRouter function routes based on StateKeyNodeStatus — reuse it or create a custom router
  4. Add compilation and execution tests in control_flow_test.go
Adding a New Agent Node Type
  1. Define a config struct with the required fields
  2. Create a factory function returning graph.NodeFunc
  3. Read inputs from graph.State, perform work, return a graph.State with at minimum:
    • StateKeyNodeStatusSuccess or Failure
    • StateKeyOutput → text result
  4. Register the node with sg.AddNode(id, yourFunc)
Integrating ReAcTree into a New Expert

Follow the pattern in pkg/codeowner/expert.go:

func NewExpert(/* ... */) *Expert {
    wm := memory.NewWorkingMemory()
    treeExec := reactree.NewTreeExecutor(exp, wm, nil, reactree.DefaultTreeConfig())

    return &Expert{
        workingMemory: wm,
        treeExecutor:  treeExec,
    }
}

func (e *Expert) Chat(ctx context.Context, msg string) (string, error) {
    result, err := e.treeExecutor.Run(ctx, reactree.TreeRequest{
        Goal: msg,
    })
    return result.Output, err
}
Debugging
  • Set log level to debug to see agent node prompts, output lengths, and majority vote results
  • Check StateKeyNodeStatus in events to trace success/failure paths through the graph
  • Use wm.Snapshot() to inspect the working memory at any point during execution

Dependencies

Package Purpose
trpc-agent-go/graph StateGraph, Executor, NodeFunc, State, StateSchema
trpc-agent-go/memory memory.Service for episodic memory backend
trpc-agent-go/memory/inmemory In-memory memory.Service implementation
pkg/expert LLM expert interface used by agent nodes
go-lib/logger Structured logging

Documentation

Overview

Package reactree implements the ReAcTree (Reasoning-Acting Tree) execution engine for Genie's multi-step agent workflows.

It solves the problem of running complex tasks as a behavior tree: the orchestrator builds a graph of nodes (sequence, selector, agent nodes), and the tree is ticked until completion. Each agent node runs an Expert (LLM + tools); control flow (retry, fallback, parallel stages) is expressed as tree structure rather than ad-hoc code. Without this package, multi-step flows would be hard-coded and harder to extend or reason about.

Index

Constants

View Source
const (
	AuditEventIterationStart  audit.EventType = "reactree_iteration_start"
	AuditEventIterationEnd    audit.EventType = "reactree_iteration_end"
	AuditEventCriticRejection audit.EventType = "reactree_critic_rejection"
	AuditEventReflection      audit.EventType = "reactree_reflection"
	AuditEventDryRun          audit.EventType = "reactree_dry_run"
	AuditEventPlanExecution   audit.EventType = "reactree_plan_execution"
)

AuditEventType constants for ReAcTree-specific audit events.

View Source
const (
	// StateKeyGoal is the current task goal for agent nodes to work on.
	StateKeyGoal = "reactree_goal"
	// StateKeyNodeStatus stores the NodeStatus result from the last node.
	StateKeyNodeStatus = "reactree_node_status"
	// StateKeyOutput stores the text output from the last node.
	StateKeyOutput = "reactree_output"
	// StateKeyWorkingMemory stores shared observations across nodes.
	StateKeyWorkingMemory = "reactree_working_memory"
	// StateKeyTaskCompleted indicates the agent finished without making any
	// tool calls. When true, the task was fully answered in this stage and
	// subsequent stages can be skipped to save cost and latency.
	StateKeyTaskCompleted = "reactree_task_completed"
	// StateKeyPreviousStageOutput carries the output from the previous stage
	// so subsequent stages can see what was already accomplished and avoid
	// redundant work (e.g. repeating the same web search).
	StateKeyPreviousStageOutput = "reactree_previous_stage_output"
	// StateKeyIterationContext carries the accumulated output from all prior
	// iterations in the adaptive loop. This replaces staged previousStageOutput
	// with a rolling context window.
	StateKeyIterationContext = "reactree_iteration_context"
	// StateKeyIterationCount tracks the current iteration number in the
	// adaptive loop (0-indexed).
	StateKeyIterationCount = "reactree_iteration_count"
	// StateKeyToolCallCounts reports per-tool call counts (name→count) from
	// a single agent node execution. The adaptive loop accumulates these
	// across iterations to enforce ToolBudgets.
	StateKeyToolCallCounts = "reactree_tool_call_counts"
)

State keys used by the ReAcTree graph nodes to share data.

View Source
const (
	CreateAgentToolName = "create_agent"
)

Variables

This section is empty.

Functions

func BuildFallback

func BuildFallback(
	sg *graph.StateGraph,
	nodeIDs []string,
) *graph.StateGraph

BuildFallback wires nodes into a fallback on the given StateGraph. Nodes are connected so that if the current one succeeds the graph ends, otherwise the next node is tried. If all nodes fail, the graph ends with failure. This models Behavior Tree Fallback (OR) semantics.

func BuildParallel

func BuildParallel(
	sg *graph.StateGraph,
	nodeIDs []string,
	aggregatorID string,
) *graph.StateGraph

BuildParallel wires nodes for parallel execution with majority voting. Callers are responsible for connecting the entry point (e.g., graph.Start) to each child in nodeIDs to create the fan-out. This function only wires the fan-in via AddJoinEdge to an aggregator node that performs majority voting.

Each child node's original function is wrapped so that its NodeStatus is also stored under a per-node key (StateKeyNodeStatus:<nodeID>). This lets the aggregator read individual results without them being overwritten by sibling nodes that share the same StateKeyNodeStatus key.

func BuildSequence

func BuildSequence(
	sg *graph.StateGraph,
	nodeIDs []string,
) *graph.StateGraph

BuildSequence wires nodes into a sequence on the given StateGraph. Nodes are connected with AddEdge in order. After each node, a conditional edge checks NodeStatus: if Failure, the graph ends immediately. This models Behavior Tree Sequence (AND) semantics.

func BuildSequenceWithEarlyExit

func BuildSequenceWithEarlyExit(
	sg *graph.StateGraph,
	nodeIDs []string,
) *graph.StateGraph

BuildSequenceWithEarlyExit wires nodes into a sequence that supports early termination when a stage completes the task without tool calls. Like BuildSequence, it short-circuits on Failure. Additionally, if a stage sets StateKeyTaskCompleted=true (zero tool calls), remaining stages are skipped — preventing redundant cost and latency.

func NewAgentNodeFunc

func NewAgentNodeFunc(cfg AgentNodeConfig) graph.NodeFunc

NewAgentNodeFunc creates a graph.NodeFunc that wraps an expert.Expert call. The returned function reads the goal from state, enriches the prompt with working memory and episodic memory, calls the expert, and writes the result back to state. This is the bridge between the ReAcTree concept of an "agent node" and trpc-agent-go's graph execution model.

func NewCreateAgentTool

func NewCreateAgentTool(
	modelProvider modelprovider.ModelProvider,
	expert expert.Expert,
	summarizer agentutils.Summarizer,
	toolRegistry *tools.Registry,
	workingMemory *memory.WorkingMemory,
	episodic memory.EpisodicMemory,
	toolWrapSvc *toolwrap.Service,
) *createAgentTool

NewCreateAgentTool creates a tool that spawns sub-agents with dynamic tool subsets. The llmModel is the LLM to use for sub-agents. The toolRegistry is a name→tool map of all available tools the sub-agent can choose from. The optional toolWrapSvc, when provided, wraps sub-agent tools with HITL approval gating, audit logging, and file-read caching — ensuring sub-agents cannot execute write tools without human approval.

func NewReAcTreeSchema

func NewReAcTreeSchema() *graph.StateSchema

NewReAcTreeSchema creates a graph.StateSchema with the fields used by ReAcTree nodes. This schema defines the shared state that flows between nodes in the compiled graph.

func WrapNodeForParallel

func WrapNodeForParallel(nodeID string, original graph.NodeFunc) graph.NodeFunc

WrapNodeForParallel wraps a node function so that, in addition to returning its normal state, the NodeStatus is also stored under a per-node key (StateKeyNodeStatus:<nodeID>). This allows the majority-vote aggregator to read each node's result independently.

Usage: when building a parallel graph, wrap each child node before adding it:

sg.AddNode("a", WrapNodeForParallel("a", myNodeFunc))
sg.AddNode("b", WrapNodeForParallel("b", otherNodeFunc))
BuildParallel(sg, []string{"a", "b"}, "aggregator")

func WrapToolsForDryRun

func WrapToolsForDryRun(tools []tool.Tool) ([]tool.Tool, func() []string)

WrapToolsForDryRun wraps all tools with DryRunToolWrapper for simulation. Returns the wrapped tools and a collector function to gather all invocations.

func WrapWithValidator

func WrapWithValidator(t tool.Tool, validator ActionValidator) tool.Tool

WrapWithValidator wraps the provided tool with the given validator. If validator is nil, it returns the tool unmodified.

Types

type ActionReflector

type ActionReflector interface {
	// Reflect takes a proposed goal and the output from the most recent agent
	// node execution, and produces a reflection text. If the reflection
	// determines the action is unsafe or illogical, it returns an error.
	Reflect(ctx context.Context, req ReflectionRequest) (ReflectionResult, error)
}

ActionReflector performs a Reasoning-Action-Reflection (RAR) loop before external actions. When enabled, the agent is prompted to justify why it is about to take an action, producing an "internal monologue" that is recorded for auditability and prevents hallucinated side-effectful calls.

type ActionValidator

type ActionValidator interface {
	// Validate checks if the tool call with its JSON arguments is permitted.
	// Returns an error if the action is rejected/pruned.
	Validate(ctx context.Context, toolName string, jsonArgs []byte) error
}

ActionValidator defines the middleware interface for deterministic or LLM-based branch pruning.

type AgentNodeConfig

type AgentNodeConfig struct {
	Goal          string
	Expert        expert.Expert
	WorkingMemory *memory.WorkingMemory
	Episodic      memory.EpisodicMemory
	MaxDecisions  int
	Tools         []tool.Tool
	// TaskType selects the model for this node via ModelProvider.GetModel().
	// If empty, defaults to TaskPlanning.
	TaskType modelprovider.TaskType

	// Attachments are file/media attachments from the incoming message.
	// Image attachments are passed as multimodal content to the LLM.
	Attachments []messenger.Attachment

	// BudgetExhaustedTools lists tool names whose budget has been reached.
	// The adaptive loop sets this when tools in ToolBudgets have hit their
	// limits. A prompt hint is injected telling the LLM these tools are
	// unavailable; the tools themselves are also stripped from the list.
	BudgetExhaustedTools []string

	// SystemInstruction, when set, makes this node use a lightweight
	// llmagent instead of the full Expert. This prevents plan-step
	// sub-agents from inheriting the main agent's persona (which
	// contains orchestration patterns and causes tool hallucination).
	SystemInstruction string

	// ModelProvider is used to resolve the model when SystemInstruction
	// is set (lightweight mode). Ignored when using Expert.
	ModelProvider modelprovider.ModelProvider
}

AgentNodeConfig holds configuration for creating an agent node function.

type AuditHook

type AuditHook struct {
	hooks.NoOpHook // embed no-op defaults
	// contains filtered or unexported fields
}

AuditHook implements hooks.ExecutionHook by writing structured events to an audit.Auditor. This is the bridge between the generic hook system and the existing audit infrastructure.

func NewAuditHook

func NewAuditHook(auditor audit.Auditor) *AuditHook

NewAuditHook creates an ExecutionHook that writes to the given auditor. Returns nil if auditor is nil.

func (*AuditHook) OnDryRun

func (h *AuditHook) OnDryRun(ctx context.Context, event hooks.DryRunEvent)

func (*AuditHook) OnIterationEnd

func (h *AuditHook) OnIterationEnd(ctx context.Context, event hooks.IterationEndEvent)

func (*AuditHook) OnIterationStart

func (h *AuditHook) OnIterationStart(ctx context.Context, event hooks.IterationStartEvent)

func (*AuditHook) OnPlanExecution

func (h *AuditHook) OnPlanExecution(ctx context.Context, event hooks.PlanExecutionEvent)

func (*AuditHook) OnReflection

func (h *AuditHook) OnReflection(ctx context.Context, event hooks.ReflectionEvent)

func (*AuditHook) OnToolValidation

func (h *AuditHook) OnToolValidation(ctx context.Context, event hooks.ToolValidationEvent)

type ControlFlowType

type ControlFlowType string

ControlFlowType identifies the behavior of a control flow pattern.

const (
	// ControlFlowSequence executes children in order.
	// Returns Success only if ALL children succeed (AND logic).
	ControlFlowSequence ControlFlowType = "sequence"

	// ControlFlowFallback tries children in order.
	// Returns Success on the FIRST child success (OR logic).
	ControlFlowFallback ControlFlowType = "fallback"

	// ControlFlowParallel executes all children.
	// Returns Success if a majority succeed (majority vote).
	ControlFlowParallel ControlFlowType = "parallel"
)

type CreateAgentRequest

type CreateAgentRequest struct {
	AgentName         string                 `json:"agent_name" jsonschema:"description=Name of the sub-agent,required"`
	Goal              string                 `json:"goal" jsonschema:"description=The goal or task for the sub-agent to accomplish,required"`
	ToolNames         []string               `json:"tool_names,omitempty" jsonschema:"description=Names of tools to give the sub-agent. If empty all tools are provided."`
	TaskType          modelprovider.TaskType `` /* 378-byte string literal not displayed */
	MaxToolIterations int                    `` /* 193-byte string literal not displayed */
	MaxLLMCalls       int                    `` /* 181-byte string literal not displayed */
	TimeoutSeconds    float64                `` /* 210-byte string literal not displayed */

	// SummarizeOutput controls whether large sub-agent output is summarized
	// before returning to the parent agent. When false (default), the raw
	// output is returned as-is, preserving all detail. Set to true only when
	// the output is expected to be very large and a condensed version suffices.
	SummarizeOutput bool `` /* 245-byte string literal not displayed */

	// Steps enables multi-step plan execution. When provided, the tool builds
	// a graph from these steps using the specified Flow type, instead of
	// running a single sub-agent. Each step becomes an agent node.
	Steps []PlanStep `` /* 185-byte string literal not displayed */

	// Flow selects how Steps are coordinated:
	//   sequence  — steps run in order (fail-fast)
	//   parallel  — steps run concurrently (majority vote)
	//   fallback  — steps tried in order (first success wins)
	// Defaults to sequence if not specified.
	Flow string `` /* 156-byte string literal not displayed */
}

CreateAgentRequest is the input for the create_agent tool.

type CreateAgentResponse

type CreateAgentResponse struct {
	Output string `json:"output"`
	Status string `json:"status"`
}

CreateAgentResponse is the output for the create_agent tool.

type DeterministicValidator

type DeterministicValidator struct {
	BlockedTools []string
}

DeterministicValidator implements ActionValidator by checking against safe policies.

func NewDeterministicValidator

func NewDeterministicValidator(blockedTools []string) *DeterministicValidator

NewDeterministicValidator creates a validator that rejects tools in the BlockedTools list

func (*DeterministicValidator) Validate

func (d *DeterministicValidator) Validate(ctx context.Context, toolName string, jsonArgs []byte) error

type DryRunResult

type DryRunResult struct {
	// PlannedSteps is the number of graph nodes that would execute.
	PlannedSteps int
	// ToolsUsed lists the tool names that would be invoked.
	ToolsUsed []string
	// EstimatedCost is a heuristic cost label (low/medium/high).
	EstimatedCost string
	// Summary is a human-readable description of the simulated plan.
	Summary string
}

DryRunResult holds the simulated execution plan summary.

func BuildDryRunSummary

func BuildDryRunSummary(toolsUsed []string, iterationCount int) DryRunResult

BuildDryRunSummary creates a human-readable dry run report.

type DryRunToolWrapper

type DryRunToolWrapper struct {
	tool.Tool
	// contains filtered or unexported fields
}

DryRunToolWrapper wraps a tool.Tool so that Call() records the invocation without executing any real side effects. It returns a mock response.

func NewDryRunToolWrapper

func NewDryRunToolWrapper(t tool.Tool) *DryRunToolWrapper

NewDryRunToolWrapper creates a wrapper that intercepts calls for simulation.

func (*DryRunToolWrapper) Call

func (d *DryRunToolWrapper) Call(ctx context.Context, jsonArgs []byte) (any, error)

Call records the tool invocation and returns a mock success response without executing the underlying tool.

func (*DryRunToolWrapper) Invocations

func (d *DryRunToolWrapper) Invocations() []string

Invocations returns a copy of the tool names that were "called" during simulation.

func (*DryRunToolWrapper) StreamableCall

func (d *DryRunToolWrapper) StreamableCall(ctx context.Context, jsonArgs []byte) (*tool.StreamReader, error)

StreamableCall records the tool invocation and returns nil (no stream in dry run).

type ExpertReflector

type ExpertReflector struct {
	// contains filtered or unexported fields
}

ExpertReflector uses a lightweight LLM call to produce reflections. It uses the front-desk/efficiency model to keep costs low.

func NewExpertReflector

func NewExpertReflector(exp expert.Expert) *ExpertReflector

NewExpertReflector creates a reflector that uses the given expert for reflection prompts. The expert should be configured with a cheap/fast model (e.g. TaskEfficiency).

func (*ExpertReflector) Reflect

Reflect generates an internal monologue evaluating whether the proposed action is aligned with the goal. This is the "R" in the RAR loop.

type NoOpReflector

type NoOpReflector struct{}

NoOpReflector is a no-op implementation that always proceeds. Used when action reflection is disabled.

func (*NoOpReflector) Reflect

Reflect always returns a proceed result with no monologue.

type NodeResult

type NodeResult struct {
	Status NodeStatus
	Output string
	Err    error
}

NodeResult carries the outcome of a node's execution, including any output text produced and any error encountered.

type NodeStatus

type NodeStatus int

NodeStatus represents the outcome of a node execution in the ReAcTree. This enum follows the standard Behavior Tree convention: Running, Success, or Failure.

const (
	// Running indicates the node is still executing and needs more ticks.
	Running NodeStatus = iota
	// Success indicates the node completed its goal successfully.
	Success
	// Failure indicates the node failed to complete its goal.
	Failure
)

func (NodeStatus) String

func (s NodeStatus) String() string

String returns a human-readable representation of the NodeStatus.

type OrchestratorConfig

type OrchestratorConfig struct {
	// Expert provides the LLM policy p_LLM(·) used by each agent node
	// to sample actions. All nodes in the plan share this expert.
	// Thread safety: Expert.Do() creates independent runners per call,
	// so concurrent use from parallel nodes is safe.
	Expert expert.Expert

	// WorkingMemory is the shared blackboard (Section 4.2) that stores
	// environment-specific observations across all agent nodes in the tree.
	// Enables steps to share results without re-exploration.
	WorkingMemory *memory.WorkingMemory

	// Episodic is the experience store (Section 4.2) that records
	// subgoal-level trajectories. Successful step results are stored
	// as episodes for future in-context retrieval.
	Episodic memory.EpisodicMemory

	// MaxDecisions caps the number of LLM calls per agent node (D_max
	// in Algorithm 1, line 11). Prevents runaway iteration.
	MaxDecisions int

	ToolRegistry *tools.Registry

	// ToolWrapSvc wraps plan step tools with HITL approval, audit logging,
	// and caching — same as single sub-agent tools. Without this, plan step
	// agents would bypass human approval for write tools.
	ToolWrapSvc *toolwrap.Service

	// WrapRequest carries per-request fields (ThreadID, RunID)
	// needed for HITL wrapping to propagate approval events to the UI.
	WrapRequest toolwrap.WrapRequest

	// Timeout is the wall-clock deadline for the entire plan execution.
	// Prevents stuck plan steps from hanging the parent agent indefinitely.
	// Defaults to 3 minutes if zero.
	Timeout time.Duration

	// Toggles holds opt-in configurations for predictability.
	Toggles Toggles

	// ModelProvider resolves models for lightweight plan-step agents.
	// When set (along with ToolRegistry), plan-step agents use a minimal
	// sub-agent instruction instead of the full Expert persona.
	ModelProvider modelprovider.ModelProvider
}

OrchestratorConfig holds the dependencies for executing a Plan. This is the Go implementation of the inputs to Algorithm 2 (ExecCtrlFlowNode).

Reference: ReAcTree (arXiv:2511.02424), Section 4.1, Algorithm 2

type OrchestratorResult

type OrchestratorResult struct {
	Status  NodeStatus
	Outputs map[string]string // step name → output
}

OrchestratorResult captures the outcome of a planned execution. Status maps to the success/failure return of Algorithm 2. Outputs maps each step name to its text result.

func ExecutePlan

func ExecutePlan(ctx context.Context, plan Plan, cfg OrchestratorConfig) (OrchestratorResult, error)

ExecutePlan runs a Plan by building a graph with the appropriate control flow and agent nodes. This is the Go implementation of Algorithm 2 (ExecCtrlFlowNode).

For "sequence" flow: steps run in order, sharing state. Each step's output becomes the previous stage output for the next step.

For "parallel" flow: steps run concurrently via graph.AddJoinEdge. Results are aggregated by majority vote.

For "fallback" flow: steps are tried in order. First success returns.

type Plan

type Plan struct {
	// Flow corresponds to f^n in the paper — the control flow type that
	// governs how child agent nodes are coordinated.
	//
	// Paper (Section 4.1, "Control Flow Nodes"):
	//   sequence (→) — returns success only if ALL children succeed
	//   fallback (?)  — returns success on the FIRST child success
	//   parallel (⇒) — aggregates outcomes via majority voting
	Flow ControlFlowType `json:"flow"`

	// Steps correspond to the subgoals [g_1^n, ..., g_K^n] in the paper.
	// Each step becomes an independent agent node n_i with its own goal,
	// tool set, and local context.
	Steps []PlanStep `json:"steps"`
}

Plan represents a decomposed task with subgoals and a control flow strategy.

This implements the paper's "Expand" action (Algorithm 1, lines 20-28):

a_t^n = (f^n, [g_1^n, ..., g_K^n])

where f^n is the control flow type (Flow) and each g_i^n is a natural language subgoal (Steps). A control flow node n_f with type f^n is attached as a child of the expanding agent node, and agent nodes n_i with subgoals g_i^n are added as children of n_f.

Reference: ReAcTree (arXiv:2511.02424), Section 4.1, "Agent Nodes > Expanding"

type PlanStep

type PlanStep struct {
	// Name uniquely identifies this step (used as graph node ID).
	Name string `json:"name"`

	// Goal is g_i^n — the natural language subgoal for this agent node.
	// The paper emphasizes that isolating subgoals reduces hallucination
	// and logical errors by keeping each agent focused on its local context.
	Goal string `json:"goal"`

	// Tools define the executable skill set A_t^n available to this node.
	// send_message is always stripped (framework invariant).
	Tools []string `json:"tools,omitempty"`

	// TaskType selects the LLM model for this step.
	TaskType modelprovider.TaskType `json:"task_type,omitempty"`
}

PlanStep is a single subgoal in a Plan, corresponding to one agent node n_i in the paper's tree. Each agent node operates as an LLM-based task planner with its own subgoal g_i^n, context c_t^n, and action space A_t^n.

Reference: ReAcTree (arXiv:2511.02424), Section 4.1, "Agent Nodes"

type ReflectionRequest

type ReflectionRequest struct {
	// Goal is the current task goal.
	Goal string
	// ProposedOutput is the output the agent is about to produce.
	ProposedOutput string
	// ToolCallsMade lists the tool names that were invoked in this turn.
	ToolCallsMade []string
	// IterationCount is the current iteration number.
	IterationCount int
}

ReflectionRequest contains the inputs for a reflection step.

type ReflectionResult

type ReflectionResult struct {
	// Monologue is the internal justification produced by the reflector.
	Monologue string
	// ShouldProceed indicates whether the action should continue.
	ShouldProceed bool
}

ReflectionResult captures the outcome of a reflection step.

type StageConfig

type StageConfig struct {
	// Name is the display name shown in the TUI progress bar (e.g., "Understanding").
	Name string
	// Instruction is appended to the goal for this stage (optional).
	// Example: "Read relevant files and gather context before acting."
	Instruction string
	// TaskType selects the LLM model for this stage via ModelProvider.GetModel().
	// If empty, the expert's default (TaskPlanning) is used.
	// Example: modelprovider.TaskToolCalling for execution-heavy stages.
	TaskType modelprovider.TaskType
}

StageConfig defines a named stage in a multi-stage ReAcTree. Each stage becomes an agent node in a sequence graph. The stage instruction is appended to the goal to guide the LLM's focus for that stage.

type Toggles

type Toggles struct {
	EnableCriticMiddleware bool `mapstructure:"enable_critic_middleware"`
	EnableActionReflection bool `mapstructure:"enable_action_reflection"`
	EnableDryRunSimulation bool `mapstructure:"enable_dry_run_simulation"`
	EnableMCPServerAccess  bool `mapstructure:"enable_mcp_server_access"`
	EnableAuditDashboard   bool `mapstructure:"enable_audit_dashboard"`

	// Reflector is the ActionReflector used for RAR loops.
	// Only used when EnableActionReflection is true.
	Reflector ActionReflector `json:"-"`

	// Hooks are lifecycle callbacks invoked at well-defined points during
	// tree execution. Multiple hooks can be composed via hooks.NewChainHook.
	// Hooks replace the previous AuditEmitter field — the AuditHook
	// implementation provides the same audit-logging behavior.
	Hooks hooks.ExecutionHook `json:"-"`
}

Toggles configures optional predictability and bounding mechanisms. All fields default to zero values (disabled). Callers opt in by setting booleans and injecting the corresponding dependency.

type TreeConfig

type TreeConfig struct {
	// MaxDepth limits how deep the tree can grow through recursive expansion.
	MaxDepth int

	// MaxDecisionsPerNode limits how many LLM calls a single agent node can make.
	MaxDecisionsPerNode int

	// MaxTotalNodes limits the total number of nodes in the tree.
	MaxTotalNodes int

	// Stages defines the named stages for multi-stage execution.
	// If empty, a single root node is built (backward compatible).
	// If set, a sequence graph is built with one agent node per stage.
	// Deprecated: prefer MaxIterations for adaptive loop execution.
	Stages []StageConfig

	// MaxIterations sets the maximum number of adaptive-loop iterations.
	// When > 0 (and Stages is empty), the executor runs a single agent node
	// in a loop that accumulates context and terminates when the LLM produces
	// zero tool calls or the iteration cap is reached. This replaces the fixed
	// stage pipeline with dynamic, task-driven expansion.
	MaxIterations int

	// ToolBudgets limits how many times specific tools can be called across
	// all iterations of the adaptive loop. When a tool's budget is exhausted,
	// it is removed from the tool list so the LLM is forced to proceed
	// without it. Example: {"ask_clarifying_question": 1} ensures the agent
	// asks at most one clarifying question before using defaults.
	ToolBudgets map[string]int

	// Toggles holds opt-in configurations for predictability, bounding,
	// and enterprise readiness.
	Toggles Toggles

	// Checkpointer is used to save and restore execution state.
	Checkpointer graph.CheckpointSaver `json:"-"`
}

TreeConfig holds configuration for a ReAcTree execution run. These limits prevent runaway tree growth and unbounded LLM calls.

func DefaultTreeConfig

func DefaultTreeConfig() TreeConfig

DefaultTreeConfig returns sensible defaults for tree execution.

type TreeExecutor

type TreeExecutor interface {
	// Run executes a ReAcTree for the given goal and returns the result.
	Run(ctx context.Context, req TreeRequest) (TreeResult, error)
}

TreeExecutor orchestrates a full ReAcTree run from a top-level goal. It builds a graph.StateGraph, compiles it, creates a graph.Executor, and runs it — delegating all orchestration to trpc-agent-go's graph package.

func NewTreeExecutor

func NewTreeExecutor(
	exp expert.Expert,
	workingMem *memory.WorkingMemory,
	episodic memory.EpisodicMemory,
	config TreeConfig,
) TreeExecutor

NewTreeExecutor creates a TreeExecutor configured with the given expert and options. The expert is used as the LLM backend for all agent nodes in the tree.

type TreeRequest

type TreeRequest struct {
	Goal     string
	Tools    []tool.Tool
	TaskType modelprovider.TaskType
	// Attachments are file/media attachments from the incoming message.
	// Image attachments are passed as multimodal content to the LLM.
	Attachments []messenger.Attachment

	// WorkingMemory overrides the tree-level working memory for this request.
	// When set, enables per-sender memory isolation. If nil, falls back to
	// the tree's shared working memory.
	WorkingMemory *memory.WorkingMemory

	// EpisodicMemory overrides the tree-level episodic memory for this request.
	// When set, enables per-sender episode isolation. If nil, falls back to
	// the tree's shared episodic memory.
	EpisodicMemory memory.EpisodicMemory
}

TreeRequest contains all inputs for a single tree execution.

type TreeResult

type TreeResult struct {
	Status    NodeStatus
	Output    string
	NodeCount int
}

TreeResult captures the outcome of a complete ReAcTree execution run.

type ValidatingToolWrapper

type ValidatingToolWrapper struct {
	tool.Tool
	// contains filtered or unexported fields
}

ValidatingToolWrapper wraps a tool.Tool with an ActionValidator.

func (*ValidatingToolWrapper) Call

func (v *ValidatingToolWrapper) Call(ctx context.Context, jsonArgs []byte) (any, error)

Call wraps the tool Call method by running the validator first.

func (*ValidatingToolWrapper) StreamableCall

func (v *ValidatingToolWrapper) StreamableCall(ctx context.Context, jsonArgs []byte) (*tool.StreamReader, error)

StreamableCall wraps the tool StreamableCall method by running the validator first.

Directories

Path Synopsis
memoryfakes
Code generated by counterfeiter.
Code generated by counterfeiter.
Code generated by counterfeiter.
Code generated by counterfeiter.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL