ToolLoopAgent

abstract class ToolLoopAgent<TContext, TOutput>(settings: AgentSettings<TContext> = AgentSettings(), val model: LanguageModel = settings.model ?: throw InvalidArgumentError("model", "ToolLoopAgent requires model via parameter or settings"), val instructions: String = settings.instructions ?: throw InvalidArgumentError( "instructions", "ToolLoopAgent requires instructions via parameter or settings", ), val tools: ToolSet<TContext> = settings.tools ?: throw InvalidArgumentError("tools", "ToolLoopAgent requires tools via parameter or settings"), val output: Output<TOutput>? = settings.output as Output<TOutput>?, val stopWhen: StopCondition = settings.stopWhen ?: StepCountIs(20)) : Agent<TContext, TOutput> (source)

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-calls is returned, OR

  • A 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:

  1. Emit StreamEvent.ToolApprovalRequest in the stream.

  2. Append ContentPart.ToolApprovalRequest to the assistant message.

  3. End the loop — return control to the host with GenerateResult.pendingApprovals populated.

  4. Host calls generate again with messages = result.messages + ToolApprovalResponseMessage(...).

  5. 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

Link copied to clipboard
constructor(settings: AgentSettings<TContext> = AgentSettings(), model: LanguageModel = settings.model ?: throw InvalidArgumentError("model", "ToolLoopAgent requires model via parameter or settings"), instructions: String = settings.instructions ?: throw InvalidArgumentError( "instructions", "ToolLoopAgent requires instructions via parameter or settings", ), tools: ToolSet<TContext> = settings.tools ?: throw InvalidArgumentError("tools", "ToolLoopAgent requires tools via parameter or settings"), output: Output<TOutput>? = settings.output as Output<TOutput>?, stopWhen: StopCondition = settings.stopWhen ?: StepCountIs(20))

Properties

Link copied to clipboard
Link copied to clipboard
Link copied to clipboard

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.

Link copied to clipboard

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.

Link copied to clipboard

v6 CallSettings.frequencyPenalty agent-default.

Link copied to clipboard
open val id: String

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.).

Link copied to clipboard
Link copied to clipboard

Port-side log sink for non-fatal warnings (see Logger). The loop warns here when a Telemetry integration throws and the event is dropped — the swallow contract keeps telemetry from altering the loop, and the warn keeps a broken integration DISCOVERABLE instead of perfectly silent.

Link copied to clipboard
Link copied to clipboard

Effective per-step parallelism cap, retained for source compatibility with existing callers.

Link copied to clipboard

Maximum retries for each non-streaming model round-trip. Streaming retry is handled separately.

Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard

v6 CallSettings.presencePenalty agent-default.

Link copied to clipboard

Wire-level response constraint for providers that support it.

Link copied to clipboard
val seed: Int?
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard

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.

Link copied to clipboard
Link copied to clipboard

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.

Link copied to clipboard
open override val tools: ToolSet<TContext>

The tool surface this agent carries. Exposed at the interface so consumers can inspect / dispatch without casting to ToolLoopAgent.

Link copied to clipboard
val topK: Int?
Link copied to clipboard
val topP: Float?
Link copied to clipboard
open val version: String?

Optional version string — "1.0.0", a git sha, anything stable across an app build. Null = unversioned. Lets telemetry pin issues to a specific agent revision.

Functions

Link copied to clipboard
fun close()

Cancel the engine scope and any in-flight engine job. Call when a long-lived host (ViewModel / Repository) that drove the engine surface is disposed. The per-call generate()/stream() API needs no close().

Link copied to clipboard
suspend fun collectAgentEvents(prompt: String? = null, messages: List<ModelMessage> = emptyList(), options: TContext? = null, abortSignal: AbortSignal = AbortSignalNever, onEvent: suspend (AgentEvent) -> Unit)

Convenience over events: collect the lifecycle stream with one suspend handler — agent.collectAgentEvents { when (it) { is AgentEvent.Chunk -> … } }.

Link copied to clipboard

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.

Link copied to clipboard
fun events(prompt: String? = null, messages: List<ModelMessage> = emptyList(), options: TContext? = null, abortSignal: AbortSignal = AbortSignalNever): Flow<AgentEvent>

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).

Link copied to clipboard
open override fun generate(prompt: String? = null, messages: List<ModelMessage> = emptyList(), options: TContext? = null, abortSignal: AbortSignal = AbortSignalNever): Flow<GenerateResult<TOutput>>

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().

Link copied to clipboard
Link copied to clipboard
open override fun stream(prompt: String? = null, messages: List<ModelMessage> = emptyList(), options: TContext? = null, abortSignal: AbortSignal = AbortSignalNever): Flow<StreamEvent>

Streaming generation. Cold flow — starts when collected.