Help us shape the future of Go! Take the 2026 Go Developer Survey

core

package
v0.0.0-...-de24b69 Latest Latest
Warning

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

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

Documentation

Overview

Package core holds AgentKit's canonical vocabulary and every interface seam. Machinery lives elsewhere; core holds declarations, the small pure functions that are part of the contract, and the EventStream.

core imports only schema. Nothing in AgentKit below the root package may import the root package, which is what keeps anything that needs an *Agent out of core.

Index

Constants

View Source
const BlockErrorCode = "blocked_by_policy"
View Source
const MinMaxPendingBytes int64 = 1 << 20

MinMaxPendingBytes is the floor for StreamOptions.MaxPendingBytes.

A floor of "the largest single event AgentKit can emit" is not computable: the largest event is a ToolResultEvent bounded only by the tool output cap, which is configured elsewhere and can be raised after the stream is created. A documented constant is a promise something can keep.

Variables

View Source
var (
	ErrMaxTurns       = errors.New("agentkit: maximum turns reached")
	ErrBudgetExceeded = errors.New("agentkit: budget exceeded")
	ErrToolRejected   = errors.New("agentkit: tool call rejected")
	ErrRefusal        = errors.New("agentkit: model refused the request")
	// ErrBusy: a conflicting operation was attempted while a turn was in
	// flight. The caller may retry; it is never queued.
	ErrBusy = errors.New("agentkit: agent is busy")
	// ErrAborted: the run stopped on a cancelled context, or a provider ended
	// its stream as aborted. On a cancellation the run's error wraps it
	// beside the context's own error, so errors.Is matches either.
	ErrAborted       = errors.New("agentkit: run aborted")
	ErrStreamOverrun = errors.New("agentkit: stream consumer overrun")

	// ErrNoNestedCaller: CallNested was used where no NestedCaller is
	// attached — a tool that declares no ReachableTools, or one invoked by
	// something that is neither an agent run nor a Runner.
	ErrNoNestedCaller = errors.New(
		"agentkit: no nested caller on the context; only a tool that declares ReachableTools can call other tools")
	// ErrTerminated: an interceptor voted to end the run during a nested
	// call. The wrapper should stop and return; its result carries the vote
	// to the batch whatever it returns.
	ErrTerminated = errors.New("agentkit: run terminated by an interceptor during a nested call")

	// Raised by the loop / session layer.
	ErrNotContinuable = errors.New("agentkit: transcript is not continuable")
	ErrSessionExists  = errors.New("agentkit: session log already exists at path")
	// ErrSessionNotEmpty makes the forbidden "construct, then patch the model
	// onto it" resume a runtime failure on the first line rather than a style
	// violation.
	ErrSessionNotEmpty = errors.New(
		"agentkit: session store is not empty; resume with LoadSession + NewAgentFromSession")

	// ErrUnguardedExecute: a toolset that reaches a shell tool (execute,
	// run_command) with no Guard is refused by agentkit.NewRunner, and so by
	// agentkit.New, before any request. A headless embedder with no policy
	// would otherwise hand the model an unrestricted shell by omission.
	// Supply an interceptor — guard.Restricted is the shipped starting point
	// — or pass guard.AllowAll to say so explicitly.
	ErrUnguardedExecute = errors.New(
		"agentkit: a shell tool is reachable but Toolset.Guard is nil; " +
			"supply an interceptor (guard.Restricted is the shipped starting point) " +
			"or guard.AllowAll to opt out explicitly")
)

Sentinel errors, all comparable with errors.Is.

Functions

func BatchTerminates

func BatchTerminates(votes []bool) bool

BatchTerminates is a batch's vote to end the run: an AND over finalized results. An empty batch never terminates. OR semantics are the obvious-but-wrong default: the model emits N parallel calls, and a unilateral finish would compute N-1 results it never sees.

func MarshalEvent

func MarshalEvent(e Event, enc MessageEncoder) ([]byte, error)

MarshalEvent encodes e as its discriminated JSON form.

func ReportUsage

func ReportUsage(ctx context.Context, u Usage)

ReportUsage hands u to the context's usage reporter, if there is one. Code that spends on the agent's behalf outside the loop — a custom Summarizer, a transform that calls a model — reports what each call cost through it. Without a reporter it does nothing.

func ShouldIterate

func ShouldIterate(m *AssistantMessage) bool

ShouldIterate is ExtractToolUse's boolean form. The loop calls this; nothing in the loop compares a StopReason to decide whether to continue.

func StreamChannel

func StreamChannel(s *EventStream) <-chan StreamEvent

StreamChannel adapts an EventStream to a provider's channel: every event, then a last item carrying the stream's error, and its result message too when that was not the last event. The consumer must drain the channel.

func WithNestedCaller

func WithNestedCaller(ctx context.Context, c NestedCaller) context.Context

WithNestedCaller attaches c to ctx. An agent run or a Runner does this for every tool that declares ReachableTools before calling it, so a tool implementation never constructs a NestedCaller itself.

func WithUsageReporter

func WithUsageReporter(ctx context.Context, report func(Usage)) context.Context

WithUsageReporter returns a context whose ReportUsage calls go to report. The agent installs one around its context transform, so a model call made off the loop — a compaction summary — is counted in Agent.Usage, and so seen by StopPolicy budgets, like the turns it makes room for.

Types

type API

type API string

API identifies a wire protocol. Providers are keyed by this, never by vendor: Model.Provider is a vendor id used only for credential resolution and catalog lookup.

const (
	APIAnthropicMessages API = "anthropic-messages"
	APIOpenAICompletions API = "openai-completions"
	APIOpenAIResponses   API = "openai-responses"
	APIGoogleGenerative  API = "google-generative-ai"
	APIOllamaChat        API = "ollama-chat"
	APIFaux              API = "faux" // shipped and supported
)

type AfterToolCallContext

