anthropic

package
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 26, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

actor/anthropic

The first real actor.Provider: it calls the Anthropic Messages API to propose the next action for an in-flight campaign task. It composes with the frozen actor seam like any other Provider — see ../provider.go for the interface it implements, and spec/plans/goal-driven-mvp-slice-2.md for the design decisions this package is built to.

Quick start

import "chatwright.dev/runtime/actor/anthropic"

provider, err := anthropic.New(anthropic.Config{})
// Config{} is enough: the API key comes from ANTHROPIC_API_KEY, the model
// defaults to anthropic.DefaultModel, MaxTokens defaults to
// anthropic.DefaultMaxTokens.

Provider satisfies actor.Provider, so it plugs directly into actor.Loop or, for CI, into an actor.CassetteProvider — see Cassette workflow below.

Config and environment variables

Field Default Notes
APIKey $ANTHROPIC_API_KEY Never read from an actor.Prompt, never written to a cassette — see actor's own doctrine on cassette contents. New returns ErrMissingAPIKey if both are empty.
Model anthropic.DefaultModel (claude-haiku-4-5) See "Why Haiku 4.5" below.
MaxTokens anthropic.DefaultMaxTokens (1024) The response is one small JSON object plus a one-sentence rationale — 1024 tokens is generous headroom, well under the ~16000-token threshold where the Anthropic SDKs require streaming.
HTTPClient, BaseURL, MaxRetries SDK defaults For tests: point HTTPClient at a fake http.RoundTripper, or BaseURL at an httptest.Server. MaxRetries overrides the SDK's automatic retry-with-backoff on 429/5xx (default 2) — tests set it to 0 so an error-taxonomy assertion doesn't wait through real backoff delays.
Now time.Now Only used to measure Usage.Latency; inject a fake clock for deterministic latency assertions.
DisableCostEstimate false See Cost estimate below.

Only ANTHROPIC_API_KEY is read from the environment — no other Anthropic env var (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ...) is consulted by this package, though the underlying SDK client may still see them if they happen to be set in the process environment.

Why claude-haiku-4-5

A campaign is many small, latency-sensitive turns per task — choosing one of four actions from a short, well-structured prompt, not open-ended reasoning — so the fast/cheap tier is the right default, not the most capable model. Per Anthropic's current model line-up, claude-haiku-4-5 is "the fastest and most cost-effective model for simple tasks" at $1.00 / $5.00 per million input/output tokens, versus $3+ / $15+ for the Sonnet and Opus tiers. A caller running a harder campaign (more ambiguous goals, more reasoning to pick the right action) sets Config.Model to a stronger model explicitly — nothing in this package assumes Haiku.

Response contract: structured outputs, not a bare JSON prompt

Propose asks the model to reply with exactly one JSON objectkind (send-text | click | task-done | give-up), text, action_id, rationale — via Anthropic's structured outputs (output_config.format with a json_schema), which claude-haiku-4-5 supports. This is the reliable route: the API enforces the JSON shape server-side, rather than us hoping a plain-text instruction is followed.

Even so, response.go makes exactly one repair attempt if the reply somehow doesn't parse as-is (e.g. wrapped in prose or a markdown fence): it retries against the substring from the first { to the last }. A second failure — or a reply that parses but violates the contract (missing kind, a click with no action_id, ...) — returns a typed *anthropic.InvalidResponseError wrapping the raw text. Propose never fabricates a Proposal: on any parse or validation failure it returns the zero-value actor.Proposal and a non-nil error, never a guess.

ObservationSequence on a click proposal is never taken from the model's reply — it is always prompt.Observation.Sequence, the only observation the model could have seen that turn. The model only ever needs to name an action_id; Chatwright is the one that stamps which observation it was chosen from, matching actor.Proposal.ObservationSequence's own contract.

