Documentation
¶
Overview ¶
Package claudeexecutor provides a generic executor for Claude-based agents that reduces boilerplate while maintaining flexibility for agent-specific logic.
The executor handles the common conversation loop pattern including:
- Prompt rendering from templates
- Message streaming and accumulation
- Tool call execution and response handling
- JSON response parsing
- Trace management for evaluation
Basic Usage ¶
Create an executor with a client and prompt template:
client := anthropic.NewClient(
vertex.WithGoogleAuth(ctx, region, projectID, "https://www.googleapis.com/auth/cloud-platform"),
)
tmpl, _ := template.New("prompt").Parse("Analyze: {{.Input}}")
exec, err := claudeexecutor.New[*Request, *Response](
client,
tmpl,
claudeexecutor.WithModel[*Request, *Response]("claude-3-opus@20240229"),
claudeexecutor.WithMaxTokens[*Request, *Response](16000),
)
if err != nil {
return nil, err
}
// Define tools if needed
tools := map[string]claudetool.Metadata[*Response]{
"read_file": {
Definition: anthropic.ToolParam{
Name: "read_file",
Description: anthropic.String("Read a file"),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"path": map[string]any{
"type": "string",
"description": "File path",
},
},
Required: []string{"path"},
},
},
Handler: func(ctx context.Context, toolUse anthropic.ToolUseBlock, trace *agenttrace.Trace[*Response]) map[string]any {
// Tool implementation
return map[string]any{"content": "file contents"}
},
},
}
// Execute the agent
response, err := exec.Execute(ctx, request, tools)
Options ¶
The executor supports several configuration options:
- WithModel: Override the default model (defaults to claude-sonnet-4@20250514)
- WithMaxTokens: Set maximum response tokens (defaults to 8192, max 32000)
- WithTemperature: Set response temperature (defaults to 0.1)
- WithSystemInstructions: Provide system-level instructions
- WithThinking: Enable extended thinking mode with a token budget
- WithCacheFirstUserBlock: Also cache the first user message (off by default)
- WithUserPromptSuffix: Render a static suffix as a second block of the initial user message, outside the shared cacheable prefix (off by default)
- WithMaxToolCallsBeforeFinalize: Soft-cap the agentic loop (off by default)
- WithForceSubmitToolChoice: Force the terminal submit tool via tool_choice (off by default)
Prompt Caching ¶
Anthropic prompt caching is enabled by default (disable with WithoutCacheControl). The executor places cache breakpoints on the static request prefix — the last tool definition and the system prompt — and advances a moving breakpoint along the conversation tail each turn. The tail breakpoint means the accumulated message history (the rendered prompt and every prior tool result) is written to the cache once and read at the cached-token price on subsequent turns, instead of being re-billed at the full input price on every turn of the loop. Two tail markers are retained (current and previous) so a turn that appends many blocks — for example several parallel tool calls — still resumes from the prior turn's cache entry. Cache reads and writes are reported per turn via the gen_ai.usage.cache_* metrics and on the trace.
WithUserPromptSuffix extends the shared prefix across executions: it splits the initial user message into a leading payload block (the rendered prompt, carrying a cache breakpoint) and a trailing static suffix block. Because cache entries are matched at breakpoint block boundaries, executions that share the payload but differ only in the suffix — for example multi-pass reviewers examining one changeset through different lenses — read the tools + system + payload prefix from a single cache entry instead of each re-paying the payload at full input price.
Extended Thinking ¶
Extended thinking allows Claude to show its internal reasoning process before responding. When enabled, reasoning blocks are captured in the trace:
exec, err := claudeexecutor.New[*Request, *Response](
client,
prompt,
claudeexecutor.WithThinking[*Request, *Response](2048), // 2048 token budget for thinking
)
Reasoning blocks are stored in trace.Reasoning as []agenttrace.ReasoningContent, where each block contains:
- Thinking: the reasoning text
Note: When thinking is enabled, temperature is automatically set to 1.0 as required by the Claude API. See: https://docs.claude.com/en/docs/build-with-claude/extended-thinking
Claude Opus 4.7 Compatibility ¶
Opus 4.7 introduced two breaking changes that the executor handles transparently so callers don't need model-aware logic:
- Sampling parameters (temperature, top_p, top_k) are rejected with a 400. WithTemperature is silently dropped for Opus 4.7 models; a warning is logged once per Execute if the caller explicitly set it.
- Extended-thinking budgets are replaced by adaptive thinking. WithThinking(N) is mapped to adaptive thinking for Opus 4.7 (the budget is advisory to the model via adaptive mode). A warning is logged once per Execute noting the mapping. Display is set to "summarized" so reasoning traces remain populated.
See: https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7
Type Safety ¶
The executor is generic over Request and Response types, ensuring type safety throughout the conversation. The trace parameter in tool handlers is properly typed with the Response type.
Index ¶
- Constants
- type Interface
- type Option
- func WithCacheFirstUserBlock[Request promptbuilder.Bindable, Response any]() Option[Request, Response]
- func WithEffort[Request promptbuilder.Bindable, Response any](effort string) Option[Request, Response]
- func WithForceSubmitToolChoice[Request promptbuilder.Bindable, Response any](deferUntilToolName string) Option[Request, Response]
- func WithMaxTokens[Request promptbuilder.Bindable, Response any](tokens int64) Option[Request, Response]
- func WithMaxToolCallsBeforeFinalize[Request promptbuilder.Bindable, Response any](n int) Option[Request, Response]
- func WithMaxTurns[Request promptbuilder.Bindable, Response any](turns int) Option[Request, Response]
- func WithModel[Request promptbuilder.Bindable, Response any](model string) Option[Request, Response]
- func WithProvider[Request promptbuilder.Bindable, Response any](p Provider) Option[Request, Response]
- func WithResourceLabels[Request promptbuilder.Bindable, Response any](labels map[string]string) Option[Request, Response]
- func WithResultValidator[Request promptbuilder.Bindable, Response any](v callbacks.ResultValidator[Response]) Option[Request, Response]
- func WithRetryConfig[Request promptbuilder.Bindable, Response any](cfg retry.RetryConfig) Option[Request, Response]
- func WithSubmitResultProvider[Request promptbuilder.Bindable, Response any](provider SubmitResultProvider[Response]) Option[Request, Response]
- func WithSystemInstructions[Request promptbuilder.Bindable, Response any](prompt *promptbuilder.Prompt) Option[Request, Response]
- func WithTemperature[Request promptbuilder.Bindable, Response any](temp float64) Option[Request, Response]
- func WithThinking[Request promptbuilder.Bindable, Response any](budgetTokens int64) Option[Request, Response]
- func WithToolCallConcurrency[Request promptbuilder.Bindable, Response any](n int) Option[Request, Response]
- func WithUserPromptSuffix[Request promptbuilder.Bindable, Response any](suffix *promptbuilder.Prompt) Option[Request, Response]
- func WithoutCacheControl[Request promptbuilder.Bindable, Response any]() Option[Request, Response]
- type Provider
- type SubmitResultProvider
Examples ¶
Constants ¶
const DefaultMaxTurns = 200
DefaultMaxTurns is the default maximum number of conversation turns (LLM round-trips) before the executor aborts. Each turn corresponds to one Claude API call. This prevents runaway loops when the model keeps calling tools without converging on a result.
const DefaultToolCallConcurrency = 10
DefaultToolCallConcurrency is the default bound on how many of a single turn's tool calls run concurrently. Models routinely emit several independent tool calls in one turn (parallel tool use); dispatching their handlers concurrently cuts wall-clock latency. Override with WithToolCallConcurrency — a value of 1 restores strictly sequential dispatch.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Interface ¶
type Interface[Request promptbuilder.Bindable, Response any] interface { // Execute runs the agent conversation with the given request and tools // Optional seed tool calls can be provided - these will be executed and their results prepended to the conversation Execute(ctx context.Context, request Request, tools map[string]claudetool.Metadata[Response], seedToolCalls ...anthropic.ToolUseBlock) (Response, error) }
Interface is the public interface for Claude agent execution
type Option ¶
type Option[Request promptbuilder.Bindable, Response any] func(*executor[Request, Response]) error
Option is a functional option for configuring the executor
func WithCacheFirstUserBlock ¶ added in v0.7.2
func WithCacheFirstUserBlock[Request promptbuilder.Bindable, Response any]() Option[Request, Response]
WithCacheFirstUserBlock places an additional Anthropic cache breakpoint on the first user content block (the rendered prompt), in addition to the tool definitions and system prompt that are cached by default.
This is useful when the first user message carries a large payload (for example, per-request evidence embedded in the prompt) and the agent loop spans several turns: with the breakpoint, the API reads that payload from cache at 10% of the base input price on turns after the first, instead of re-billing it at full price each turn.
The breakpoint is only placed when prompt caching is enabled and a breakpoint slot remains within the API's limit, so it can never cause the API to reject a request for having too many breakpoints. Off by default.
Caching benefits accrue within a single model's iteration loop. A workflow that switches models mid-flight (for example, a cheap first pass that escalates to a stronger model) does not share a cached prefix across the switch, because the cache is keyed by the exact request prefix including the model. See: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
func WithEffort ¶ added in v0.9.9
func WithEffort[Request promptbuilder.Bindable, Response any](effort string) Option[Request, Response]
WithEffort sets the reasoning effort (output_config.effort), which controls how deeply the model thinks and its overall token spend. Valid values are "low", "medium", "high", "xhigh", and "max"; leaving it unset keeps the model's default ("high"). Effort is GA on every serving backend (Vertex AI and the first-party API) and needs no beta header. It is the recommended depth control on Claude 4.7+/Sonnet 5, which removed the extended-thinking budget — "xhigh" is the recommended setting for hard coding/agentic work.
func WithForceSubmitToolChoice ¶ added in v0.7.2
func WithForceSubmitToolChoice[Request promptbuilder.Bindable, Response any](deferUntilToolName string) Option[Request, Response]
WithForceSubmitToolChoice forces the model to call its terminal submit tool via tool_choice instead of leaving the choice to the model. This eliminates the wasted turn the executor would otherwise spend reactively redirecting a model that answered with plain text instead of calling the submit tool.
The force is applied on the first turn when deferUntilToolName is empty or names a tool that is NOT registered for the run. When deferUntilToolName names a tool that IS registered (for example a deferred-evidence fetch tool), the first turn stays at tool_choice auto so the model can gather that deferred evidence first; the submit tool is forced only on the turn after that gate tool has been called at least once.
The option is a no-op unless a terminal submit tool is configured via WithSubmitResultProvider — without one there is no tool to force toward. It is opt-in and off by default, so callers that do not set it keep the existing reactive behavior unchanged.
Not compatible with WithThinking: the API requires tool_choice auto/none while extended thinking is active and returns a 400 for a forced tool_choice, so construction fails when both are set. The order in which the two options are applied does not matter — the conflict is checked after all options are applied.
func WithMaxTokens ¶
func WithMaxTokens[Request promptbuilder.Bindable, Response any](tokens int64) Option[Request, Response]
WithMaxTokens sets the maximum tokens for responses
Example ¶
ExampleWithMaxTokens demonstrates configuring the maximum number of tokens the executor may generate per response.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/agents/executor/claudeexecutor"
"chainguard.dev/driftlessaf/agents/promptbuilder"
)
func main() {
opt := claudeexecutor.WithMaxTokens[promptbuilder.Noop, *struct{}](16000)
fmt.Printf("option is nil: %v\n", opt == nil)
}
Output: option is nil: false
func WithMaxToolCallsBeforeFinalize ¶ added in v0.7.2
func WithMaxToolCallsBeforeFinalize[Request promptbuilder.Bindable, Response any](n int) Option[Request, Response]
WithMaxToolCallsBeforeFinalize bounds the agentic loop with a soft cap: once the model has made n investigative (non-terminal) tool calls, the executor injects a single instruction asking it to call its terminal submit tool now and forces that tool on the next turn.
This is distinct from WithMaxTurns, which aborts the run when exceeded. The soft cap instead steers the model toward emitting a result based on the evidence gathered so far, which is preferable for workflows that should always return a verdict rather than fail. A value of zero (the default) disables the nudge entirely. The nudge only takes effect when a terminal tool is configured via WithSubmitResultProvider; without one there is no tool to steer toward, so the option is a no-op.
Not compatible with WithThinking; the API requires tool_choice auto/none while thinking is active, and the forced tool_choice this option uses on the finalize turn returns a 400.
func WithMaxTurns ¶ added in v0.2.0
func WithMaxTurns[Request promptbuilder.Bindable, Response any](turns int) Option[Request, Response]
WithMaxTurns sets the maximum number of conversation turns (LLM round-trips) before the executor aborts. This prevents runaway loops where the model keeps calling tools without converging on a result. Default is DefaultMaxTurns.
func WithModel ¶
func WithModel[Request promptbuilder.Bindable, Response any](model string) Option[Request, Response]
WithModel allows overriding the model name
Example ¶
ExampleWithModel demonstrates configuring the Claude model used by the executor.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/agents/executor/claudeexecutor"
"chainguard.dev/driftlessaf/agents/promptbuilder"
)
func main() {
opt := claudeexecutor.WithModel[promptbuilder.Noop, *struct{}]("claude-3-opus@20240229")
fmt.Printf("option is nil: %v\n", opt == nil)
}
Output: option is nil: false
func WithProvider ¶ added in v0.7.56
func WithProvider[Request promptbuilder.Bindable, Response any](p Provider) Option[Request, Response]
WithProvider declares which backend serves this executor's requests, so metrics and traces carry the true serving provider. Defaults to ProviderVertex, which matches anthropicauth.NewClient's fallback when no federation config is present; callers that construct a federation client must pass ProviderAnthropic.
Example ¶
ExampleWithProvider demonstrates declaring the serving backend so metrics (gen_ai.provider.name) and trace turns carry the true provider. Callers that build a federation client via anthropicauth pass ProviderAnthropic; the default is ProviderVertex, matching the Vertex fallback.
package main
import (
"fmt"
"chainguard.dev/driftlessaf/agents/executor/claudeexecutor"
"chainguard.dev/driftlessaf/agents/promptbuilder"
)
func main() {
opt := claudeexecutor.WithProvider[promptbuilder.Noop, *struct{}](claudeexecutor.ProviderAnthropic)
fmt.Printf("option is nil: %v\n", opt == nil)
}
Output: option is nil: false
func WithResourceLabels ¶
func WithResourceLabels[Request promptbuilder.Bindable, Response any](labels map[string]string) Option[Request, Response]
WithResourceLabels sets labels for GCP billing attribution when using Claude via Vertex AI. Automatically includes default labels from environment variables:
- service_name: from K_SERVICE, falling back to CLOUD_RUN_JOB (defaults to "unknown")
- product: from CHAINGUARD_PRODUCT (defaults to "unknown")
- team: from CHAINGUARD_TEAM (defaults to "unknown")
Custom labels passed to this function will override defaults if they use the same keys.
func WithResultValidator ¶ added in v0.7.59
func WithResultValidator[Request promptbuilder.Bindable, Response any](v callbacks.ResultValidator[Response]) Option[Request, Response]
WithResultValidator registers a validator that gates the terminal submit tool. When the model calls the submit tool with a payload that parses, every registered validator runs concurrently against the parsed response; any findings reject the submission back to the model as the tool's result — the loop continues until a submission passes — and a validator error aborts the run. Repeatable: each call appends a validator, and their findings are concatenated in registration order. Only meaningful when a submit tool is configured via WithSubmitResultProvider; without one there is nothing to gate.
func WithRetryConfig ¶
func WithRetryConfig[Request promptbuilder.Bindable, Response any](cfg retry.RetryConfig) Option[Request, Response]
WithRetryConfig sets the retry configuration for handling transient Claude API errors. This is particularly useful for handling 429 rate limit and 529 overloaded errors. If not set, a default configuration is used.
func WithSubmitResultProvider ¶
func WithSubmitResultProvider[Request promptbuilder.Bindable, Response any](provider SubmitResultProvider[Response]) Option[Request, Response]
WithSubmitResultProvider registers the submit_result tool using the supplied provider. This is opt-in - agents must explicitly call this to enable submit_result.
func WithSystemInstructions ¶
func WithSystemInstructions[Request promptbuilder.Bindable, Response any](prompt *promptbuilder.Prompt) Option[Request, Response]
WithSystemInstructions sets custom system instructions
func WithTemperature ¶
func WithTemperature[Request promptbuilder.Bindable, Response any](temp float64) Option[Request, Response]
WithTemperature sets the temperature for responses Claude models support temperature values from 0.0 to 1.0 Lower values (e.g., 0.1) produce more deterministic outputs Higher values (e.g., 0.9) produce more creative/random outputs
func WithThinking ¶
func WithThinking[Request promptbuilder.Bindable, Response any](budgetTokens int64) Option[Request, Response]
WithThinking enables extended thinking mode with the specified token budget The budget_tokens parameter sets the maximum tokens Claude can use for reasoning This must be less than max_tokens and at least 1024 tokens is recommended
func WithToolCallConcurrency ¶ added in v0.7.10
func WithToolCallConcurrency[Request promptbuilder.Bindable, Response any](n int) Option[Request, Response]
WithToolCallConcurrency bounds how many of a single turn's tool calls run concurrently when the model emits more than one in a turn (parallel tool use). Defaults to DefaultToolCallConcurrency.
Results are always consumed in the order the model emitted them, so the tool_use/tool_result pairing the API requires is preserved, and the first terminal result (in order) ends the run.
A value of 1 runs the turn's tool calls strictly in order, one at a time. Set it to 1 for agents whose tool handlers mutate shared state (a worktree, a cache) without their own synchronization; concurrent dispatch is otherwise safe because handlers share only the trace, which is concurrency-safe.
func WithUserPromptSuffix ¶ added in v0.7.56
func WithUserPromptSuffix[Request promptbuilder.Bindable, Response any](suffix *promptbuilder.Prompt) Option[Request, Response]
WithUserPromptSuffix renders a static, operator-authored prompt as a second text block of the initial user message, after the rendered request prompt.
This exists for fleets of executions that share one large payload but vary a small trailing instruction — for example multi-pass reviewers that each examine the same changeset through a different lens. Anthropic prompt caching writes and reads cache entries at cache_control block boundaries, and a prefix is only shareable when the bytes up to a marked block are identical; splitting the initial message keeps the varying suffix out of the shared prefix, so the tool definitions, system prompt, and leading payload block are served from one cache entry across all such executions while the suffix block varies freely after the breakpoint.
Setting this option implies WithCacheFirstUserBlock: the leading block gets the cache breakpoint that ends the shareable prefix — without that marker the split would buy nothing. The suffix must be fully bound by the caller; the request is never bound into it, and it is built once per Execute. See: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
func WithoutCacheControl ¶ added in v0.2.0
func WithoutCacheControl[Request promptbuilder.Bindable, Response any]() Option[Request, Response]
WithoutCacheControl disables Anthropic prompt caching.
Prompt caching is enabled by default because it significantly reduces input token costs for multi-turn agentic workflows. The API caches the request prefix (tool definitions + system prompt) and serves it at 10% of the normal input token price on subsequent turns. The only cost is a 1.25x write premium on the first turn, which is amortized across all subsequent cache reads within the 5-min TTL.
You would only disable this if you have a single-turn agent that runs less than once every 5 minutes, where the 1.25x write cost would never be recouped. See: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
type Provider ¶ added in v0.7.56
type Provider string
Provider identifies the serving backend a Claude request goes to. The same Claude model can be served by two different providers with two different bills: Google Vertex AI (GCP billing) or the Anthropic first-party API via Workload Identity Federation (Anthropic workspace billing). The provider is stamped on every metric (gen_ai.provider.name) and trace turn (system), so stored telemetry distinguishes the backends explicitly instead of inferring them from model-ID shape (the Vertex-only "@version" suffix).
type SubmitResultProvider ¶
type SubmitResultProvider[Response any] func() (claudetool.SubmitMetadata[Response], error)
SubmitResultProvider constructs tool metadata for submit_result.