type AfterToolCallContext struct {
	ToolName  string
	ToolUseID string
	// ParentToolUseID and ParentToolName are as in BeforeToolCallContext.
	ParentToolUseID string
	ParentToolName  string
	Arguments       map[string]any
	Result          *ToolResultMessage // mutable in place
	Elapsed         time.Duration
	// ToolResult is the handler's result as returned, after panic and
	// Handler-error conversion, passed by value. It is read-only: changing
	// it (or anything reached through its Metadata pointer) has no effect
	// on the emitted, persisted or stop-policy message. Result remains the
	// one mutable record and already carries Metadata.
	ToolResult ToolResult
}

type AfterToolCallDecision

type AfterToolCallDecision struct {
	// Terminate is *bool because nil means "no opinion".
	Terminate *bool
}

type AgentDoneEvent

type AgentDoneEvent struct {
	Result RunResult
	Usage  Usage // session aggregate
}

func (AgentDoneEvent) Class

func (AgentDoneEvent) Class() EventClass

func (AgentDoneEvent) EventType

func (AgentDoneEvent) EventType() EventType

type AgentStartEvent

type AgentStartEvent struct {
	SessionID string
	Provider  string
	API       API
	Model     string
}

func (AgentStartEvent) Class

func (AgentStartEvent) Class() EventClass

func (AgentStartEvent) EventType

func (AgentStartEvent) EventType() EventType

type AssistantMessage

type AssistantMessage struct {
	Content Content

	// StopReason is the canonical enum; RawStopReason is the provider's own
	// finish string, verbatim. Neither may be read by the continuation check
	// — use ExtractToolUse.
	StopReason    StopReason
	RawStopReason string
	// StopDetail is the provider's own account of why it stopped, when it
	// gives one beyond the reason — a refusal's category and explanation
	// ("cyber: …"). Like RawStopReason it never drives control flow.
	StopDetail   string
	ErrorMessage string
	Usage        Usage
	Timestamp    time.Time

	// Model is the REQUESTED model, never ResponseModel: transcript repair
	// replays a turn's thinking signatures only to the model that issued
	// them, and a fallback-served turn judged against ResponseModel would
	// have its own valid signatures stripped.
	Model         string
	ResponseModel string
	ResponseID    string
	// Effort is the thinking effort the turn was requested with.
	Effort Effort
}

func (AssistantMessage) Clone

func (m AssistantMessage) Clone() Message

func (AssistantMessage) Role

func (AssistantMessage) Role() Role

type BeforeToolCallContext

type BeforeToolCallContext struct {
	ToolName  string
	ToolUseID string
	// ParentToolUseID and ParentToolName identify the wrapper call that made
	// this call through CallNested. Both are empty for a call the model made
	// directly.
	ParentToolUseID string
	ParentToolName  string
	Tool            Tool
	Arguments       map[string]any
	// RawInput is the model's own bytes, key order intact.
	RawInput  json.RawMessage
	Assistant *AssistantMessage
	Batch     []ToolUseBlock
	Index     int
	TurnCount int
}

BeforeToolCallContext is handed to the single tool-authorization boundary. Arguments are already validated and coerced.

type BeforeToolCallDecision

type BeforeToolCallDecision struct {
	Block bool
	// Terminate is honoured ONLY when Block is set.
	Terminate bool
	Reason    string
	// Arguments, when non-nil, replaces Arguments. The interceptor may widen
	// as well as narrow.
	Arguments map[string]any
}

type BlockType

type BlockType string
const (
	BlockText     BlockType = "text"
	BlockThinking BlockType = "thinking"
	BlockToolUse  BlockType = "tool_use"
)

type CacheRetention

type CacheRetention string

CacheRetention is a provider-neutral tri-state: how long a provider keeps the prompt cache.

const (
	CacheRetentionNone  CacheRetention = "none"
	CacheRetentionShort CacheRetention = "short"
	CacheRetentionLong  CacheRetention = "long"
)

type Content

type Content []ContentBlock

func (Content) Clone

func (c Content) Clone() Content

func (Content) Text

func (c Content) Text() string

Text concatenates every TextBlock, for RunResult.FinalText.

type ContentBlock

type ContentBlock interface {
	BlockType() BlockType
	CloneBlock() ContentBlock
	// contains filtered or unexported methods
}

ContentBlock is a sealed union: TextBlock | ThinkingBlock | ToolUseBlock. Sealed because every provider adapter switches exhaustively over it and a third-party variant would make that switch silently incomplete.

type Effort

type Effort string

Effort is how much thinking a model puts into a turn: on an adaptive model it is sent as output_config.effort, on a budget model it picks the row's budget_tokens.

const (
	EffortLow    Effort = "low"
	EffortMedium Effort = "medium"
	EffortHigh   Effort = "high"
	EffortXHigh  Effort = "xhigh"
	EffortMax    Effort = "max"
)

type ErrorEvent

type ErrorEvent struct {
	Message string
	// Err is not serialized; the wire form carries Message.
	Err      error
	Terminal bool
}

func (ErrorEvent) Class

func (ErrorEvent) Class() EventClass

func (ErrorEvent) EventType

func (ErrorEvent) EventType() EventType

type Event

type Event interface {
	EventType() EventType
	Class() EventClass
	// contains filtered or unexported methods
}

Event is a sealed union. clone and size are unexported so the union cannot be extended from outside and so EventStream.Push can deep-copy every event centrally, rather than trusting every producer to remember.

type EventClass

type EventClass uint8

EventClass splits events into deltas, snapshots and authoritative events.

A split into deltas and authoritative events alone ("complete and final for the item it names, exactly one per item, always emitted") would place everything that is not a delta into the authoritative class. But MessageUpdateEvent is by construction repeated and non-final, and a stream carries many of them. A consumer treating it as authoritative would treat the first MessageUpdateEvent as final and stop rendering.

const (
	// ClassIncremental is an optimization. It may be coalesced, may be absent
	// entirely (a non-streaming provider emits none), and is never truth.
	ClassIncremental EventClass = iota
	// ClassSnapshot is complete as of now, repeatable, never final. A consumer
	// may render from it and must not treat it as closing anything.
	ClassSnapshot
	// ClassAuthoritative is complete and final for the item it names, exactly
	// one per item, always emitted. Receiving one means DISCARD the deltas
	// accumulated for that item and take this payload whole.
	ClassAuthoritative
)

type EventStream

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

EventStream is an unbounded mutex + condition-variable queue. It is not a channel: the producer is a paid model call holding an HTTP connection open, and blocking it on a slow consumer stalls the SSE body read, trips the provider's stream idle timeout and kills the request. A slow consumer costs memory and nothing else.

There is no Close, no channel and no BufferSize.

func ErrorStream

func ErrorStream(msg *AssistantMessage, err error) *EventStream

ErrorStream returns an already-ended stream carrying one ErrorEvent. This is the pre-closed error stream, for failures raised before any bytes are sent — an unregistered Api, an unusable option, an OnPayload abort.

The original error is retrievable unwrapped through Err, so errors.Is and errors.As still work on the caller's own sentinel. That is how a function whose only return is *EventStream both encodes failures in the stream and propagates an OnPayload error to the caller unmodified.

func EventStreamOf

func EventStreamOf(ch <-chan StreamEvent, err error) *EventStream

EventStreamOf adapts a provider's channel to an EventStream, so a consumer reads events and the result the same way whatever produced them. err is Stream's own error: it ends the stream at once.

func NewEventStream

func NewEventStream(opts StreamOptions) *EventStream

func (*EventStream) End

func (s *EventStream) End(res StreamResult)

End records the terminal result and wakes everyone. It is idempotent; the first call wins. The producer pushes the terminal EVENT immediately before calling End, so a consumer sees that event and then ok=false.

Every producer MUST `defer stream.End(...)`: Result blocks until it lands.

func (*EventStream) Err

func (s *EventStream) Err() error

Err does not block. It reports ErrStreamOverrun for a dropped consumer even when the run itself succeeded.

func (*EventStream) Events

func (s *EventStream) Events() iter.Seq[Event]

Events is a range-over-func iterator over the stream. Breaking out early is safe: the producer is never blocked on this consumer.

func (*EventStream) Next

func (s *EventStream) Next() (Event, bool)

Next blocks until an event is available or the stream is done. Events queued before End are drained before it reports done, so End never loses events.

Single-consumer: two goroutines calling Next split the stream. Fan-out is the caller's concern.

func (*EventStream) Push

func (s *EventStream) Push(e Event)

Push never blocks. It deep-copies e before enqueueing — centralizing the copy here is what makes the guarantee hold for producers that forget. Pushes after End, and after an overrun, are dropped silently.

func (*EventStream) Result

func (s *EventStream) Result() *AssistantMessage

Result blocks until End and returns the assistant message. It works even if no event was ever read — the result is fed by End, not by consumption, and that is what makes abandoning a stream safe.

func (*EventStream) RunResult

func (s *EventStream) RunResult() (RunResult, error)

RunResult blocks until End and returns the agent-level result. It is the run-facing accessor; on a provider-level stream the RunResult is zero and the error is the stream's own. It is separate from Result because one accessor cannot yield both an AssistantMessage and a RunResult.

func (*EventStream) Wait

func (s *EventStream) Wait() StreamResult

Wait blocks until End and returns the terminal result.

type EventType

type EventType string
const (
	EvAgentStart EventType = "agent_start"
	EvAgentDone  EventType = "agent_done"
	EvTurnStart  EventType = "turn_start"
	EvTurnEnd    EventType = "turn_end"

	EvMessageStart  EventType = "message_start"
	EvMessageUpdate EventType = "message_update"
	EvMessageEnd    EventType = "message_end"

	EvTextStart EventType = "text_start"
	EvTextDelta EventType = "text_delta"
	EvTextEnd   EventType = "text_end"

	EvThinkingStart EventType = "thinking_start"
	EvThinkingDelta EventType = "thinking_delta"
	EvThinkingEnd   EventType = "thinking_end"

	// Model-stream decode events: the model writing a tool call. Distinct
	// from the execution triple below, which reports the tool RUNNING; one
	// name for both would produce two events of that name per call with
	// different payloads. The loop emits the execution triple.
	EvToolCallStart  EventType = "tool_call_start"
	EvToolInputDelta EventType = "tool_input_delta"
	EvToolCallEnd    EventType = "tool_call_end"

	EvToolExecutionStart  EventType = "tool_execution_start"
	EvToolExecutionUpdate EventType = "tool_execution_update"
	EvToolExecutionEnd    EventType = "tool_execution_end"

	EvToolResult EventType = "tool_result"
	EvError      EventType = "error"
)

type ExecutionMode

type ExecutionMode uint8
const (
	// Parallel is the zero value, so every custom tool defaults correctly with
	// no field set.
	Parallel ExecutionMode = iota
	Sequential
)

type Message

type Message interface {
	Role() Role
	Clone() Message
	// contains filtered or unexported methods
}

Message is a sealed union: UserMessage | AssistantMessage | ToolResultMessage.

An interface, not a Role-discriminated fat struct. ToolResultMessage is a first-class role whose ToolUseID, IsError, ToolName and Usage exist on no other role. A fat struct puts those fields on every message and makes "flatten results into a shared user message" a one-line accident instead of a type error. Sealed by isMessage() so provider adapters' role switches stay total.

type MessageEncoder

type MessageEncoder func(Message) (json.RawMessage, error)

MessageEncoder renders one canonical message as JSON.

type MessageEndEvent

type MessageEndEvent struct{ Message AssistantMessage }

func (MessageEndEvent) Class

func (MessageEndEvent) Class() EventClass

func (MessageEndEvent) EventType

func (MessageEndEvent) EventType() EventType

type MessageStartEvent

type MessageStartEvent struct{ Message AssistantMessage }

func (MessageStartEvent) Class

func (MessageStartEvent) Class() EventClass

func (MessageStartEvent) EventType

func (MessageStartEvent) EventType() EventType

type MessageUpdateEvent

type MessageUpdateEvent struct{ Message AssistantMessage }

func (MessageUpdateEvent) Class

func (MessageUpdateEvent) EventType

func (MessageUpdateEvent) EventType() EventType

type Messages

type Messages []Message

func (Messages) Clone

func (ms Messages) Clone() Messages

type Model

