v0.5.0 — Tool loop helper (ADR-0011)
First non-port addition past v0.4.x: a small, app-neutral helper that wraps the provider-protocol tool round-trip over an llms.ChatClient.
What's new
llms/toolloop — synchronous tool-loop helper. Run(ctx, Config) Outcome over any ChatClient. App-supplied Tool{Definition, Execute}; the loop never executes side effects itself.
out := toolloop.Run(ctx, toolloop.Config{
Client: client,
Request: req,
Tools: []toolloop.Tool{weather},
})
switch out.Kind {
case toolloop.OutcomeFinalAnswer:
fmt.Println(out.Response.Text)
case toolloop.OutcomeMaxIterations, toolloop.OutcomeLLMError,
toolloop.OutcomeToolError, toolloop.OutcomeCanceled:
// inspect out.Err, out.Messages, out.TotalUsage
}Full design: docs/toolloop-proposal.md. Binding scope and non-goals: ADR-0011.
Behaviors pinned
- Verbatim assistant-message round-trip preserves
ToolCall.ProviderSignature(ADR-0010), so Gemini 3 multi-turn tool loops work end-to-end. - Fail-closed config (
*toolloop.ConfigError): nilClient, emptyMessages,Request.Toolsset, duplicate tool names, nilExecute, bothToolChoices non-zero, unknownToolChoice.Type,RequiresTools()with no tools,ToolChoiceToolnaming an unknown tool — all rejected before any provider call. - Pre-execute
MaxIterationsstop: the limit-hitting assistant turn is appended toOutcome.Messagesas diagnostic state; its tool calls are not executed and the transcript is not directly re-feedable intoCompletewithout repairing the unresolved pairing. - Defensive
ToolCallcopy before dispatch and event emission so a misbehaving executor or observer cannot corrupt the round-trip through shared slice backing (clonedParametersandProviderSignature). - Failure split:
ToolResult{IsError: true}is model-visible (loop continues); a non-nilExecuteerror is loop-visible →OutcomeToolError. An executor returningcontext.Canceled→OutcomeCanceled. - Cancellation detected via
errors.Is(err, context.Canceled)(andctx.Err() == context.Canceled), matching the toolkit's X5 contract that caller cancel is not converted into a*llms.ProviderError. - Unknown-tool recovery is the default: an unregistered tool name appends an error tool result and continues, so the model can self-correct when the provider can't enforce
tool_choice.
Non-goals (binding per ADR-0011)
This is a tool loop, not an agent loop. No agent state, terminal/state-transition tools, ProcessEffect, story IDs, request IDs, audit taxonomy, persistence, authorization, tenant isolation, schema-generation, tool registries, moderation hooks, automatic streaming (deferred per ADR-0003), or prompt management. PRs that try to grow IterationEvent/ToolCallEvent with app-specific fields require a superseding ADR.
Compatibility
Additive new surface. No core llms type changes; no MAESTRO_DIVERGENCES.md row required. Existing v0.4.x consumers are unaffected.
Pre-1.0; v0.x minor versions may break.
🤖 Generated with Claude Code