Whether a click's action_id is still valid is the loop's job (observe.Engine.Validate against the engine's current state) — this package deliberately does not duplicate that check. Providers are dumb transports; per actor's design, safety lives in the loop.

Error taxonomy

Go type When Notes
*anthropic.AuthenticationError HTTP 401/403 Bad, missing, revoked or under-scoped API key. Not retryable without fixing the key.
*anthropic.RateLimitError HTTP 429 Retryable after backoff — this package makes exactly Config.MaxRetries SDK-level retry attempts (default 2) before returning this.
*anthropic.InvalidResponseError Unparseable/contract-violating reply, or a refusal (stop_reason: "refusal", empty content) Carries Raw (truncated) and StopReason. Never a fabricated Proposal.
wrapped generic error Any other transport/API failure (5xx, network failure, cancelled context) Still unwraps to the underlying *anthropic-sdk-go.Error via errors.As/errors.Unwrap when the failure reached the API at all.

Usage/cost mapping

Usage.Model, Usage.InputTokens, Usage.OutputTokens and Usage.Latency are read straight from the API response (usage.input_tokens / usage.output_tokens) and the call's wall-clock duration.

Usage.Cost is filled in automatically for models pricing.go has an entry for (see PricingUSDPerMillionTokens), sourced from Anthropic's published pricing as of PricingSnapshotDate = 2026-06-24 (https://platform.claude.com/docs/en/pricing) — this is a point-in-time snapshot, not a live price feed, and Anthropic can change list prices at any time. Every entry is the model's standard, non-promotional rate: claude-sonnet-5 in particular carries a temporary lower "intro" rate through 2026-08-31 that is deliberately not used, so an estimated spend against goal.Budgets.MaxCost never understates the model's steady-state cost. A model with no pricing-table entry leaves Usage.Cost nil rather than guess. Set Config.DisableCostEstimate to leave it nil unconditionally. Treat Usage.Cost as a budgeting estimate, never an invoice — refresh pricing.go (and PricingSnapshotDate) when it drifts from the source URL above.

Cassette workflow (record once, replay free)

Provider is a plain actor.Provider, so it wraps in actor.CassetteProvider exactly like the ScriptedProvider docs describe:

provider, err := anthropic.New(anthropic.Config{}) // ANTHROPIC_API_KEY set
cassette := actor.NewCassette("actor/anthropic model=" + anthropic.DefaultModel)
recorder, err := actor.NewCassetteProvider(actor.ModeRecord, provider, cassette)

// ... run the campaign/loop against recorder ...

err = recorder.Cassette().Save("testdata/cassettes/my-campaign.json")
  1. Record once, locally, with a real ANTHROPIC_API_KEY set and actor.ModeRecord. Every Propose call is appended to the cassette, keyed by a hash of the provider config plus the exact actor.Prompt sent — see ../cassette.go.
  2. Commit the cassette under testdata/cassettes/ — it is human-readable, indented JSON with no provider auth in it (API keys never enter an actor.Prompt, so there is nothing to redact), safe to review in a PR diff.
  3. CI replays it with actor.ModeReplay and no API key at all: a cache hit returns the recorded Proposal/Usage verbatim, at zero token cost; a cache miss (the campaign's behaviour changed enough to ask a new question) is a hard test failure naming the missing prompt, never a silent live fallback.

Re-record whenever the campaign's goal/task/observation shape changes enough that the old cassette no longer covers the prompts a run actually asks. Changing this package's own prompt rendering (prompt.go, promptContractVersion) does not by itself require re-recording — a cassette entry's lookup key is a hash of the canonical actor.Prompt JSON, not of anything this package renders from it — but a rendering change can still shift what the live model would say next time you do re-record, so bump promptContractVersion alongside any change that could plausibly affect model behaviour, as a breadcrumb for whoever reviews the next recording.

Testing

  • go test ./actor/anthropic/... runs the full suite above at zero token cost — everything is driven through a fake http.RoundTripper (see helpers_test.go), never the network.

  • One optional live smoke test, gated behind CHATWRIGHT_LIVE_LLM=1 AND a set ANTHROPIC_API_KEY — skipped with a clear message otherwise, so go test ./... never spends a token in CI:

    CHATWRIGHT_LIVE_LLM=1 ANTHROPIC_API_KEY=sk-ant-... \
      go test ./actor/anthropic/ -run TestLive -v
    

Documentation

Overview

Package anthropic is the first real actor/actor.Provider implementation: it calls the Anthropic Messages API to propose the next action for an in-flight campaign task.

It composes with the frozen actor seam like any other Provider — nothing in this package changes actor.Provider, actor.Prompt, actor.Proposal, actor.Usage or the Loop's semantics. In particular it is meant to be wrapped in an actor.CassetteProvider: record once against the live API with a real key, commit the cassette under testdata/cassettes/, and CI replays it at zero token cost (see README.md).

Providers are dumb transports (see actor's package doc): this one renders a Prompt to text, asks the model to reply with exactly one JSON object (Anthropic's structured-outputs contract enforces the shape server-side — see prompt.go), and maps that reply to a Proposal. It never fabricates a Proposal: an unparseable or malformed reply is a typed error, not a guess — see response.go.

Index

Constants

View Source
const DefaultMaxTokens = int64(1024)

DefaultMaxTokens is the default Config.MaxTokens: generous headroom for a short rationale plus the fixed JSON scaffolding, well under the ~16000-token non-streaming ceiling, so every call stays a plain non-streaming request.

View Source
const DefaultModel = sdk.ModelClaudeHaiku4_5

DefaultModel is the default model this package proposes with: claude-haiku-4-5, Anthropic's fastest and most cost-effective current model (see README.md for the source). A campaign is many small, latency-sensitive turns per task — action selection from a short, well-structured prompt, not open-ended reasoning — so the fast/cheap tier is the right default; callers that want a stronger model for harder campaigns set Config.Model explicitly.

View Source
const PricingSnapshotDate = "2026-06-24"

PricingSnapshotDate is when PricingUSDPerMillionTokens was last checked against Anthropic's published pricing. It is a point-in-time snapshot, not a live price feed — Anthropic can change list prices at any time. Update both together when they drift from the source below.

Variables

View Source
var ErrMissingAPIKey = errors.New("actor/anthropic: no API key: set Config.APIKey or the ANTHROPIC_API_KEY environment variable")

ErrMissingAPIKey means New was called with an empty Config.APIKey and the ANTHROPIC_API_KEY environment variable was also unset (or empty).

View Source
var PricingUSDPerMillionTokens = map[string]modelPrice{
	DefaultModel:        {Input: 1.00, Output: 5.00},
	"claude-sonnet-5":   {Input: 3.00, Output: 15.00},
	"claude-sonnet-4-6": {Input: 3.00, Output: 15.00},
	"claude-opus-4-8":   {Input: 5.00, Output: 25.00},
	"claude-opus-4-7":   {Input: 5.00, Output: 25.00},
	"claude-fable-5":    {Input: 10.00, Output: 50.00},
	"claude-mythos-5":   {Input: 10.00, Output: 50.00},
}

PricingUSDPerMillionTokens is a snapshot of Anthropic's per-model list pricing (US dollars per 1,000,000 tokens) as of PricingSnapshotDate, sourced from pricingSourceURL. Propose uses it to fill actor.Usage.Cost automatically (see Config.DisableCostEstimate) for every model it has an entry for; a model with no entry leaves Usage.Cost nil rather than guess.

Every entry here is the model's standard, non-promotional rate. claude-sonnet-5 in particular carries a temporary lower "intro" rate ($2/$10 per MTok) through 2026-08-31 that is deliberately NOT used here, so a campaign's estimated spend against goal.Budgets.MaxCost never understates the model's steady-state cost.

Treat Usage.Cost as an estimate for campaign budgeting, not an invoice — see AGENTS.md's "fidelity is declared" principle. Refresh this table (and PricingSnapshotDate) when it drifts from pricingSourceURL.

Functions

This section is empty.

Types

type AuthenticationError

type AuthenticationError struct{ Err error }

AuthenticationError wraps an Anthropic API 401/403 response: the API key is missing, invalid, revoked, or lacks access to the requested model. Never retryable without fixing the key.

func (*AuthenticationError) Error

func (e *AuthenticationError) Error() string

func (*AuthenticationError) Unwrap

func (e *AuthenticationError) Unwrap() error

type Config

type Config struct {
	// APIKey authenticates every request. If empty, New reads the
	// ANTHROPIC_API_KEY environment variable; if that is also empty or
	// unset, New returns ErrMissingAPIKey. Never sourced from an
	// actor.Prompt.
	APIKey string

	// Model is the Anthropic model id to propose with. Empty uses
	// DefaultModel.
	Model string

	// MaxTokens bounds the model's reply. <= 0 uses DefaultMaxTokens.
	MaxTokens int64

	// HTTPClient overrides the HTTP client the Anthropic SDK issues
	// requests with. Nil uses the SDK's default client. Tests set this to
	// an *http.Client backed by a fake http.RoundTripper so Propose never
	// touches the network — see provider_test.go.
	HTTPClient *http.Client

	// BaseURL overrides the Anthropic API base URL. Empty uses the SDK's
	// default (https://api.anthropic.com/). Tests that prefer a real HTTP
	// server over a fake RoundTripper point this at an httptest.Server.
	BaseURL string

	// Now supplies the provider's notion of the current time, used only to
	// measure Usage.Latency around the API call. Nil uses time.Now. Inject
	// a fake clock for deterministic latency assertions in tests.
	Now func() time.Time

	// DisableCostEstimate turns off the automatic Usage.Cost estimate (see
	// pricing.go) even for models this package has pricing for. Usage.Cost
	// is always left nil for models it has no pricing for, regardless of
	// this flag.
	DisableCostEstimate bool

	// MaxRetries overrides the Anthropic SDK's automatic retry count for
	// retryable errors (429, 5xx, connection failures) with exponential
	// backoff. Nil keeps the SDK default (2). Tests set this to a pointer
	// to 0 so an error-taxonomy test asserts on the first response instead
	// of waiting through real backoff delays.
	MaxRetries *int
}

Config configures a Provider.

type InvalidResponseError

type InvalidResponseError struct {
	// Raw is the model's raw response text that failed to parse, or empty
	// if the response carried no text content at all.
	Raw string
	// StopReason is the API response's stop_reason, when known — e.g.
	// "refusal" or "max_tokens" explain why Raw is empty or truncated.
	StopReason string
	Err        error
}

InvalidResponseError means the model's reply could not be turned into a valid actor.Proposal — malformed JSON even after the one repair attempt (see response.go), a JSON object that does not match the response contract (missing/invalid "kind", or a kind whose required field is empty), or an API response with no text content at all (e.g. a refusal with an empty content array). Raw carries the model's raw text (truncated for the error message) so a developer can see what went wrong; Propose never fabricates a Proposal in its place.

func (*InvalidResponseError) Error

func (e *InvalidResponseError) Error() string

func (*InvalidResponseError) Unwrap

func (e *InvalidResponseError) Unwrap() error

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider is an actor.Provider backed by the Anthropic Messages API. Build one with New. The zero value is not usable.

func New

func New(cfg Config) (*Provider, error)

New builds a Provider from cfg. It returns ErrMissingAPIKey if neither cfg.APIKey nor the ANTHROPIC_API_KEY environment variable supplies a key.

func (*Provider) Propose

func (p *Provider) Propose(ctx context.Context, prompt actor.Prompt) (actor.Proposal, actor.Usage, error)

Propose implements actor.Provider: it renders prompt (see prompt.go), calls the Anthropic Messages API for exactly one structured-output JSON reply, and maps that reply to a Proposal (see response.go). It never returns a fabricated Proposal — any failure to obtain and parse a valid reply is a typed error (see errors.go), leaving the caller's zero-value Proposal untouched.

type RateLimitError

type RateLimitError struct{ Err error }

RateLimitError wraps an Anthropic API 429 response. Retryable after backoff; this package does not retry internally (see README.md) — the loop/caller decides whether and when to retry a failed Propose call.

func (*RateLimitError) Error

func (e *RateLimitError) Error() string

func (*RateLimitError) Unwrap

func (e *RateLimitError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL