agentloop
Status (verified 2026-09 against the source tree): no
agentlooppackage exists (internal/has no such directory, and nothing imports one). Live equivalents: the SDK agent run path (sdk.Agent.Run) uses the L2 session core — ReAct-era options (WithHumanInput,WithMaxIterations,WithToolDiscovery) no longer shape execution, andWithHumanInputreturnsErrHumanInputUnsupported. Runtime tool discovery lives ininternal/tools/toolsource(DiscoverToolsName = "discover_tools") withToolExpanderinsdk/discovery.go;HumanInputFuncandFriendlyErrremain insdk.internal/runtime/loop.gois an evolutionary round-loop plugin (LoopPlugin), not a ReAct engine. Source is authoritative; the body below is historical.
The internal/agentloop package (package agentloop) provided the ReAct
(reason+act) execution loop — the core iterative engine that drove an
agent’s reasoning and tool-use cycle. It was extracted from sdk.Agent.Run
into an independently testable unit with mock LLM, mock tools, mock events,
and mock memory.
The loop follows a strict iterate → generate → tool-call → feed-back pattern:
- Iterate: repeat until the LLM produces a final answer or a budget is exhausted.
- Generate: call the LLM with the accumulated message history and available tools.
- Tool-call: inspect the LLM response for tool calls; execute each via
the
ToolExecutor. - Feed-back: append tool results to the message history and loop back to step 2.
Responsibility
- Drive the ReAct loop with configurable budgets:
MaxIter(default 10),MaxTokens(per-iteration), andTimeout(total wall-clock budget). - Invoke the LLM via
LLMCaller.Generatewith the full message history, tool definitions, and iteration-specific token budget. - Execute tool calls via
ToolExecutor.Executesequentially, feeding each result back into the message history. - Support runtime tool discovery via the
ToolExpanderinterface: when the LLM calls the meta-tooldiscover_tools, the engine extracts the requested tool names, callsToolExpander.Expand, and injects the expanded tool definitions into the next LLM turn. - Support human-in-the-loop approval via
HumanInputFunc: before each tool call, the user can approve or reject it; the engine waits for the decision. - Persist structured memory via
MemorySink.AddMessageandstructuredMemorySinkfor full round-trip memory (session/role/content). - Emit every LLM call, tool event, and completion as structured events via
EventSink.Append. - Return a
Resultwith the final output, tool-call count, memory usage flag, token consumption, and wall-clock duration.
Architecture
flowchart TD
R["Request<br/>Messages, Tools, Budgets"] --> E["Engine.Run"]
E --> CHK{"MaxIter ≤ 0<br/>or Timeout?"}
CHK -- yes --> ER["Result<br/>Output, Tokens, Duration"]
CHK -- no --> LLM["LLMCaller.Generate<br/>Message history + Tools"]
LLM --> RESP{"Response:<br/>Tool calls?"}
RESP -- no, final answer --> ER
RESP -- yes --> TC["executeToolCalls<br/>For each tool call"]
TC --> HUM{"HumanInput<br/>approved?"}
HUM -- no, skip --> TC
HUM -- yes --> EXEC["ToolExecutor.Execute<br/>name + args"]
EXEC --> FB["Feed result back<br/>→ message history"]
FB --> CHK
TC --> DISCOVER{"discover_tools<br/>meta-tool?"}
DISCOVER -- yes --> EXP["expandDiscoveredTools<br/>ToolExpander.Expand → inject"]
EXP --> LLM
EMIT["EventSink.Append<br/>LLMCall / ToolEvent / TaskCompleted"] --> LLM
MEM["MemorySink.AddMessage<br/>structured memory"] --> LLM
EMIT --> ER
External interfaces
package agentloop
const DefaultMaxIterations = 10
const DiscoverToolsName = "discover_tools"
// --- Engine ---
type Engine struct {
LLM LLMCaller
Tools ToolExecutor
Events EventSink
Memory MemorySink
Tracer func(format string, args ...any)
MemEnabled bool
DistillEnabled bool
}
// --- Request / Result ---
type Request struct {
Messages []*llmcore.LLMMessage
Tools []llmcore.Tool
MaxIter int
MaxTokens int
Timeout time.Duration
AgentName string
SessionID string
Input string
HumanInput HumanInputFunc
ToolExpander ToolExpander
}
type Result struct {
Output string
ToolCalls int
MemoryUsed bool
InputTokens int
OutputTokens int
Duration time.Duration
}
// --- Interfaces ---
type HumanInputFunc func(ctx context.Context, toolName string, args map[string]any) (bool, error)
type LLMCaller interface {
Generate(ctx context.Context, req *llmcore.GenerateRequest) (*llmcore.GenerateResponse, error)
GetProvider() llmcore.LLMProvider
}
type ToolExecutor interface {
Execute(ctx context.Context, name string, args map[string]any) (tools.Result, error)
}
type ToolExpander interface {
Expand(ctx context.Context, names []string) ([]llmcore.Tool, error)
}
type EventSink interface {
Append(ctx context.Context, streamID string, events []*ares_events.Event, expectedVersion int64) error
}
type MemorySink interface {
AddMessage(ctx context.Context, sessionID, role, content string) error
}
// --- Main entry point ---
func (e *Engine) Run(ctx context.Context, req *Request) (*Result, error)
// --- Helper ---
func FriendlyErr(scope string, provider llmcore.LLMProvider, origErr error) error
Key types and methods
| Type / Method | Purpose |
|---|---|
Engine | Stateless ReAct loop engine. Fields: LLM, Tools, Events, Memory, Tracer, MemEnabled, DistillEnabled. Designed to be wired with mock implementations for independent testing. |
Run | The main entry point. Executes the ReAct loop: generate → tool-call → feed-back → repeat until final answer, budget exhaustion, or timeout. Returns Result with output, token counts, and duration. |
Request | Encapsulates the full input: message history, available tools, iteration/token/timeout budgets, agent/session identification, human-in-the-loop function, and runtime tool expander. |
Result | Structured output: final answer text, tool-call count, memory usage flag, input/output token counts, and wall-clock duration. |
LLMCaller | Generates LLM responses. Called once per iteration with the full message history and tool definitions. |
ToolExecutor | Executes a single tool call. Invoked per tool call in the LLM response. |
ToolExpander | Expands a set of requested tool names into full core.Tool definitions. Used by runtime tool discovery. |
EventSink | Persists structured events (LLM call, tool event, task completed) to durable storage with optimistic concurrency control (expectedVersion). |
MemorySink | Persists structured memory (session, role, content) for agent memory recall across turns. |
HumanInputFunc | Human-in-the-loop approval function. Invoked before each tool call; returns true to execute or false to skip. |
FriendlyErr | Wraps an LLM error with a human-readable scope identifier and provider name for observability. |
Module collaboration
agentloop->internal/llm(viaLLMCaller): generates LLM responses with the full message history and tool definitions.agentloop->internal/tools(viaToolExecutor): executes tool calls and feeds results back into the loop.agentloop->internal/ares_events(viaEventSink): emits structured events for every LLM call, tool execution, and task completion.agentloop->internal/runtime/memory(viaMemorySink): persists structured memory for agent recall across iterations.agentloop->internal/core(viallmcore.LLMMessage,llmcore.GenerateRequest,llmcore.GenerateResponse,core.Tool,llmcore.ToolCall,llmcore.LLMProvider): shares core model types.
Extension points
- Replace the LLM backend by implementing
LLMCallerwith any provider; the engine is provider-agnostic and only depends onllmcore.GenerateRequest/llmcore.GenerateResponse. - Add human-in-the-loop approval by providing a
HumanInputFunc; the engine pauses before each tool call and waits for the decision. - Enable runtime tool discovery by wiring a
ToolExpander; when the LLM callsdiscover_tools, the engine extracts the requested names, expands them, and injects the definitions into the next turn. - Instrument the loop via the
Tracerfield; every significant event (iteration, tool call, discovery) is traced. - Customise budgets per request:
MaxIter(default 10),MaxTokens(per-iteration), andTimeout(total wall-clock budget). Return aResultwith the final output, tool-call count, memory usage flag, token consumption, and wall-clock duration.
Bilingual status
This page is the English reference. A Chinese translation with identical structure and technical content is published as agentloop.zh.md. All code identifiers, type names, and signatures are kept in English in both files; only the prose differs.
Maturity
Not present as a package (historical). No agentloop package exists in the
current tree; this page records the extracted ReAct engine. Live paths
(sdk.Agent.Run L2, internal/tools/toolsource, sdk/discovery.go) are
production-tested.