Tool Loop Agent
Abstract base Agent implementation — the canonical v6 ToolLoopAgent. Extend it; do not instantiate it. A concrete agent subclasses this (e.g. class MyAgent(...) : ToolLoopAgent<C, O>(...)) so the agent's identity, tools, lifecycle overrides, and DI live in one named type — never a bare ToolLoopAgent(...) held as a field.
Runs the model, executes tool calls, accumulates the message list, checks stopWhen after each step, surfaces lifecycle events. Manages the loop end-to-end (invariant I-7) — no manual loop in user code.
Per the v6 source comment in packages/ai/src/agent/tool-loop-agent.ts, the loop continues until:
A finish reason other than
tool-callsis returned, ORA tool that is invoked does not have an execute function, OR
A tool call needs approval, OR
A stop condition is met (default
StepCountIs(20)).
Approval flow (v6 RPC semantics)
When a tool's needsApproval(input, context) returns true:
Emit StreamEvent.ToolApprovalRequest in the stream.
Append ContentPart.ToolApprovalRequest to the assistant message.
End the loop — return control to the host with
GenerateResult.pendingApprovalspopulated.Host calls generate again with
messages = result.messages + ToolApprovalResponseMessage(...).Loop resumes; approved tools execute; denied tools are skipped and the denial reason flows back to the model as a tool result.
Generation isn't kept "in flight" while the user decides — host can serialize, persist, transport, then resume.
Construction accepts either direct common parameters or AgentSettings. Direct parameters win over matching settings fields. model, instructions, and tools are required through one of those paths. Because all constructor parameters have defaults, Kotlin also exposes a public no-arg constructor; ToolLoopAgent<Unit, String>() compiles but fails immediately with InvalidArgumentError because the required model, instructions, and tools are missing.
Constructors
Properties
Port-level engine state machine (isStreaming, currentToolCalls, pendingApprovals, totalSteps, etc.). Distinct from the app-level host-specific orchestration state which is a host-specific reactive lifecycle surface layered on top. Renamed from state so subclasses can declare their own state member without member-hiding clashes.
Self-healing callback fired when a tool call's arguments fail to decode. Return a corrected call to retry, or null to surface StreamEvent.ToolError. See ToolCallRepairFunction.
v6 CallSettings.frequencyPenalty agent-default.
Stable identifier for the agent, useful for telemetry, log routing, and the Agent.version / id / tools accessors v6 exposes (per historical parity gap #33). Implementations default to "agent"; override in the constructor or via class member to set a more specific tag ("chat-agent", "lineup-subagent", etc.).
Effective per-step parallelism cap, retained for source compatibility with existing callers.
Maximum retries for each non-streaming model round-trip. Streaming retry is handled separately.
v6 CallSettings.presencePenalty agent-default.
Wire-level response constraint for providers that support it.
Telemetry for this agent's invocations (upstream v7 telemetry). Telemetry integrations registered globally via registerTelemetry observe every generate/stream call automatically; this setting adds call metadata (TelemetrySettings.functionId) or per-agent TelemetrySettings.integrations — which, when non-empty, REPLACE the global registrations for this agent's calls (upstream per-call semantics). Telemetry observes — an integration throw never alters loop behavior.
Explicit bounded policy for per-step tool execution. Defaults cap both concurrent tool executors and total accepted tool calls so a model cannot create unbounded child coroutines or unbounded in-step work.
The tool surface this agent carries. Exposed at the interface so consumers can inspect / dispatch without casting to ToolLoopAgent.
Functions
Convenience over events: collect the lifecycle stream with one suspend handler — agent.collectAgentEvents { when (it) { is AgentEvent.Chunk -> … } }.
Drive the engine. Each action either advances engineState or aborts the in-flight stream. Idempotent — calling ToolLoopAgentAction.Cancel when nothing is streaming is a no-op.
Observe the FULL agent-lifecycle event stream as a Flow<AgentEvent> — the Flow-first replacement for the former bag of nullable onX callbacks. Cold: one loop run per collection. Carries every lifecycle event (AgentEvent.Started, AgentEvent.StepStarted, every AgentEvent.Chunk, AgentEvent.StepFinished, the tool-call pair, AgentEvent.Errored, AgentEvent.Aborted, AgentEvent.Finished).
Cold flow: no model call or tool execution starts until collected. Each collection reruns the full generation from the beginning, including tool executions and their side effects, and may incur provider cost or billing again. One-shot callers should collect exactly one value, usually with .first().
Streaming generation. Cold flow — starts when collected.