type Model struct {
	ID              string
	ContextWindow   int
	MaxOutputTokens int

	InputCostPerMillion      float64
	OutputCostPerMillion     float64
	CacheReadCostPerMillion  float64
	CacheWriteCostPerMillion float64

	// ThinkingKind is how the model takes extended thinking.
	ThinkingKind ThinkingKind
	// Efforts is the wire value of each effort the model takes: a token
	// budget on a budget model, an effort name on an adaptive one. With a map,
	// an effort absent from it (or nil) is not sent; with no map at all, the
	// effort is sent by its own name.
	Efforts map[Effort]*string
	// Compat is the catalog row's capability object, as JSON (for example
	// {"supports_sampling": false}).
	Compat json.RawMessage
}

Model is a catalog row: what a request to the model may ask for and what it costs. Prices are USD per million tokens.

type NestedCaller

type NestedCaller interface {
	Call(ctx context.Context, calls ...ToolUseBlock) ([]ToolResult, error)
}

NestedCaller runs tool calls on behalf of a wrapper tool — one that declares ReachableTools — through the same pipeline as a call the model makes: argument preparation and validation, BeforeToolCall, the handler, AfterToolCall and the execution events.

Call returns one result per call, in the order of calls. A call that fails — unknown to the wrapper, invalid, blocked, aborted, a handler error — is a result with OK false, never a Go error. The error is for the wrapper as a whole: ErrTerminated when an interceptor voted to end the run.

type NoToolsMode

type NoToolsMode string
const (
	NoToolsNone    NoToolsMode = ""
	NoToolsBuiltin NoToolsMode = "builtin"
	NoToolsAll     NoToolsMode = "all"
)

type PreparedArguments

type PreparedArguments struct {
	Raw  json.RawMessage
	Args map[string]any

	// Coercions records what step 3 changed, for diagnostics.
	Coercions []schema.Coercion
}

PreparedArguments is the output of the argument pipeline.

Raw   the bytes handed to the handler
Args  the decoded map, for interceptors and policies to read

Raw is the model's own bytes whenever the pipeline changed nothing, so the handler — and any replay — sees exactly what the model wrote. When a step did change the value, Raw is rebuilt from the original bytes: every key the change left alone keeps its position and its bytes, and only changed or new keys are re-encoded. A map has no order, and re-encoding it whole would sort the keys.

func PrepareArguments

func PrepareArguments(t Tool, c ToolUseBlock) (PreparedArguments, error)

PrepareArguments runs the fixed argument pipeline before the handler. Every stage runs inside the caller's panic-recover boundary.

  1. Tool.PrepareArguments — per-tool shape repair, runs FIRST, on a copy.
  2. Delete explicit nulls for OPTIONAL properties.
  3. Coerce primitives against the declared schema.
  4. Validate; a failure echoes the model's own arguments back in the order it wrote them, so the error is self-correcting.

Step 2 is the non-obvious one: constrained sampling forces the model to emit every declared property, so optional fields arrive as explicit nulls. Treating them as present is a validation failure on well-formed output.

func (PreparedArguments) TryWithArgs

func (p PreparedArguments) TryWithArgs(args map[string]any) (PreparedArguments, error)

TryWithArgs is WithArgs returning an error for a value that is not representable in JSON. An interceptor's replacement arguments are the embedder's code: one that produces NaN fails that call, not the run.

func (PreparedArguments) WithArgs

func (p PreparedArguments) WithArgs(args map[string]any) PreparedArguments

WithArgs replaces the arguments, as an interceptor may do, rebuilding Raw so the keys the interceptor kept keep their positions.

It panics if a value is not representable in JSON (NaN, a channel); TryWithArgs reports that as an error instead.

type ProviderClient

type ProviderClient interface {
	Stream(ctx context.Context, req Request) (<-chan StreamEvent, error)
}

ProviderClient is a model behind a wire API. Stream sends one request and returns its events on a channel that is closed when the turn ends. It returns an error, and no channel, only when the request cannot be made at all — its context is already done, say; a failure once streaming has begun is the last item's Err.

type Request

type Request struct {
	System []ContentBlock
	// Prefix is sent after the system prompt and before Messages on every
	// request, with a cache breakpoint on its last block.
	Prefix   Messages
	Messages Messages
	// Tools is []ToolWire, not []Tool, so no provider can reach a handler or
	// any other field a request body must not carry.
	Tools         []ToolWire
	ToolChoice    ToolChoice
	MaxTokens     *int     // upper bound; capped at the model's output cap
	Temperature   *float64 // nil is absent; an explicit 0 is sent
	TopP          *float64
	StopSequences []string
	// Effort is the thinking effort; empty sends none.
	Effort Effort
}

Request is the canonical, provider-independent model call. Messages are the RAW canonical history: transcript repair happens inside the provider, never here, because the loop is not running when a transcript is loaded from disk and no caller may be able to skip it. The model is the provider's: a ProviderClient is built for one.

type ResultHandler

type ResultHandler func(ctx context.Context, input json.RawMessage) ToolResult

ResultHandler is the richer form. It exists because ToolHandler's signature cannot express a full ToolResult: ToolResult.Terminate is `json:"-"` and Metadata is stripped by ToLLMMap, so there is no channel through json.RawMessage for either. Without it, the tools that end a run (finish, submit_answer, exit_plan_mode) are unimplementable by a consumer, and every built-in tool that truncates cannot report metadata.

type Role

type Role string
const (
	RoleUser       Role = "user"
	RoleAssistant  Role = "assistant"
	RoleToolResult Role = "tool_result"
)

type RunResult

type RunResult struct {
	Messages   Messages
	StopReason RunStopReason
	// LastReason is the final assistant message's canonical reason, kept
	// because RunStopReason and StopReason are different vocabularies.
	LastReason StopReason
	Usage      Usage
	TurnCount  int
	Error      error
}

RunResult is the flattened ACTIVE BRANCH — a view, not the durable representation, and not what a caller should persist.

func (RunResult) FinalText

func (r RunResult) FinalText() string

type RunStopReason

type RunStopReason string

RunStopReason is why a RUN ended. It is a distinct type from StopReason because "max_turns" is not something any model can say, and because RunResult.StopReason == StopReasonLength would otherwise be ambiguous between "the model hit its output cap" and "the run hit its turn cap".

