Skip to content

v0.5.0 — Tool loop helper (ADR-0011)

Choose a tag to compare

@dratner dratner released this 19 May 23:41
· 6 commits to main since this release
5b5bee9

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): nil Client, empty Messages, Request.Tools set, duplicate tool names, nil Execute, both ToolChoices non-zero, unknown ToolChoice.Type, RequiresTools() with no tools, ToolChoiceTool naming an unknown tool — all rejected before any provider call.
  • Pre-execute MaxIterations stop: the limit-hitting assistant turn is appended to Outcome.Messages as diagnostic state; its tool calls are not executed and the transcript is not directly re-feedable into Complete without repairing the unresolved pairing.
  • Defensive ToolCall copy before dispatch and event emission so a misbehaving executor or observer cannot corrupt the round-trip through shared slice backing (cloned Parameters and ProviderSignature).
  • Failure split: ToolResult{IsError: true} is model-visible (loop continues); a non-nil Execute error is loop-visible → OutcomeToolError. An executor returning context.CanceledOutcomeCanceled.
  • Cancellation detected via errors.Is(err, context.Canceled) (and ctx.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