Files
ragflow/internal/harness/README.md
Yingfeng dd20561fca Feat: add event sourcing and replay to harness (#16326)
### Motivation

This PR evolves the harness from a pure execution runtime into an
**observable, replayable agent evaluation platform**. The current
`harness/graph` checkpoint mechanism is insufficient for true
event-sourced introspection—we need append-only event logs capturing
every tool call, state transition, memory write, and approval decision,
enabling deterministic replay, fork/diff, postmortem analysis, and
time-travel debugging.

### Key Design Goals

1. **Event-Sourced Execution Model**  
Replace coarse checkpoints with granular, append-only event logs. Every
operation becomes a durable event: tool invocation, state mutation,
memory update, human approval. This unlocks deterministic replay,
branching execution histories, and regression datasets derived directly
from production failures.

2. **First-Class Replay & Evaluation Loop**  
Replay is not an afterthought—it is a core primitive. A single live run
seeds an offline corpus that supports: repeated playback, model
substitution, tool result mocking, and strategy comparison. The harness
graduates from "executor" to "continuous evaluation platform" where
failed production traces convert directly into offline regression
suites.

3. **Operational Observability**  
   Beyond raw traces, expose metrics that prove stability over time:
   - Tool success / failure rates
   - Approval latency distributions
   - Retry frequencies
   - Checkpoint restore reliability
   - Memory retrieval quality
   - Cost per completed task
   - Fork replay pass rates

The underlying thesis: the bottleneck for most agent systems is not
execution capability, but the inability to **demonstrate continuous,
measurable improvement**.


### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-07-06 23:31:54 +08:00

1167 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Agent Harness Go
[![Go Reference](https://pkg.go.dev/badge/ragflow/internal/harness.svg)](https://pkg.go.dev/ragflow/internal/harness)
[![Go Report Card](https://goreportcard.com/badge/ragflow/internal/harness)](https://goreportcard.com/report/ragflow/internal/harness)
A Go framework for building **stateful, multi-agent applications** with LLMs. It provides a **graph-based execution engine** (`graphengine/`) with Pregel-style BSP execution, plus a **full Agent Development Kit** (`agentcore/`) built on top of it — supporting ReAct agents, middleware, workflows, checkpointing, human-in-the-loop, and streaming.
---
- [Quick Start](#quick-start)
- [Architecture Overview](#architecture-overview)
- [Layer 1: Graph Engine (graphengine)](#layer-1-graph-engine-graphengine)
- [Layer 2: Agent Development Kit (agentcore)](#layer-2-agent-development-kit-agentcore)
- [Layer 3: Push-Based AgentLoop](#layer-3-push-based-agentloop)
- [Checkpoint & Resume](#checkpoint--resume)
- [Interrupts (Human-in-the-Loop)](#interrupts-human-in-the-loop)
- [Cancellation System](#cancellation-system)
- [Prebuilt Components](#prebuilt-components)
- [Observability (OpenTelemetry)](#observability-opentelemetry)
- [Event Sourcing & Replay](#event-sourcing--replay)
- [Project Structure](#project-structure)
- [Examples](#examples)
- [Contributing](#contributing)
- [License](#license)
---
## Quick Start
### Minimal StateGraph
```go
package main
import (
"context"
"fmt"
"log"
"ragflow/internal/harness"
)
type State struct {
Messages []string
Counter int
}
func main() {
ctx := context.Background()
// 1. Create a graph builder
builder := harness.NewStateGraph(State{})
// 2. Add nodes (functions that read/write shared state)
builder.AddNode("agent", func(ctx context.Context, state interface{}) (interface{}, error) {
s := state.(State)
s.Messages = append(s.Messages, "Hello from agent")
s.Counter++
return s, nil
})
// 3. Add edges (define execution order)
builder.AddEdge(harness.Start, "agent")
builder.AddEdge("agent", harness.End)
// 4. Compile the graph (validates structure)
graph, err := builder.Compile()
if err != nil {
log.Fatal(err)
}
// 5. Run the graph
result, err := graph.Invoke(ctx, State{
Messages: []string{"Starting..."},
Counter: 0,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Result: %+v\n", result)
}
```
### Minimal ReAct Agent
```go
package main
import (
"context"
"ragflow/internal/harness/core"
"ragflow/internal/harness/core/schema"
)
func main() {
model := myChatModel{} // implements agentcore.Model[*schema.Message]
agent := agentcore.NewReActAgent(&agentcore.ReActConfig[*schema.Message]{
Model: model,
Tools: []agentcore.Tool{&myTool{}},
Instruction: "You are a helpful assistant.",
}).WithName("my_agent")
runner := agentcore.NewTypedRunner(agentcore.RunnerConfig[*schema.Message]{Agent: agent})
iter := runner.Run(context.Background(), []*schema.Message{
schema.UserMessage("Hello!"),
})
for {
ev, ok := iter.Next()
if !ok { break }
if ev.Err != nil { /* handle error */ }
if ev.Output != nil && ev.Output.MessageOutput != nil {
// consume output
}
}
}
```
---
## Architecture Overview
The framework is organized into three logical layers:
```
┌─────────────────────────────────────────────────────────────────┐
│ Layer 3: AgentLoop (push-based execution, preempt/stop) │
├─────────────────────────────────────────────────────────────────┤
│ Layer 2: AgentCore ADK (ReActAgent, Runner, Middleware, Tools) │
│ ├─ ReActAgent with iterate-loop or graph-backed exec │
│ ├─ Middleware system (5 hooks + ToolContributor) │
│ ├─ Tool system (standard + enhanced + tool_registry) │
│ │ └─ ToolInvokeMiddleware (onion model: timeout, │
│ │ retry, fallback, approval, rate-limit) │
│ ├─ flowAgent (sub-agent management, transfer routing) │
│ └─ workflowAgent (Sequential / Parallel / Loop) │
├─────────────────────────────────────────────────────────────────┤
│ Layer 1: Graph Engine (graphengine) │
│ ├─ StateGraph builder (nodes, edges, channels) │
│ ├─ Pregel BSP execution engine (superstep loop) │
│ ├─ Channels (LastValue, Topic, BinOp, etc.) │
│ ├─ Checkpoint (MemorySaver, SqliteSaver, Postgres) │
│ └─ Prebuilt components (ToolNode, ConditionalNode...) │
└─────────────────────────────────────────────────────────────────┘
```
**Layer 1** (`graphengine/`) is the foundation — a general-purpose stateful graph execution engine using the [Pregel](https://research.google.com/pubs/pub37252.html) BSP model. It is **LLM-agnostic** and can run any kind of stateful computation.
**Layer 2** (`agentcore/`) builds on the engine to provide an Agent Development Kit: ReAct agents, middleware chains, tool abstraction, workflow orchestration, sub-agent management, and the Runner entry point.
**Layer 3** (`agentcore/agent_loop.go`) provides push-based agent execution for chat/streaming applications, with preempt/stop controllers and turn lifecycle management.
---
## Layer 1: Graph Engine (graphengine)
### Core Concepts
The engine follows a **Builder → Compile → Execute** pattern:
1. **Build Phase**: Define nodes, edges, and state channels via `StateGraph`
2. **Compile Phase**: Validate the graph (reachability, schema, cycles)
3. **Execution Phase**: Run the Pregel superstep loop
### StateGraph
`graph.StateGraph` is the main builder. Nodes communicate by reading/writing a **shared state object** (a struct or map).
```go
builder := harness.NewStateGraph(MyState{})
```
### Nodes
Nodes are functions `func(ctx, state) (updatedState, error)`:
```go
builder.AddNode("my_node", func(ctx context.Context, state interface{}) (interface{}, error) {
s := state.(MyState)
// read/write state...
return s, nil
})
```
Options (retry, tags, triggers, field mappings):
```go
builder.AddNodeWithOptions("risky_node", nodeFunc, harness.NodeOptions{
RetryPolicy: &harness.RetryPolicy{
MaxAttempts: 3,
InitialInterval: 500 * time.Millisecond,
BackoffFactor: 2.0,
},
})
```
### Edges
Edges define the control flow between nodes:
```go
// Simple edge
builder.AddEdge("node_a", "node_b")
// Conditional edges (routing based on condition function)
builder.AddConditionalEdges("router", func(ctx context.Context, state interface{}) (interface{}, error) {
s := state.(MyState)
if s.Value > threshold { return "high", nil }
return "low", nil
}, map[string]string{
"high": "high_value_node",
"low": "low_value_node",
})
// Branches (multi-way fan-out)
builder.AddBranch("router", conditionFunc, thenFunc)
```
**Data edges** provide field-level data routing without affecting execution order:
```go
builder.AddDataEdge("node_a", "node_b",
harness.NewFieldMapping("result", "input"),
)
```
### Node Trigger Modes
| Mode | Description | Best For |
|---|---|---|
| `AnyPredecessor` (default) | Node triggers when **any** predecessor completes | BSP-style graphs with cycles/loops |
| `AllPredecessor` | Node triggers when **all** predecessors complete | DAG-style fan-in/aggregation |
```go
builder.WithNodeTriggerMode(harness.AllPredecessor)
```
`AnyPredecessor` supports cyclic graphs; `AllPredecessor` does not.
### Channels
Channels define **how state is stored and updated**. Every state field maps to a channel:
| Channel | Semantics | Use Case |
|---|---|---|
| `LastValue` (default) | Keeps only the last value written | Ordinary state fields |
| `AnyValue` | Accepts multiple writes, keeps last | Similar to LastValue but relaxed |
| `Topic` | PubSub mode; `accumulate` flag controls clearing | Message queues, event buses |
| `BinaryOperatorAggregate` | Reduces values via binary operator (add, append, merge) | Numerical counters, list accumulation |
| `ReducerChannel` | Decorator wrapping any channel with a reducer | Custom reduction logic |
| `NamedBarrierValue` | Waits for named nodes to write before readable | Synchronization barriers |
| `EphemeralValue` | Auto-clears after first read | One-shot signals |
| `UntrackedValue` | Not checkpointed | Temporary computation caches |
```go
builder.AddChannel("messages", harness.NewTopic(string, true)) // accumulating topic
builder.AddChannel("counter", harness.NewBinaryOperatorAggregate(0, harness.IntAdd))
```
Built-in binary operators: `IntAdd`, `ListAppend`, `StringConcat`, `MergeReducer`, `AddMessagesReducer`.
**Schema annotations** via struct tags:
```go
type State struct {
Messages []string `harness:"reducer=append"`
Counter int `harness:"reducer=add"`
}
```
### Compile
The `Compile` step validates the graph and produces an executable `CompiledGraph`:
```go
graph, err := builder.Compile(
harness.WithCheckpointer(saver), // enable persistence
harness.WithInterrupts("review"), // set interrupt points
harness.WithRecursionLimit(25), // max supersteps
harness.WithDebug(true), // enable debug logging
)
```
### Execution
```go
// Synchronous
result, err := graph.Invoke(ctx, initialState, config)
// Streaming (event-driven)
stream := graph.Stream(ctx, initialState, config, types.StreamModeUpdates)
for event := range stream {
// handle checkpoint, task_start, task_end, update, values, interrupt, error, final
}
```
### Pregel Engine Internals
The `pregel.Engine` runs the BSP superstep loop:
```
Input Application → Checkpoint Restore (if any)
└─> Superstep Loop:
├─ prepareNextTasks (which nodes are ready?)
├─ shouldInterrupt (check for interrupt points)
├─ executeTasksAsync (concurrent via AsyncPipeline)
│ └─ each task: read channels → run node fn → return output
├─ applyWrites (merge outputs into channels)
├─ checkpoint (save state)
├─ stream events (via StreamManager)
└─ repeat until: no more tasks, recursion limit, interrupt, or cancel
```
**Concurrency model**: `AsyncExecutor` uses a semaphore-based goroutine pool with configurable `maxConcurrency`. Nodes that are independent (no data/control dependencies) execute in parallel.
**Stream events**: checkpoint, task_start, task_end, update, values, interrupt, error, final, debug — filtered by `StreamMode`.
### Checkpointer Interface
```go
type BaseCheckpointer interface {
Get(ctx, config) (Checkpoint, error)
Put(ctx, config, Checkpoint) error
List(ctx, config, limit) ([]Checkpoint, error)
}
```
**Implementations**:
| Saver | When to Use |
|---|---|
| `MemorySaver` | In-memory, for testing or single-instance |
| `SqliteSaver` | File-based persistence via SQLite |
| `PostgresSaver` | Production, multi-instance with shared DB |
### Errors
| Error | Cause |
|---|---|
| `GraphRecursionError` | Exceeded recursion limit |
| `GraphInterrupt` | Graph paused for human intervention |
| `InvalidUpdateError` | Channel wrote invalid state |
| `NodeNotFoundError` / `EdgeNotFoundError` | Graph validation failure |
---
## Layer 2: Agent Development Kit (agentcore)
### Architecture
AgentCore provides high-level Agent abstractions on top of the graph engine:
```
Agent interface (Run/Resume)
└─ ReActAgent (ReAct loop with tool execution)
├─ Uses ToolsNode or executeInlineTools for tool dispatch
├─ Middleware chain (BeforeAgent → BeforeModel → AfterModel → AfterAgent)
├─ Model wrapper chain (EventSender → Retry → Failover → StateWrapper)
└─ Supports both standard for-loop and graph-backed execution
└─ flowAgent (sub-agent management & transfer routing)
└─ workflowAgent (Sequential / Parallel / Loop orchestration)
└─ Runner (entry point: Run/Resume/Query)
```
### Agent Interface
All agents implement `TypedAgent[M]` (M = `*schema.Message` or `*schema.AgenticMessage`):
```go
type TypedAgent[M any] interface {
Name(ctx context.Context) string
Description(ctx context.Context) string
Run(ctx context.Context, input *TypedAgentInput[M], opts ...RunOption) *AsyncIterator[*TypedAgentEvent[M]]
Resume(ctx context.Context, info *ResumeInfo, opts ...RunOption) *AsyncIterator[*TypedAgentEvent[M]]
}
```
### ReActAgent
Builds a ReAct (Reasoning + Acting) loop:
**Simple for-loop** (default): `buildReActRunFunc` runs the loop inline.
```
BeforeAgent (middleware)
└─> Loop (RemainingIterations > 0):
├─ BeforeModelRewrite (middleware)
├─ StateModifier (optional)
├─ GenModelInput (build input messages)
├─ model.Generate (with wrapper chain)
├─ AfterModelRewrite (middleware)
├─ extractToolCalls
├─ if tool calls → ToolsNode.Execute (or executeInlineTools)
└─ if no tool calls → break
AfterAgent (middleware)
```
**Graph-backed** (`GraphReAct=true`): each iteration becomes a StateGraph node, enabling automatic checkpoint at every step.
```go
cfg := &agentcore.ReActConfig[*schema.Message]{Model: model}
cfg.GraphReAct = true
cfg.GraphReActCheckpointer = checkpoint.NewMemorySaver()
```
The model wrapper chain layers on top of the base model:
```
base Model
→ EventSender (emits model output events)
→ Retry (backoff + ShouldRetry)
→ Failover (backup models)
→ User Middleware.WrapModel (custom)
→ StateWrapper (deep copy + ID injection + cancel check)
→ Callback Injection (tracing/monitoring)
```
### Tools
**Standard Tool** (string I/O):
```go
type WeatherTool struct{}
func (t *WeatherTool) Name() string { return "get_weather" }
func (t *WeatherTool) Description() string { return "Get weather for a city" }
func (t *WeatherTool) Invoke(ctx, args string, opts...) (string, error)
func (t *WeatherTool) Stream(ctx, args string, opts...) (*schema.StreamReader[string], error)
```
**Enhanced Tool** (structured I/O via `*schema.ToolResult`):
```go
type WeatherTool struct{}
// EnhancedTool embeds Tool + adds:
func (t *WeatherTool) EnhancedInvoke(ctx, args *schema.ToolArgument, opts...) (*schema.ToolResult, error)
func (t *WeatherTool) EnhancedStream(ctx, args *schema.ToolArgument, opts...) (*schema.StreamReader[*schema.ToolResult], error)
```
**Reflective Tool** (from any struct-typed function):
```go
type WeatherArgs struct {
City string `json:"city" description:"The city name"`
}
tool, _ := harness.ReflectTool("get_weather", "Get current weather",
func(ctx context.Context, args *WeatherArgs) (string, error) { ... })
```
**Tool Invocation Middleware Chain** (`ToolInvokeMiddleware`):
```go
tool := ToolWrapperChain(
ToolToInvokeFn(myTool),
NewTimeoutToolMiddleware(5*time.Second),
NewRetryToolMiddleware(&ToolRetryConfig{MaxAttempts: 3}),
NewFallbackToolMiddleware(fallbackFn),
)
```
Built-in wrappers: **Timeout**, **Retry** (exponential backoff), **Fallback**, **Approval** (human-in-the-loop for tool calls).
**ToolRegistry** — centralized tool management with aliases, categories, filtering, and merge:
```go
registry := agentcore.NewToolRegistry()
registry.Register(myTool, agentcore.WithAlias("weather"), agentcore.WithCategory("search"))
tool := registry.Lookup("get_weather")
searchTools := registry.LookupByCategory("search")
```
**LoopGuard** — detects repeated tool calls with identical arguments:
```go
ToolsConfig: &agentcore.ToolsNodeConfig{
Tools: tools,
LoopGuard: agentcore.NewLoopGuard(maxSame=2, maxFails=3),
}
```
### Middleware System
**TypedReActMiddleware[M]** — the core middleware interface with 5 hook points:
| Hook | Signature | Purpose |
|---|---|---|
| `BeforeAgent` | `(ctx, *ReActAgentContext)` | Modify instruction, tools, return-directly map |
| `BeforeModelRewrite` | `(ctx, state, *ModelContext)` | Transform state before model call |
| `AfterModelRewrite` | `(ctx, state, *ModelContext)` | Transform state after model call |
| `AfterAgent` | `(ctx, state)` | Post-execution cleanup |
| `WrapModel` | `(ctx, Model[M], *ModelContext)` → Model[M] | Decorate the model call |
**ToolContributor[M]** — an **optional interface** that middlewares can implement to contribute tools to the agent. The agent loop automatically collects contributed tools during build, eliminating the need for manual `BindToConfig` calls:
| Method | Purpose |
|---|---|
| `ContributeTools(ctx) []Tool` | Returns tools to add to the agent's tool set |
| `ContributeToolInfos(ctx) []*schema.ToolInfo` | Returns structured ToolInfo entries to bind to the model |
| `ContributeReturnDirectly(ctx) map[string]bool` | Returns tool names that cause the agent to exit immediately |
Tool contribution is automatically merged with `config.Tools`. Middleware that contributed tools will have them available in the `ReActAgentContext.Tools` during `BeforeAgent` for inspection.
**ToolInvokeMiddleware** (onion model) — a separate orthogonal system for tool-call level cross-cutting concerns (see [Tools section](#tools)).
Embed `BaseMiddleware[*schema.Message]` and override only needed hooks:
```go
type LoggingMiddleware struct {
agentcore.BaseMiddleware[*schema.Message]
}
func (m *LoggingMiddleware) BeforeModelRewrite(ctx, state, mc) (context.Context, *agentcore.ReActAgentState, error) {
log.Printf("model input: %d messages", len(state.Messages))
return ctx, state, nil
}
```
For middlewares that contribute tools, implement `ToolContributor` and optionally override `BeforeAgent` for non-tool configuration:
```go
type FilesystemMiddleware struct {
agentcore.BaseMiddleware[*schema.Message]
backend Backend
}
// Tools are auto-collected during agent build.
func (m *FilesystemMiddleware) ContributeTools(ctx context.Context) []core.Tool {
return m.buildTools()
}
// BeforeAgent is still available for modifying instruction etc.
func (m *FilesystemMiddleware) BeforeAgent(ctx context.Context, rc *core.ReActAgentContext) (context.Context, *core.ReActAgentContext, error) {
// Non-tool modifications go here
return ctx, rc, nil
}
```
**Prebuilt middlewares** (in `agentcore/middlewares/`):
| Middleware | Tool Contribution | Purpose |
|---|---|---|
| `filesystem` | `ContributeTools` | Provides read/write/edit/ls/grep/execute tools |
| `skill` | `ContributeTools` (Fork mode) + `BeforeAgent` (Inline mode) | Loads and executes skills from SKILL.md files |
| `dynamictool/toolsearch` | `ContributeTools` + `ContributeToolInfos` | Dynamic tool search for large tool libraries |
| `subagent` | `ContributeTools` (or `Init` for inheritance) | Injects sub-agents as callable tools |
| `summarization` | — | Auto-compresses long conversation history |
| `reduction` | — | Truncates large tool results |
| `patchtoolcalls` | — | Fixes dangling tool calls in message history |
| `plantask` | — | Task management CRUD for coding sessions |
| `telemetry` | — (uses `WrapModel`) | OpenTelemetry tracing/monitoring |
### Runner
Primary entry point for agent execution:
```go
runner := agentcore.NewTypedRunner(agentcore.RunnerConfig[*schema.Message]{
Agent: agent,
EnableStreaming: true,
CheckPointStore: store,
})
// Run
iter := runner.Run(ctx, []*schema.Message{schema.UserMessage("Hello")})
// Convenience
iter := runner.Query(ctx, "Hello")
// Resume from checkpoint
iter, err := runner.Resume(ctx, "checkpoint-id")
```
`Runner` wraps the agent with `flowAgent` for session management, transfer routing, and checkpoint/resume.
### Workflow Agents
**Sequential** — runs sub-agents one after another:
```go
wf, _ := agentcore.NewSequential(ctx, &agentcore.SequentialConfig{
Name: "pipeline", SubAgents: []agentcore.Agent{agentA, agentB},
})
```
**Parallel** — runs sub-agents concurrently with event isolation:
```go
wf, _ := agentcore.NewParallel(ctx, &agentcore.ParallelConfig{
Name: "collectors", SubAgents: []agentcore.Agent{agentC, agentD},
})
```
**Loop** — repeats a sequence of sub-agents up to MaxIterations:
```go
wf, _ := agentcore.NewLoop(ctx, &agentcore.LoopConfig{
Name: "reflection", SubAgents: []agentcore.Agent{mainAgent, critiqueAgent},
MaxIterations: 5,
})
```
### SubAgentMiddleware
Dynamically injects sub-agents as callable tools that the parent LLM can invoke via tool calls:
```
Parent Agent (ReActAgent)
├─ Tools: [..., researcher_AgentTool, coder_AgentTool] ← auto-injected via ToolContributor
├─ Middlewares: [SubAgentMiddleware, ...]
└─ Tool dispatch: ToolsNode (merged config + contributed tools)
When LLM calls "researcher":
└─ researcher_AgentTool.Invoke(ctx, args)
└─ Runner.Run(runCtx_with_depth_1)
└─ Researcher Agent (independent ReAct loop)
```
Sub-agent tools are automatically contributed via the `ToolContributor` interface. No manual `BindToConfig` call is needed for basic usage:
```go
saMW := subagent.New(specs, &subagent.Config{
EmitInternalEvents: true,
MaxDepth: 5,
})
// Just add to Middlewares — tools are auto-collected.
cfg := &agentcore.ReActConfig[*schema.Message]{
Model: parentModel,
Middlewares: []agentcore.ReActMiddleware{saMW},
}
agent := agentcore.NewReActAgent(cfg)
```
Three ways to declare sub-agents:
```go
// 1. Pre-built Agent
spec := SubAgentSpec{
Name: "researcher", Description: "Research",
Agent: agentcore.NewReActAgent(cfg).WithName("researcher"),
}
// 2. Declarative AgentConfig (recommended)
spec := SubAgentSpec{
Name: "researcher", Description: "Research",
AgentConfig: &AgentConfig{
Model: claudeModel,
Tools: []agentcore.Tool{searchTool},
SystemPrompt: "You are a research assistant.",
Middlewares: []agentcore.ReActMiddleware{ownMiddleware},
},
InheritParentMiddlewares: true, // inherits parent's non-subagent middlewares
ExcludedParentMiddlewareNames: []string{
"*filesystem.middleware[*schema.Message]",
},
}
// 3. AgentFactory (legacy)
spec := SubAgentSpec{
Name: "researcher", Description: "Research",
AgentFactory: func(ctx context.Context) (agentcore.Agent, error) {
return agentcore.NewReActAgent(cfg).WithName("researcher"), nil
},
}
```
**Recursion depth guard**`Config.MaxDepth` limits nesting. Depth propagated via context:
```go
mw := subagent.New(specs, &subagent.Config{
EmitInternalEvents: true, // forward sub-agent events to parent stream
MaxDepth: 3, // allow parent→child→grandchild, block deeper
})
```
**Middleware inheritance** — when a spec has `InheritParentMiddlewares: true`, call `Init(ctx, config)` after adding the middleware to `config.Middlewares`:
```go
saMW := subagent.New(specs, &subagent.Config{MaxDepth: 5})
cfg.Middlewares = append(cfg.Middlewares, saMW)
saMW.Init(ctx, cfg) // enables middleware inheritance for sub-agents
agent := agentcore.NewReActAgent(cfg)
```
> **Migration note**: The legacy `BindToConfig` method is deprecated and retained for backward compatibility. New code should use the `ToolContributor` interface (automatic) or call `Init(ctx, config)` when middleware inheritance is needed.
### Sub-Agent Architecture: flowAgent vs SubAgentMiddleware
| Aspect | flowAgent (deterministic) | SubAgentMiddleware (LLM-driven) |
|---|---|---|
| Invocation | Code-driven via `TransferToAgent` action | LLM-driven via tool call |
| Control flow | Pre-registered via `SetSubAgents()`, routed by `runLoop` | LLM decides when to invoke |
| Execution context | Shares parent session, events accumulated | Independent Runner, no session sharing |
| Middleware inheritance | N/A | Optional via `InheritParentMiddlewares` |
| Orchestration | Sequential/Parallel/Loop (workflowAgent) | LLM decides sequencing |
| Best for | Predictable multi-step pipelines | Dynamic task decomposition by LLM |
Both can be combined: a workflowAgent step can use SubAgentMiddleware for dynamic sub-agent delegation within a structured pipeline.
### Event System
**AsyncIterator / AsyncGenerator** — async pull/push event mechanism.
**Event types**: Model output events, tool result events, error events, action events (interrupt, transfer, exit, break-loop).
**Event constructors**: `ToolInvokeEvent`, `ToolStreamEvent`, `EnhancedToolInvokeEvent`, `EnhancedToolStreamEvent` (preserve `Extra` metadata for multimodal).
---
## Layer 3: Push-Based AgentLoop
`AgentLoop` enables push-based agent interaction where external events can be injected while the agent is running — designed for chat/streaming applications.
**Lifecycle:**
```
idle ──beginPlanningTurn──▶ planning ──beginActiveTurn──▶ active ──endActiveTurn──▶ idle
│ ▲
└────────abortPlanningTurn─────────────────────────────┘
```
**Key components:**
- **preemptController** — turn-targeted preempt with snapshot/ack mechanism
- **stopController** — global terminal stop with optional active-turn cancel
- **bridgeStore** — bridges AgentLoop checkpoints with Runner checkpoints
- **TurnContext** — per-turn Preempted/Stopped channels, StopCause
- **Callbacks**: `GenInput`, `GenResume`, `PrepareAgent`, `OnAgentEvents`
**Push options:** `WithPreempt`, `WithPreemptTimeout`, `WithPreemptDelay`
**Stop options:** `WithGraceful`, `WithImmediate`, `WithGracefulTimeout`, `UntilIdleFor`, `WithSkipCheckpoint`, `WithStopCause`
---
## Checkpoint & Resume
Checkpoints are serialized via gob encoding with type registration (`schema.RegisterType`).
**Checkpoint payload** includes: run context (run path, session values), interrupt info (state data, interrupt signal), agent state (`*ReActAgentState`).
**Resume flow:**
1. `Runner.Resume` → loads checkpoint from store
2. Reconstructs run context from checkpoint data
3. Calls `ResumableAgent.Resume` with `ResumeInfo`
4. `ReActAgent.Resume` restores state from `InterruptState` and re-enters run function
5. `ReActAgentResumeData.HistoryModifier` allows input modification on resume
**Gob encodability check** proactively validates values at `SetRunLocalValue` time, catching unregistered types early.
```go
store := &myCheckpointStore{}
// Run with checkpoint ID
iter := runner.Run(ctx, msgs, agentcore.WithCheckPointID("run-001"))
// Resume from checkpoint
iter, err := runner.Resume(ctx, "run-001")
```
The **graph engine** side provides `Durability` modes: `Sync` (blocking after each superstep), `Async` (non-blocking), `Exit` (only on graph exit). The `CheckpointManager` uses optimistic locking for version conflict detection.
---
## Interrupts (Human-in-the-Loop)
**Graph-level interrupts** pause execution at specified nodes:
```go
graph, err := builder.Compile(harness.WithInterrupts("human_review"))
```
Inside a node:
```go
func humanReviewNode(ctx context.Context, state interface{}) (interface{}, error) {
result, err := harness.InterruptFunc("Please review and approve")
if err != nil {
return nil, err
}
return processResult(result), nil
}
```
**Resume with command:**
```go
result, err := graph.Invoke(ctx, harness.NewCommand().WithResume(approval), config)
```
**ReActGraph** (graph-backed agent): interrupt set at the `"execute_tools"` node for human-in-the-loop before tool execution. With Checkpointer, each node transition saves a checkpoint automatically.
```go
cfg := &agentcore.ReActConfig[*schema.Message]{Model: model}
cfg.GraphReAct = true
cfg.GraphReActInterruptBefore = []string{"execute_tools"}
```
---
## Cancellation System
Three cancel modes:
| Mode | Behavior |
|---|---|
| `CancelImmediate` | Stop immediately |
| `CancelAfterChatModel` | Stop after current model call completes |
| `CancelAfterToolCalls` | Stop after current tool calls complete |
**State machine:** `cancelContext` transitions `Running → Cancelling → Done/Handled`.
**Key features:**
- Children derive from parents with configurable recursive propagation
- `deriveAgentToolCancelContext` — creates child cancel context for nested agent tools
- `timeoutEscalation` — timeout triggers escalation from graceful to immediate
- `cancelMonitoredToolHandler` / `cancelMonitoredModel` — check cancel state before dispatch
- `InterruptFromGraph` — coordinates graph-level interrupts with the cancel state machine
```go
opt, cancel := agentcore.WithCancel()
defer cancel(agentcore.WithCancelMode(agentcore.CancelAfterChatModel))
iter := runner.Run(ctx, msgs, opt)
// Later, to cancel:
handle, ok := cancel(agentcore.WithCancelMode(agentcore.CancelImmediate))
if ok { handle.Wait() }
```
**Error types:** `CancelError` (with `AgentCancelInfo`), `StreamCanceledError`, `ErrCancelTimeout`, `ErrExecutionEnded`.
---
## Prebuilt Components
Package `prebuilt/` provides ready-to-use ReAct state machine and node factories for the graph engine:
### ReAct Agent
```go
agent := prebuilt.NewReactAgent(&prebuilt.ReactAgentConfig{
Model: myLLM,
Tools: []prebuilt.Tool{myTool},
SystemPrompt: "You are a coding assistant.",
MaxIterations: 10,
StopCondition: func(state *ReActState) bool { return state.Iteration >= 5 },
})
```
The ReAct state machine runs: **Input → Model.Generate → ParseAction → (Answer: done | Tool: execute → loop)**
### Node Factories
- **`ToolNode(tool)`** — wraps a `Tool` as a graph node with standardized output format
- **`ValidationNode(func, errorMessage)`** — input validation, passes through on success
- **`ConditionalNode(condition, branches, defaultBranch)`** — conditional routing node
- **`TransformNode(func)`** — pure data transformation node
---
## Observability (OpenTelemetry)
```go
// NOTE: telemetry package is not included in this internal copy.
// RAGFlow has its own observability setup in internal/observability/.
```
---
## Event Sourcing & Replay
The harness framework provides a **fourth layer** for event-driven agent introspection: append-only event logging, deterministic replay, and live metrics collection. All Layer 4 components integrate via the existing `CallbackManager`, requiring zero changes to Layers 13.
### Event Sourcing
An **append-only event log** records every granular action during agent execution as an immutable event — tool calls, state transitions, memory writes, approvals, LLM invocations, and checkpoint operations. Each event carries a monotonic logical clock, causal parent references, and a structured payload. This replaces a checkpoint-only approach with a full audit log that supports deterministic replay, forking, and postmortem analysis.
**Three event store backends** are available:
| Backend | Path | Use Case |
|---------|------|----------|
| `MemoryEventStore` | `events/memory.go` | In-memory, for testing/single-instance |
| `LocalFileEventStore` | `events/localfile.go` | File-based with segment rotation (by time or size) |
| `NATSEventStore` | `events/nats.go` | Production distributed via NATS JetStream |
### Replay Engine
The `ReplayEngine` replays a trace from the event log **deterministically**, supporting:
- **Model substitution** — replay with a different LLM while keeping tool results frozen
- **Tool result injection** — replace recorded tool outputs with live execution or synthetic data
- **Fork** — branch a new execution from any point in the event log
- **Diff** — compare two execution traces to detect regression or behavioral changes
### Observability Metrics
Automated metrics collection covers: tool success rate, approval latency, retry rate, checkpoint restore success, memory hit quality, cost per completed task, and fork replay pass rate. Metrics export to Prometheus.
### Evaluation Loop
A production trace can be automatically converted into a **regression dataset**. The `RunReplayEval` function replays each case with multiple model/strategy combinations, comparing results and raising regression alerts.
> **Detailed design, type definitions, and source-level examples** are documented in [harness.md](harness.md).
---
## Project Structure
```
harness-go/
├── agentcore/ # Agent Development Kit (Layer 2 + 3)
│ ├── react_agent.go # ReActAgent: ReAct loop, freeze, run/resume
│ ├── react_loop.go # ReAct for-loop implementation
│ ├── react_graph.go # Graph-based ReAct using StateGraph
│ ├── contracts.go # Middleware, Tool, Model interfaces
│ ├── tools_node.go # ToolsNode: tool dispatch, middleware chains
│ ├── tool_invoke.go # ToolInvocationContext, middleware wrappers
│ ├── tool_registry.go # ToolRegistry: aliases, categories, filtering
│ ├── tool_schema.go # Reflection-based ToolInfo generation
│ ├── event_sender.go # Event sender middlewares
│ ├── model_chain.go # Model wrapper chain builder
│ ├── state_wrapper.go # StateModelWrapper: deep copy, ID injection
│ ├── retry.go # Model retry with backoff
│ ├── failover.go # Model failover across backup models
│ ├── flow.go # flowAgent: sub-agent management, transfer
│ ├── workflow.go # workflowAgent: Sequential/Parallel/Loop
│ ├── runner.go # Runner: run/resume/query entry point
│ ├── agent_loop.go # AgentLoop: push-based execution
│ ├── cancel.go # Cancel state machine, cancel modes
│ ├── callback.go # Callback handler, gob encodability check
│ ├── session.go # Session, BranchEvents, fork/join
│ ├── agent_handoff.go # Deterministic transfer, message ID utils
│ ├── turn_buffer.go # AgentLoop buffer implementation
│ ├── config.go # Agent option types
│ ├── interrupt.go # Interrupt types and signals
│ ├── resume_data.go # Resume data types
│ ├── utils.go # AsyncIterator, AsyncGenerator
│ ├── tool.go # AgentTool (sub-agent as Tool), depth guard
│ ├── tool_contributor.go # ToolContributor interface + collection helpers
│ ├── instruction.go # Instruction management
│ │
│ ├── backend/ # Filesystem backend abstraction
│ ├── evals/ # Eval framework (LLM-as-judge, scorers, replay-based eval)
│ ├── internal/ # Internal helpers (default system prompt)
│ ├── middlewares/ # 10 middleware implementations
│ │ ├── subagent/ # SubAgentMiddleware (LLM-driven delegation)
│ │ ├── summarization/ # Auto-summarization
│ │ ├── reduction/ # Tool output reduction
│ │ ├── filesystem/ # Filesystem tools
│ │ ├── skill/ # Skill loading
│ │ ├── patchtoolcalls/ # Dangling tool call fixer
│ │ ├── plantask/ # Task management
│ │ ├── agentsmd/ # Agents.md injection
│ │ ├── telemetry/ # OpenTelemetry tracing *(removed in internal copy)*
│ │ └── dynamictool/ # Dynamic tool registration
│ ├── prebuilt/ # Prebuilt agents (deep, supervisor, planexecute)
│ └── schema/ # Message, ToolCall, ToolResult, StreamReader
├── graphengine/ # Graph Engine (Layer 1)
│ ├── graph/ # StateGraph builder, CompiledGraph
│ │ ├── graph.go # Nodes, Edges, ConditionalEdges, Branches
│ │ ├── state.go # State schema validation, annotations
│ │ ├── message.go # MessageGraph, MessagesState
│ │ └── compiled.go # CompiledStateGraph, subgraph support
│ ├── channels/ # Channel implementations
│ │ ├── base.go # Channel interface, BaseChannel, Registry
│ │ ├── last_value.go # LastValue
│ │ ├── topic.go # Topic (PubSub)
│ │ ├── binop.go # BinaryOperatorAggregate
│ │ ├── reducer.go # ReducerChannel (decorator)
│ │ ├── barrier.go # NamedBarrierValue
│ │ └── ephemeral.go # EphemeralValue
│ ├── checkpoint/ # Checkpoint persistence
│ │ ├── memory.go # MemorySaver
│ │ ├── sqlite.go # SqliteSaver
│ │ └── postgres.go # PostgresSaver
│ ├── pregel/ # Pregel BSP execution engine
│ │ ├── engine.go # Engine: superstep loop, task scheduling
│ │ ├── async.go # AsyncExecutor, AsyncPipeline
│ │ ├── stream.go # StreamManager, StreamEvent types
│ │ ├── write.go # Channel writes
│ │ ├── read.go # Channel reads
│ │ ├── retry.go # Node retry
│ │ ├── subgraph.go # Subgraph execution
│ │ └── websocket.go # WebSocket streaming
│ ├── types/ # Core types
│ │ ├── types.go # NodeFunc, EdgeFunc, Interrupt, Command
│ │ ├── config.go # RunnableConfig
│ │ ├── stream.go # StreamProtocol, ChannelStream
│ │ └── scratchpad.go # Scratchpad storage
│ ├── constants/ # Reserved keys, virtual node names
│ ├── errors/ # Custom error types
│ ├── interrupt/ # Interrupt utilities
│ ├── runnable/ # Runnable abstraction layer
│ ├── task/ # Task decorators
│ ├── managed/ # Managed execution
│ ├── viemu/ # Visual emulation
│ └── visualization/ # DOT graph output
├── events/ # Event Sourcing (append-only event log)
│ ├── event.go # Event, EventID, EventType, typed payloads
│ ├── recorder.go # EventRecorder — GraphCallback → Event
│ ├── clock.go # LogicalClock (monotonic uint64)
│ ├── memory.go # MemoryEventStore
│ ├── localfile.go # LocalFileEventStore
│ └── nats.go # NATSEventStore
├── replay/ # Replay Engine
│ ├── replay.go # ReplayEngine — deterministic replay
│ ├── fork.go # Fork — branch from any event
│ ├── diff.go # Diff — compare two execution traces
│ └── injector.go # ModelOverride / ToolOverride strategies
├── metrics/ # Observability & Metrics
│ ├── metrics.go # MetricsCollector, autoMetricCollector
│ ├── aggregator.go # MetricsAggregator, MetricsWindow
│ └── exporter.go # PrometheusExporter
├── graphengine/ # Graph Engine (Layer 1)
│ ├── dataset.go # EventLog → 回归数据集转换
│ └── replay_eval.go # Replay-based evaluation
├── prebuilt/ # Prebuilt ReAct agent + node factories
│ ├── prebuilt.go # ReAct agent state machine
│ ├── tool_node.go # ToolNode factory
│ ├── validation_node.go # ValidationNode factory
│ ├── conditional_node.go # ConditionalNode factory
│ └── transform_node.go # TransformNode factory
├── server/ # HTTP server *(removed in internal copy)*
├── telemetry/ # OpenTelemetry integration *(removed in internal copy)*
├── harness.md # Event Sourcing & Replay design document
├── harness.go # Top-level re-exports and init()
├── harness_test.go # Integration tests
├── Makefile # Build, test, lint targets
└── examples/ # Example applications
├── workflow/ # Workflow examples (loop, sequential)
└── open-agent-builder/ # Web-based agent builder
```
---
## Examples
### StateGraph with Conditional Routing & Retry
```go
builder.AddConditionalEdges("router", func(ctx context.Context, state interface{}) (interface{}, error) {
s := state.(MyState)
if s.Value > threshold {
return "high", nil
}
return "low", nil
}, map[string]string{
"high": "high_value_node",
"low": "low_value_node",
})
builder.AddNodeWithOptions("risky_node", nodeFunc, harness.NodeOptions{
RetryPolicy: &harness.RetryPolicy{
MaxAttempts: 3,
InitialInterval: 500 * time.Millisecond,
BackoffFactor: 2.0,
},
})
```
### Agent with Middleware Stack
```go
import (
"ragflow/internal/harness/core"
"ragflow/internal/harness/core/middlewares/filesystem"
"ragflow/internal/harness/core/middlewares/summarization"
"ragflow/internal/harness/core/middlewares/subagent"
)
agent := agentcore.NewReActAgent(&agentcore.ReActConfig[*schema.Message]{
Model: model,
Middlewares: []agentcore.ReActMiddleware{
subAgentMW,
filesystem.New(&filesystem.Config{Backend: fsBackend}),
summarization.New(&summarization.Config{
TokenLimit: 100000,
Model: summaryModel,
}),
},
Instruction: "You are a coding assistant.",
})
```
### Agent with Sub-Agents
```go
spec := subagent.SubAgentSpec{
Name: "researcher",
Description: "Research a topic using web search",
AgentConfig: &subagent.AgentConfig{
Model: claudeModel,
Tools: []agentcore.Tool{webSearchTool},
SystemPrompt: "You are a research assistant.",
Middlewares: []agentcore.ReActMiddleware{ownMiddleware},
},
InheritParentMiddlewares: true,
}
saMW := subagent.New([]subagent.SubAgentSpec{spec}, &subagent.Config{
EmitInternalEvents: true,
MaxDepth: 5,
})
cfg := &agentcore.ReActConfig[*schema.Message]{
Model: parentModel,
Middlewares: []agentcore.ReActMiddleware{saMW, filesystem.New(...)},
}
saMW.Init(ctx, cfg) // enables middleware inheritance
agent := agentcore.NewReActAgent(cfg)
```
### Full loop example
See [examples/workflow/loop/](examples/workflow/loop/).
```go
wf, err := agentcore.NewLoop(ctx, &agentcore.LoopConfig{
Name: "reflection_agent",
SubAgents: []agentcore.Agent{mainAgent, critiqueAgent},
MaxIterations: 5,
})
runner := agentcore.NewTypedRunner(agentcore.RunnerConfig[*schema.Message]{Agent: wf})
iter := runner.Query(ctx, "briefly introduce multimodal embedding models")
```
### Custom Middleware
```go
type LoggingMiddleware struct {
agentcore.BaseMiddleware[*schema.Message]
}
func (m *LoggingMiddleware) BeforeModelRewrite(
ctx context.Context,
state *agentcore.ReActAgentState,
mc *agentcore.ModelContext,
) (context.Context, *agentcore.ReActAgentState, error) {
log.Printf("model input: %d messages", len(state.Messages))
return ctx, state, nil
}
func (m *LoggingMiddleware) AfterModelRewrite(
ctx context.Context,
state *agentcore.ReActAgentState,
mc *agentcore.ModelContext,
) (context.Context, *agentcore.ReActAgentState, error) {
log.Printf("model output: %d messages", len(state.Messages))
return ctx, state, nil
}
```
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## License
MIT License — see [LICENSE](LICENSE) for details.