const (
	RunStopEndTurn        RunStopReason = "end_turn"
	RunStopMaxTurns       RunStopReason = "max_turns"
	RunStopBudgetExceeded RunStopReason = "budget_exceeded"
	RunStopPolicy         RunStopReason = "stop_policy"
	RunStopToolTerminate  RunStopReason = "tool_terminate"
	RunStopError          RunStopReason = "error"
	RunStopAborted        RunStopReason = "aborted"
	// RunStopTimeout: the run outlived its configured timeout
	// (agentkit.Config.Timeout).
	RunStopTimeout RunStopReason = "timeout"
	// RunStopRefusal: the run ended on a model's refusal (StopReasonRefusal
	// with no tool calls). Run also returns an error wrapping ErrRefusal.
	RunStopRefusal RunStopReason = "refusal"
)

type StopReason

type StopReason string

StopReason is the canonical, lossy normalization of a provider finish reason. RawStopReason on AssistantMessage keeps the provider's own string.

NAMING: the Go identifier of an output-cap stop is StopReasonLength and its wire string is "max_tokens". They are the same thing.

const (
	StopReasonStop         StopReason = "stop"
	StopReasonLength       StopReason = "max_tokens"
	StopReasonToolUse      StopReason = "tool_use"
	StopReasonStopSequence StopReason = "stop_sequence"
	StopReasonRefusal      StopReason = "refusal"
	StopReasonError        StopReason = "error"
	StopReasonAborted      StopReason = "aborted"
)

func (StopReason) ShortCircuits

func (r StopReason) ShortCircuits() bool

ShortCircuits reports the two reasons that terminate a turn BEFORE tool extraction. Every other reason, recognized or not, is control-flow-identical.

type StreamEvent

type StreamEvent struct {
	Event Event
	Err   error
}

StreamEvent is one item of a provider stream: an event, and on the last item of a failed stream the error. The provider's final assistant message is the Message of the last MessageEndEvent, a failed turn included: its StopReason says it failed and ErrorMessage says why.

type StreamOptions

type StreamOptions struct {
	// MaxPendingBytes bounds queued-but-unread payload. Zero is unbounded (the
	// default). A non-zero value below MinMaxPendingBytes is raised to it.
	// Exceeding it drops the CONSUMER (Next returns false, Err returns
	// ErrStreamOverrun) and lets the run finish with the result still
	// available — it never drops events.
	MaxPendingBytes int64
}

type StreamResult

type StreamResult struct {
	// Message is non-nil once the stream has ended, always — including on a
	// pre-flight failure: partial content plus the failure must be one value.
	Message *AssistantMessage
	// Result is set on agent-level streams and nil on provider-level ones.
	Result *RunResult
	Err    error
}

StreamResult is what End records.

type TextBlock

type TextBlock struct{ Text string }

TextBlock holds a Go string, not []byte, deliberately: strings are immutable, so the deep copy EventStream takes at every Push is a header copy rather than a byte copy. Cloning a partial assistant message is therefore O(blocks), not O(streamed characters).

func (TextBlock) BlockType

func (TextBlock) BlockType() BlockType

func (TextBlock) CloneBlock

func (b TextBlock) CloneBlock() ContentBlock

type TextDeltaEvent

type TextDeltaEvent struct {
	BlockIndex int
	Delta      string
}

func (TextDeltaEvent) Class

func (TextDeltaEvent) Class() EventClass

func (TextDeltaEvent) EventType

func (TextDeltaEvent) EventType() EventType

type TextEndEvent

type TextEndEvent struct {
	BlockIndex int
	Text       string
}

TextEndEvent carries the WHOLE text, not just a final delta. It is authoritative: a consumer discards the deltas it accumulated and takes the payload whole, which is impossible if the end event carries only a delta.

func (TextEndEvent) Class

func (TextEndEvent) Class() EventClass

func (TextEndEvent) EventType

func (TextEndEvent) EventType() EventType

type TextStartEvent

type TextStartEvent struct{ BlockIndex int }

func (TextStartEvent) Class

func (TextStartEvent) Class() EventClass

func (TextStartEvent) EventType

func (TextStartEvent) EventType() EventType

type ThinkingBlock

type ThinkingBlock struct {
	Thinking string
	// Signature is provider-issued and opaque; it may be empty on an aborted
	// stream, and transcript repair downgrades an unsigned block to text.
	// Never inspected.
	Signature string
	Redacted  bool
}

func (ThinkingBlock) BlockType

func (ThinkingBlock) BlockType() BlockType

func (ThinkingBlock) CloneBlock

func (b ThinkingBlock) CloneBlock() ContentBlock

type ThinkingDeltaEvent

type ThinkingDeltaEvent struct {
	BlockIndex int
	Delta      string
}

func (ThinkingDeltaEvent) Class

func (ThinkingDeltaEvent) EventType

func (ThinkingDeltaEvent) EventType() EventType

type ThinkingEndEvent

type ThinkingEndEvent struct {
	BlockIndex int
	Thinking   string
	Signature  string
	Redacted   bool
}

func (ThinkingEndEvent) Class

func (ThinkingEndEvent) Class() EventClass

func (ThinkingEndEvent) EventType

func (ThinkingEndEvent) EventType() EventType

type ThinkingKind

type ThinkingKind string

ThinkingKind is how a model takes extended thinking.

const (
	// ThinkingKindNone is a model without extended thinking.
	ThinkingKindNone ThinkingKind = "none"
	// ThinkingKindAdaptive is thinking {"type":"adaptive"} with an effort.
	ThinkingKindAdaptive ThinkingKind = "adaptive"
	// ThinkingKindBudget is thinking {"type":"enabled"} with budget_tokens.
	ThinkingKindBudget ThinkingKind = "budget"
)

type ThinkingStartEvent

type ThinkingStartEvent struct{ BlockIndex int }

func (ThinkingStartEvent) Class

func (ThinkingStartEvent) EventType

func (ThinkingStartEvent) EventType() EventType

type Tool

type Tool struct {
	Name        string
	Description string
	InputSchema *schema.Schema
	// OutputSchema describes ToolResult.Data on success, for programmatic
	// callers (a script runner binding results, a wrapper tool, generated
	// documentation). It is optional and never enforced at runtime: a nil
	// schema means the tool makes no promise. It is NOT part of ToolWire, so
	// no provider request body carries it.
	OutputSchema *schema.Schema

	// ReachableTools are the tools this tool can call on the model's behalf
	// through CallNested — a wrapper such as a script runner. A tool that
	// declares none reaches none. Every call it makes goes through the same
	// interceptor, audit and event pipeline as a direct call, and only to a
	// tool listed here (after ToolPolicy filtering). Not part of ToolWire.
	ReachableTools []Tool
	// Terminating marks a tool whose purpose is to end the run (finish,
	// submit_answer). No wrapper may reach one: a run ends on a decision the
	// model or the embedder made, never on one a wrapper made for it. Not
	// part of ToolWire.
	Terminating bool

	Handler ToolHandler
	Execute ResultHandler

	Label string // display name, never sent
	// Builtin marks a first-party tool. It exists so ToolPolicy.NoTools can
	// mean "builtin" without the resolver keeping a hardcoded name list that
	// silently stops matching when a tool is renamed.
	Builtin          bool
	ExecutionMode    ExecutionMode
	PrepareArguments func(map[string]any) map[string]any
	PromptGuidelines []string
}

Tool is the registry + loop type. Exactly one of Handler and Execute must be set.

func Named

func Named(ts []Tool, names ...string) ([]Tool, error)

Named returns the tools of ts called names, in the order of names, each a value copy: the result shares nothing with ts. It looks only at the top-level entries of ts, never at ReachableTools, so a wrapper's tools are reached through the wrapper and the policy that prunes them stays in force; when ts holds a name twice, the first wins. With no names the result is empty and non-nil.

A name that ts does not hold, or one that names repeats, is an error and the result is nil. The error for a missing name quotes every missing name, in the order of names, and lists the names ts does hold, in ts order, because the usual mistake is a typo or a qualified MCP name (server__tool) and the set as given fixes it in one step.

func ReachableTools

func ReachableTools(tools []Tool) []Tool

ReachableTools returns tools and everything they reach through ReachableTools, transitively, each name once, in order of first appearance (depth first). It is the one answer to "what can this tool set call", however deeply wrappers nest.

func (Tool) Wire

func (t Tool) Wire() ToolWire

type ToolCallEndEvent

type ToolCallEndEvent struct {
	BlockIndex int
	Block      ToolUseBlock
}

ToolCallEndEvent carries the finalized block, Input bytes included, so the consumer needs no salvage parser of its own. It AUGMENTS the accumulated call and never replaces state later events read.

func (ToolCallEndEvent) Class

func (ToolCallEndEvent) Class() EventClass

func (ToolCallEndEvent) EventType

func (ToolCallEndEvent) EventType() EventType

type ToolCallStartEvent

type ToolCallStartEvent struct {
	BlockIndex int
	ToolUseID  string
	Name       string
}

func (ToolCallStartEvent) Class

func (ToolCallStartEvent) EventType

func (ToolCallStartEvent) EventType() EventType

type ToolChoice

type ToolChoice string

ToolChoice is a provider-neutral tri-state. The zero value is Unset, so absent cannot be confused with auto and needs no pointer.

const (
	ToolChoiceUnset ToolChoice = ""
	ToolChoiceAuto  ToolChoice = "auto"
	ToolChoiceNone  ToolChoice = "none"
)

func (ToolChoice) IsSet

func (c ToolChoice) IsSet() bool

type ToolExecutionEndEvent

type ToolExecutionEndEvent struct {
	ToolUseID       string
	Name            string
	Result          Content
	IsError         bool
	ElapsedMS       int64
	ParentToolUseID string
}

func (ToolExecutionEndEvent) Class

func (ToolExecutionEndEvent) EventType

func (ToolExecutionEndEvent) EventType() EventType

type ToolExecutionStartEvent

type ToolExecutionStartEvent struct {
	ToolUseID       string
	Name            string
	ParentToolUseID string
}

ParentToolUseID, on the three execution events, is the wrapper call a nested call was made through, empty for a call the model made directly. A nested call emits these events and no ToolResultEvent: its result goes to the wrapper, not into the transcript.

func (ToolExecutionStartEvent) Class

func (ToolExecutionStartEvent) EventType

func (ToolExecutionStartEvent) EventType() EventType

type ToolExecutionUpdateEvent

type ToolExecutionUpdateEvent struct {
	ToolUseID       string
	Name            string
	Chunk           string
	ParentToolUseID string
}

func (ToolExecutionUpdateEvent) Class

func (ToolExecutionUpdateEvent) EventType

func (ToolExecutionUpdateEvent) EventType() EventType

type ToolHandler

type ToolHandler func(ctx context.Context, input json.RawMessage) (json.RawMessage, error)

ToolHandler is the plain form of a tool's handler: JSON arguments in, JSON result out.

type ToolInputDeltaEvent

type ToolInputDeltaEvent struct {
	BlockIndex int
	ToolUseID  string
	Delta      string // partial argument JSON, as received
}

func (ToolInputDeltaEvent) Class

func (ToolInputDeltaEvent) EventType

func (ToolInputDeltaEvent) EventType() EventType

type ToolMetadata

type ToolMetadata struct {
	Truncated   bool   `json:"truncated,omitzero"`
	TruncatedBy string `json:"truncated_by,omitzero"` // "lines" | "bytes"
	TotalBytes  int64  `json:"total_bytes,omitzero"`
	TotalLines  int64  `json:"total_lines,omitzero"`
	SpillPath   string `json:"spill_path,omitzero"`
	DurationMS  int64  `json:"duration_ms,omitzero"`
	ExitCode    *int   `json:"exit_code,omitzero"`
	Outcome     string `json:"outcome,omitzero"` // ok|exit|signal|timeout|abort
	LineEnding  string `json:"line_ending,omitzero"`
}

func (*ToolMetadata) Clone

func (m *ToolMetadata) Clone() *ToolMetadata

Clone returns a deep copy of m, including a fresh ExitCode pointer. Nil returns nil.

func (*ToolMetadata) IsEmpty

func (m *ToolMetadata) IsEmpty() bool

IsEmpty reports whether m is nil or has every field at its zero value (including a nil ExitCode pointer). An empty metadata carries no information and is treated as nil on the message.

type ToolPolicy

type ToolPolicy struct {
	// Tools, when non-nil (INCLUDING empty), is used verbatim and bypasses
	// everything below. Non-nil-but-empty is distinct from nil.
	Tools        []Tool
	ToolNames    []string // nil means the default built-in set
	ExcludeTools []string
	NoTools      NoToolsMode
	CustomTools  []Tool
}

ToolPolicy is the five-field tool exposure policy Resolve applies, uniformly to built-in and caller-supplied tools.

func (ToolPolicy) Resolve

func (p ToolPolicy) Resolve(registered []Tool) []Tool

Resolve is the five-field resolution over a registered set. It applies UNIFORMLY to built-in and caller-supplied tools — that uniformity is the point, and it is what makes four otherwise surprising consequences true. Each is intended and each has a test:

NoTools "all"     disables CUSTOM tools too, not just built-ins.
NoTools "builtin" leaves the allowlist unset, so custom tools SURVIVE.
ToolNames         constrains custom tools, not only built-ins.
ExcludeTools      applies to custom tools, not only built-ins.

Resolution order is Tools → NoTools → ToolNames → ExcludeTools, with CustomTools merged in before name-based selection so the selectors see one undifferentiated set.

This is what a per-agent tool allowlist resolves to, and it is what lets an agent be scoped to read-and-search-only without rebuilding the tool set by hand.

type ToolResult

type ToolResult struct {
	OK     bool           `json:"ok"`
	Data   map[string]any `json:"data"`
	Error  string         `json:"error,omitzero"`
	Detail string         `json:"detail,omitzero"`
	// Terminate never reaches the model; it is this result's vote to end the
	// run (see BatchTerminates).
	Terminate bool          `json:"-"`
	Metadata  *ToolMetadata `json:"metadata,omitzero"`
	// Blocks are extra content blocks appended after the text the model
	// reads, each its own block in the result.
	Blocks []ContentBlock `json:"-"`

	// Text is the MODEL-FACING rendering of the result. When non-empty it is
	// the text block the model reads, verbatim, in place of the JSON envelope;
	// Data, Error and Detail stay populated for programmatic consumers
	// (interceptors, the audit trail, an MCP server bridging the tool) and are
	// unaffected.
	//
	// It exists because the envelope is the wrong shape for the results that
	// dominate an agent's context. A source file inside a JSON string pays for
	// every newline, tab and quote twice (`\n`, `\t`, `\"`) — 9–13% more
	// bytes and, because escape sequences tokenize badly, an estimated 15–30%
	// more tokens on indented code — and the model reads code through a layer
	// of escaping it then has to undo when it writes an edit. Every built-in
	// tool sets Text on success; an error result leaves it empty and the
	// envelope is used.
	Text string `json:"-"`
}

ToolResult is a tool call's output envelope.

func CallNested

func CallNested(ctx context.Context, calls ...ToolUseBlock) ([]ToolResult, error)

CallNested runs calls through the NestedCaller on ctx. With none attached — the tool declares no ReachableTools, or is invoked by something that is neither an agent run nor a Runner — it runs nothing and returns ErrNoNestedCaller.

func ErrResult

func ErrResult(code, detail string) ToolResult

func OKResult

func OKResult(data map[string]any) ToolResult

func (ToolResult) LLMText

func (r ToolResult) LLMText() string

LLMText is the text the model sees for this result: Text when the tool rendered one, otherwise the envelope (ToLLMMap) as JSON. A marshal failure — a Data value that is not JSON-representable — yields a fixed error envelope rather than an empty result, so the transcript never carries a tool_result with no content.

func (ToolResult) ToLLMMap

func (r ToolResult) ToLLMMap() map[string]any

ToLLMMap strips Metadata and Terminate. Providers receive only the payload.

type ToolResultEvent

type ToolResultEvent struct{ Message ToolResultMessage }

func (ToolResultEvent) Class

func (ToolResultEvent) Class() EventClass

func (ToolResultEvent) EventType

func (ToolResultEvent) EventType() EventType

type ToolResultMessage

type ToolResultMessage struct {
	ToolUseID string
	ToolName  string
	Content   Content
	IsError   bool
	Timestamp time.Time
	// AddedToolNames marks tools that became available at this point in the
	// transcript. Opaque to the loop.
	AddedToolNames []string
	Usage          *Usage // optional; a pointer, never omitempty
	// Metadata is the producing handler's structured metadata (exit code,
	// outcome, truncation, spill path, totals, duration, line ending).
	// Persisted by the session codec, never sent to a provider. Nil for
	// handler-less results (unknown tool, invalid arguments, blocked,
	// aborted, max_tokens synthesis, repair synthesis).
	Metadata *ToolMetadata
}

func (ToolResultMessage) Clone

func (m ToolResultMessage) Clone() Message

func (ToolResultMessage) Role

func (ToolResultMessage) Role() Role

type ToolUseBlock

type ToolUseBlock struct {
	ID   string
	Name string
	// Input is the provider's own argument bytes, verbatim and AUTHORITATIVE
	// for every replay, fingerprint and serialization path.
	// Invariant: always syntactically valid JSON object bytes; NewToolUse
	// normalizes nil/empty to {}.
	Input json.RawMessage
	// ThoughtSignature is opaque and provider-issued; transcript repair strips
	// it on cross-model replay. Never inspected.
	ThoughtSignature string
}

func ExtractToolUse

func ExtractToolUse(m *AssistantMessage) []ToolUseBlock

ExtractToolUse is the loop's continuation predicate, by name. It scans Content for ToolUseBlock and reads no stop reason.

func NewToolUse

func NewToolUse(id, name string, raw json.RawMessage) (ToolUseBlock, error)

NewToolUse checks raw is a JSON object and keeps its bytes. A nil, empty or whitespace-only raw yields {}.

func (ToolUseBlock) BlockType

func (ToolUseBlock) BlockType() BlockType

func (ToolUseBlock) CloneBlock

func (b ToolUseBlock) CloneBlock() ContentBlock

func (ToolUseBlock) InputMap

func (b ToolUseBlock) InputMap() map[string]any

InputMap decodes lazily for handler and interceptor convenience. The result is NEVER re-encoded back into a request.

Numbers come back as json.Number, not float64, because numeric literals must survive verbatim. Handlers doing args["limit"].(float64) will fail their type assertion; assert json.Number and call its Int64 or Float64.

type ToolWire

type ToolWire struct {
	Name        string
	Description string
	InputSchema *schema.Schema
}

ToolWire is the projection a provider is allowed to see — the name, description and input schema, and nothing else.

It is exported because providers live in their own packages, where an unexported projection would be out of reach. Exported, the invariant is STRONGER than unexported-ness: Request.Tools is []ToolWire, so no provider can name a Handler, PromptGuidelines or PrepareArguments at all, and a new Tool field cannot widen a request body without a compile error at the boundary.

ToolWire carries no json tags on purpose: each provider encodes it into its own dialect (Anthropic input_schema, OpenAI parameters).

func ToolWires

func ToolWires(ts []Tool) []ToolWire

type TurnEndEvent

type TurnEndEvent struct {
	TurnIndex int
	Message   AssistantMessage
	// ToolResults is always non-nil; [] for a no-tool turn.
	ToolResults []ToolResultMessage
	Usage       Usage
}

func (TurnEndEvent) Class

func (TurnEndEvent) Class() EventClass

func (TurnEndEvent) EventType

func (TurnEndEvent) EventType() EventType

type TurnStartEvent

type TurnStartEvent struct{ TurnIndex int }

func (TurnStartEvent) Class

func (TurnStartEvent) Class() EventClass

func (TurnStartEvent) EventType

func (TurnStartEvent) EventType() EventType

type Usage

type Usage struct {
	// InputTokens is NET of cache reads and writes.
	InputTokens  int64
	OutputTokens int64
	// ReasoningTokens is a SUBSET of OutputTokens, not an addend.
	ReasoningTokens  int64
	CacheReadTokens  int64
	CacheWriteTokens int64
	// CacheWrite1hTokens is a SUBSET of CacheWriteTokens.
	CacheWrite1hTokens int64
	TotalTokens        int64
	// Requests is how many model requests the usage covers.
	Requests int64
	CostUSD  float64
	// BilledModel is the model that actually served the request.
	BilledModel string
	// Unpriced is true when at least one request the usage covers had a cost
	// that neither the vendor reported nor the catalog priced, so CostUSD
	// counts that request as zero. The cost bit cannot say this: Add ORs it.
	Unpriced bool

	// Set records which fields the provider actually reported, so an explicit
	// 0 beats an SDK fallback and a wholly unreported usage is
	// distinguishable from a reported all-zero one.
	Set UsageField
}

func (Usage) Add

func (u Usage) Add(o Usage) Usage

Add sums two usages field-wise and ORs their presence masks and their Unpriced flags. The session layer only sums; it never recomputes cost.

func (Usage) ContextTokens

func (u Usage) ContextTokens() int64

ContextTokens is TotalTokens when non-zero, else Input+Output+CacheRead+CacheWrite.

func (Usage) Has

func (u Usage) Has(f UsageField) bool

func (Usage) IsZero

func (u Usage) IsZero() bool

IsZero is true when nothing was reported OR everything reported was zero — an "all-zero-usage response", which must never serve as an anchor.

func (Usage) Reported

func (u Usage) Reported() bool

Reported is false when the provider reported nothing at all.

func (*Usage) SetCost

func (u *Usage) SetCost(v float64)

func (*Usage) SetField

func (u *Usage) SetField(f UsageField, v int64)

type UsageField

type UsageField uint32

UsageField is a presence bit. Presence could be a pointer or an explicit flag; a mask is chosen over eight pointers because Usage is summed on every turn, and pointer arithmetic across a summation is where double-counting actually happens.

const (
	UsageInputTokens UsageField = 1 << iota
	UsageOutputTokens
	UsageReasoningTokens
	UsageCacheReadTokens
	UsageCacheWriteTokens
	UsageCacheWrite1hTokens
	UsageTotalTokens
	UsageCostUSD
)

type UsageJSON

type UsageJSON struct {
	InputTokens        int64   `json:"input_tokens"`
	OutputTokens       int64   `json:"output_tokens"`
	ReasoningTokens    int64   `json:"reasoning_tokens"`
	CacheReadTokens    int64   `json:"cache_read_tokens"`
	CacheWriteTokens   int64   `json:"cache_write_tokens"`
	CacheWrite1hTokens int64   `json:"cache_write_1h_tokens"`
	TotalTokens        int64   `json:"total_tokens"`
	CostUSD            float64 `json:"cost_usd"`
	BilledModel        string  `json:"billed_model"`
	Reported           bool    `json:"reported"`
	Unpriced           bool    `json:"unpriced"`
}

UsageJSON is Usage's wire shape for events. Every field is present — a consumer reads the same keys whether or not the provider reported them — and `reported` says which ones the provider actually set. `unpriced` says the cost counts at least one request as zero because no one could price it.

type UsageWire

type UsageWire struct {
	InputTokens        *int64
	OutputTokens       *int64
	ReasoningTokens    *int64
	CacheReadTokens    *int64
	CacheWriteTokens   *int64
	CacheWrite1hTokens *int64
	TotalTokens        *int64
}

UsageWire is the decode-side helper providers use: every numeric is a pointer so an explicit 0 is distinguishable from absent. Into folds it onto a Usage, setting presence bits for exactly the non-nil fields.

func (UsageWire) Into

func (w UsageWire) Into(u *Usage)

type UserMessage

type UserMessage struct {
	Content   Content
	Timestamp time.Time
}

func (UserMessage) Clone

func (m UserMessage) Clone() Message

func (UserMessage) Role

func (UserMessage) Role() Role

Jump to

Keyboard shortcuts

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