agentturn

package module
v0.0.18 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 18 Imported by: 0

README

agentturn

A composable agent loop in Go over Open Responses: one loop, one transcript type, one tool contract, hooks and queues. Everything else is a front that feeds prompts in and consumes events out, or a subscriber.

  • The transcript is openresponses.Items. What a session stores, what the model receives and what a front renders are the same bytes. An item a harness adds for the model and not for the user, an interrupt report or an advisory, is wrapped in agentturn.Hidden where it is appended: the model still reads it, and its item events and its session entry say a renderer should not show it.
  • The model is any openresponses.Streamer: a remote server through Client.AsAdapter, a local adapter, or another agent served by front/responses.
  • The tool contract is its own library, agenttool, so a tool is written once and runs under any loop. The root module depends on openresponses, agenttool and the standard library; adapters with heavier dependencies are nested modules.

Install

go get github.com/ChristopherDavenport/agentturn

Go 1.25 or later.

A tool, a loop, an agent

type ReadFileArgs struct {
	Path     string `json:"path" desc:"Absolute path to read"`
	MaxBytes int    `json:"max_bytes,omitempty" desc:"Stop after this many bytes"`
}

var ReadFile = agenttool.New("read_file", "Read a file from disk",
	func(ctx context.Context, a ReadFileArgs) (string, error) {
		b, err := os.ReadFile(a.Path)
		return string(b), err
	})

cfg := agentturn.Config{
	Name:         "reader",
	Description:  "Answers questions about files on disk.",
	Model:        openresponses.NewClient(baseURL, openresponses.WithAPIKey(key)).AsAdapter(),
	ModelName:    "gpt-5",
	Instructions: "Be brief.",
	Tools:        []agenttool.Tool{ReadFile},
}

// Low-level: an iterator of events. The loop runs ahead of the consumer
// and the run_end is always the last event.
for ev := range agentturn.Run(ctx, nil, openresponses.Items{openresponses.UserText("What is in go.mod?")}, cfg) {
	switch e := ev.(type) {
	case *agentturn.ItemUpdate:
		if d, ok := e.Stream.(*openresponses.OutputTextDeltaEvent); ok {
			fmt.Print(d.Delta)
		}
	case *agentturn.RunEnd:
		// e.Reason is done, stopped, input_required, aborted or error;
		// a cancelled ctx ends the run with aborted and context.Canceled
		// on e.Err rather than panicking or hanging.
		if e.Err != nil {
			log.Fatal(e.Err)
		}
	}
}

// Stateful: queues, subscribers with barriers, abort, idle.
a := agentturn.New(cfg)
a.Subscribe(func(ctx context.Context, ev agentturn.Event) error {
	// every event is a barrier: the loop waits for this to return
	return nil
})
end, err := a.Prompt(ctx, openresponses.UserText("What is in go.mod?"))

Prompt returns the RunEnd; the error is set only when the run could not start or ended with error. A print front correlates tool_end, which arrives in completion order, with tool_start by call ID rather than by position:

started := map[string]string{}
a.Subscribe(func(_ context.Context, ev agentturn.Event) error {
	switch e := ev.(type) {
	case *agentturn.ToolStart:
		started[e.CallID] = e.Name + " " + string(e.Args)
		fmt.Println("▶", started[e.CallID])
	case *agentturn.ToolEnd:
		fmt.Printf("  [%s] %s\n", started[e.CallID], e.Result.Output.Text)
	}
	return nil
})

A run is one Prompt or Continue until the agent goes idle; a turn is one model call plus the tool executions it requested. The events of a run, in order:

event carries
run_start run ID
turn_start the exact openresponses.Request sent
model_retry a transient model failure about to be retried under Config.Retry: attempt, error, delay
item_start, item_update, item_end an item entering the transcript; item_update wraps the wire StreamEvent verbatim
response_end the folded Response with usage, before any tool of the turn runs
tool_start, tool_dispatch, tool_update, tool_end one tool call from preflight to result: tool_start when it is decided, in the model's order; tool_dispatch when it is handed to its tool; tool_end in completion order
turn_end the folded Response with usage, and the tool results
run_end the items added this run and the reason: done, stopped, input_required, aborted, error

queued belongs to no run. Steer and FollowUp accept an item and return; the goroutine that owns delivery reports it at its next event, before anything that item produces, so a host writing what it accepted tells an item it was handed from one a run made. A run in flight reports it at once, an idle agent at the start of the next run, so a gateway that must not lose an input writes it before it accepts it.

Tools

Tools come from agenttool: agenttool.New[Args, Out] turns a typed function into a tool, reflecting and validating the schema from the argument struct, and agenttool.Executor runs a batch. The loop only knows the agenttool.Tool interface; see that repository for the contract, the schema generator and the MCP adapters.

A batch of calls runs in parallel, bounded, unless the config or a tool asks for sequential execution. Config.BeforeToolCall is the policy seam: block, rewrite arguments, terminate the run, or defer the call to the caller. A deferred call ends the run with input_required and the pending calls listed; Agent.Resume takes an answer per call and continues, which is how a front asks a human before a tool runs. An answer is an output the front produced, usually a refusal, or an approval, which runs the call inside the loop with its tool events, hooks and execution mode:

cfg.BeforeToolCall = func(_ context.Context, info agentturn.ToolCallInfo) (*agentturn.ToolDecision, error) {
	if info.Call.Name == "delete_file" {
		return &agentturn.ToolDecision{Action: agentturn.Defer}, nil
	}
	return nil, nil
}
end, err := a.Prompt(ctx, openresponses.UserText("Clean up the build directory."))
if err == nil && end.Reason == agentturn.ReasonInputRequired {
	// ask the human about end.Pending, then answer every call
	var answers []agentturn.Answer
	for _, call := range end.Pending {
		if approved(call) {
			answers = append(answers, agentturn.Approve(call.CallID).WithBy(agentsession.ByHuman))
		} else {
			answers = append(answers, agentturn.Output(openresponses.NewFunctionCallOutput(call.CallID, "denied by the user")).WithBy(agentsession.ByHuman))
		}
	}
	end, err = a.Resume(ctx, answers...)
}

Answer.WithBy says who decided, in the session format's terms (human, policy, agent), and WithNote what they said; a recorder writes both on the decision. There is no default, since a policy engine answers through Resume as often as a person does, so an answer that names nobody is recorded as an anonymous decision.

The same path repairs a run that was aborted mid-batch: the cut-off calls are on end.Pending, and an agent built with agentturn.WithTranscript from a stored session marks them pending again. A call that was handed to its tool may have run, and one in a transcript the agent was seeded with may have too, so Resume approves either only as agenttool's replay rule allows, with the idempotency key and the arguments of the dispatch it repeats, and refuses otherwise with ErrAmbiguousCall. agentturn.OutcomeUnknown is the answer for such a call, which a recorder writes as an answer decision with the answer's By and Reason before the output, and Answer.WithRunAgain the only way past the rule, for a host that accepts the risk. A tool without WithReplay reads as unknown, so an approval of it after a cut is refused. After a restart, session.AgentOptions seeds the agent with the context and with what the record says of each pending call, so a call that never started is approved without the rule and put to BeforeToolCall, since nothing decided it, a call held after its dispatch is held to the rule when approved, and a call answered or refused before the crash wrote its output takes that output alone; session.ReplayAnswers applies the rule to the calls nobody holds, and is read against an agent seeded this way:

rec, s, _ := session.Resume(ctx, store, id)
opts, _ := session.AgentOptions(s)
a := agentturn.New(cfg, opts...)
defer rec.Attach(a)()
// The tools the resume will run: cfg.ResolveTools asks a ToolProvider.
answers, _ := session.ReplayAnswers(ctx, s, cfg.ResolveTools(ctx)) // plus the held calls' answers
end, err := a.Resume(ctx, answers...)

A live agent moved to another branch of the session keeps what it knew of a call pending in both, and learns the rest from the record:

_ = rec.Rebase(s, entryID)
cx, _ := s.Context()
_ = a.SetTranscript(cx.Items)
pending, _ := session.Pending(s)
_ = a.SetPending(pending)

A front whose user has moved on answers them on the way to the next message instead: a Prompt that opens with an output for each pending call is accepted, and the model sees the outputs and the message in one call. AfterToolCall overrides results; ShouldStopAfterTurn ends a run early.

A tool whose own work is to call other tools, a code-execution kernel with a loopback bridge, uses agentturn.Invoke rather than holding a tool set of its own, so the nested call goes through BeforeToolCall, raises tool_start and tool_end with Parent naming the call that made it, and reaches the recorder:

var Eval = agenttool.New("eval", "Run code that may call the agent's tools",
	func(ctx context.Context, a EvalArgs) (string, error) {
		res, err := agentturn.Invoke(ctx, "read", json.RawMessage(`{"path":"go.mod"}`))
		...
	})

A nested call appends nothing to the transcript: it is the work of the call that made it. A hook that defers one refuses it instead, since there is nobody to ask while a tool is running.

Every other request member comes from Config.Request, the base the loop builds each turn's request on: tool_choice, max_output_tokens, include: reasoning.encrypted_content for reasoning models, and so on. BeforeModelCall sees the finished request before it is sent.

Every hook is one field, so two layers that want the same one silently lose an assignment to each other: a memory that re-renders its block into the instructions and a guard that inspects the request both want BeforeModelCall. The chain helpers join them in one place, with the order where a reader can see it, since a hook that edits the request belongs before one that inspects it:

cfg.BeforeModelCall = agentturn.ChainBeforeModelCall(
	memory.BeforeModelCall(), // edits the request
	guard.BeforeModelCall(),  // inspects what will be sent
)
cfg.ShouldStopAfterTurn = agentturn.ChainShouldStopAfterTurn(
	guard.ShouldStopAfterTurn(),
	budget.ShouldStopAfterTurn(),
)

ChainBeforeTurn concatenates what each layer returns, ChainBeforeToolCall folds the decisions deny over ask over allow, and ChainOutputGuard hands each guard's replacement to the next.

Config.Retry retries a model call that failed before the model began its answer: a 429 or 5xx, a dropped stream, a refused connection. The retry happens inside the turn, honours Retry-After, reports itself as a model_retry event, and is cut short by Abort. An attempt commits when a message or a function call opens, and is never retried after that, since the transcript may hold part of the answer. An item the model completed before that point, a reasoning summary, streams to subscribers but waits: it is appended when the attempt commits and dropped when the attempt fails, so a 503 between the thinking and the first token is retried and leaves no orphan behind.

cfg.Retry = agentturn.Retry{MaxAttempts: 4}

Retry.Revise may change the request the next attempt sends, which is how a fallback chain lives in the loop rather than under it: the switch is on the model_retry event and a recorder writes it as a config delta, so the path names the model that answered, and the response entry says in attempts how many calls the turn took.

cfg.Retry = agentturn.Retry{MaxAttempts: 4, Revise: func(attempt int, req *openresponses.Request, err error) *openresponses.Request {
	req.Model = fallback[min(attempt, len(fallback)-1)]
	return nil
}}

Composition

The loop never learns a sub-agent concept. It knows a Model and a list of Tool, and every composition is one of those two things. A protocol earns a place only with a serve side and a consume side:

protocol serve consume
Open Responses front/responses openresponses.Client.AsAdapter
MCP agenttool/mcpserver agenttool/mcpclient
A2A front/a2a tools/a2a
in-process Agent tools/agent

The MCP row lives in agenttool because MCP is about tools, not about the loop: mcpserver serves any agenttool.Tool, and mcpclient produces them. An agent stands in three places inside another system:

  • As a model. front/responses makes a loop an openresponses.Adapter; openresponses.NewHandler serves it; another loop points its Model at it through Client.AsAdapter.
  • As a tool. tools/agent wraps a Config as a Tool: a child run on a fresh transcript, progress through Call.OnUpdate, the final text as the output and a ChildInfo in Result.Details. The child runs as an Agent, and WithSpawn hands it to the host before the run, so a hub can steer it, abort it without cutting its siblings, or prompt it again once the tool has returned; WithRunContext gives the child run a context of the host's making.
  • As a peer. front/a2a exposes a loop to A2A callers; tools/a2a wraps a remote A2A agent as a Tool.
  • As itself, under new settings. A handoff needs no package: Agent.SetConfig replaces the configuration and keeps the transcript, so the next Continue runs the same conversation under another agent's instructions, tools and model. A terminating tool result is the usual trigger, the loop stopping with StopTerminate or StopPartialTerminate and the destination travelling in Result.Details, where the model cannot see it; front/responses.Handoffs reads the transfers a transcript took. A session recorder writes the switch as a config delta. A one-shot trim of what the receiver sees belongs in Agent.SetTranscript between runs, not in Config.Transform, which runs on every call; a trim from the middle of the history costs the responses after it their request hash. The loop leaves another model's reasoning items out of each request on its own, so a change of model needs no trim.

Config.Name and Config.Description are the single source for how an agent presents itself in every one of these.

Modules

path module depends on
. (agentturn), front/responses, compact, tools/agent root openresponses, agenttool
front/a2a, tools/a2a nested github.com/a2aproject/a2a-go
session nested github.com/ChristopherDavenport/agentsession

Every module shares the root's version and is tagged at the same commit, the nested ones with their directory as the prefix (front/a2a/v0.0.2); each is fetched with go get like any module.

compact is the reference Transform: when the transcript exceeds a token budget it folds the older part and splices the result in front of the recent tail, caching it by prefix so repeated turns cost nothing. New folds through the model's Compact endpoint; NewLocal asks an ordinary model call for a summary and splices it in as a message, for the servers that do not implement compaction.

c := compact.New(model, compact.WithBudget(60_000))
cfg.Transform = c.Transform

l := compact.NewLocal(model, compact.WithBudget(60_000), compact.WithModel("gpt-5-mini"))
cfg.Transform = l.Transform

WithRequest(fn) edits the summary request NewLocal sends, so it can carry the reasoning setting the agent's own requests do. A summary that comes back with no text, cut short or no smaller than what it folds is asked once more; a second such answer leaves the transcript unfolded for that call, and the transform does not ask about the same prefix again until it has grown. WithMinFold(tokens) leaves a prefix smaller than tokens unfolded, for a transcript over budget because of its recent tail; NewLocal defaults it to twice an empty summary item, since a smaller prefix is not worth a summary call. After a restart, session.CompactOptions(s) seeds a transform with the last fold the record says it backed off from, so a new process does not ask again for a summary that failed.

WithOnFold reports every fold, applied or failed, with the index at which the transcript was split, so a recorder can write it. Each WithOnFold adds a callback, called in order until one errs. WithPin(fn) keeps the items fn reports through a fold: whatever part of the folded prefix they were in, they follow the summary in the request, so an injected reminder stays the reminder instead of becoming a clause of a summary. The fold summarises them too, so nothing is lost if the pin is later dropped. The compaction entry names what was kept in its pinned member, which the context algorithm places after the summary, so the calls after such a fold keep their request hashes and a resumed session still carries the pinned items.

session subscribes an Agent to an agentsession store: items on item_end, the response entry with its request hash on response_end, config entries as settings change, tool list changes as deltas, a compaction entry for every fold the compact transform reports, and a session of its own for every child run it observes, linked from the parent. Because every event is a barrier, a turn's tool preflight waits for the assistant items to be durable.

rec, s, err := session.Start(ctx, store, agentsession.Header{CWD: cwd})
defer rec.Attach(agent)()
specialist := agent.New(childCfg,
	agent.WithObserver(rec.Observe),
	agent.WithRunContext(rec.ChildContext))
c := compact.NewLocal(model, compact.WithOnFold(rec.Fold))

A child session inherits its parent's working directory, so a store that buckets by directory files it with its parent, and ChildContext puts the child's session ID on the context the child run is given, so a layer inside the child that attributes its writes to a session names the child's rather than the parent's (session.SessionIDFromContext). A second run under one call continues the child's session at its leaf; agent.ContextWithRetry says the other thing.

Design

The contract is docs/rfcs/0001-agent-loop.md, the plan behind it docs/plans/agent-layer.md. Invariants the tests hold:

  • The transcript after a run is a valid Open Responses input: every function_call is answered before the next user message, or the run ended with the unanswered calls on RunEnd.Pending, whether a hook deferred them or an abort or failure cut them off. Agent.Resume answers them; Prompt and Continue refuse until it has.
  • Exactly one run_end per run, nothing after it.
  • item_end for an assistant item follows output_item.done; partial items never reach subscribers as item_end.
  • Abort cancels the model stream and running tools through the context; the transcript keeps only completed items. Every event the abort leaves behind, the tool_end of each cut-off call and the run_end, still reaches subscribers, with a context whose cancellation is lifted. Agent.AbortCause(err) says why, and the loop reads context.Cause, so a rule that matched, an advisor and a user pressing Esc are three things RunEnd.Err and the record tell apart rather than three "context canceled".
  • Events for one run are delivered from one goroutine, the events a nested call raises from a tool's goroutine serialised with them, so a subscriber is never entered from two goroutines at once. Nothing outside a run delivers: Steer and FollowUp queue an item and return, and their queued event is reported by the run at its next event, so steering from inside a subscriber is safe.

License

MIT. See LICENSE.

Documentation

Overview

Package agentturn is a composable agent loop over Open Responses: one loop, one transcript type, one tool contract, hooks and queues. Everything else is a front that feeds prompts in and consumes events out, or a subscriber.

The transcript is openresponses.Items. What a session stores, what the model receives and what a front renders are the same bytes. The model is any openresponses.Streamer: a remote server through openresponses.Client.AsAdapter, a local adapter, or another agent served by front/responses.

Vocabulary

A run is one Run, Continue, Agent.Prompt or Agent.Continue until the agent goes idle. A turn is one model call plus the tool executions it requested.

Transcript and openresponses.Items are one type under two names, and the name says what a value is. A Transcript is a whole conversation: what Run starts from, what Filter and Transform see and return, what a hook or a tool reads from its context. Items is a fragment: the prompts of a run, the outputs of a batch, the items a run added.

Low-level loop

Run and Continue are observational: they yield Event values in order and the loop does not wait for the consumer between phases. The RunEnd is always the last event and says how the run ended.

for ev := range agentturn.Run(ctx, transcript, prompts, cfg) {
	switch e := ev.(type) {
	case *agentturn.ItemUpdate:
		if d, ok := e.Stream.(*openresponses.OutputTextDeltaEvent); ok {
			fmt.Print(d.Delta)
		}
	case *agentturn.RunEnd:
		if e.Err != nil { ... }
	}
}

Agent

Agent adds queues, subscribers and run control over the same loop. Subscribers are awaited in registration order and every event is a barrier: tool preflight for a turn does not start until every subscriber has returned for the assistant item, and Agent.Prompt settles only after the run_end subscribers finish.

Hooks

Every hook on Config is one field, so two layers that each want one silently lose an assignment to each other. ChainBeforeModelCall and its siblings join them in one place, with the order written where a reader can see it.

Composition

The loop never learns a sub-agent concept. It knows a Model and a list of agenttool.Tool, and every composition is one of those two things: front/responses serves a loop as a model, tools/agent wraps a config as a tool, and the A2A and MCP adapters do the same across a protocol.

Index

Constants

View Source
const (
	// RunAgainReason is the reason of an approval built with
	// [Answer.WithRunAgain].
	RunAgainReason = "run again: accepted by the caller"
	// RunAgainSafeReason is the reason of an approval whose tool says
	// replay is safe.
	RunAgainSafeReason = "run again: safe"
	// RunAgainKeyedReason is the reason of an approval whose tool says
	// replay is keyed, run under the key of the dispatch it repeats or
	// with new arguments under a new one.
	RunAgainKeyedReason = "run again: keyed"
)

The reasons an approval of a call that may have run carries when it gives none, naming the rule that let it run again.

View Source
const (
	EventRunStart     = "run_start"
	EventTurnStart    = "turn_start"
	EventModelRetry   = "model_retry"
	EventModelBlocked = "model_blocked"
	EventResponseEnd  = "response_end"
	EventItemStart    = "item_start"
	EventItemUpdate   = "item_update"
	EventItemEnd      = "item_end"
	EventToolStart    = "tool_start"
	EventToolDispatch = "tool_dispatch"
	EventToolUpdate   = "tool_update"
	EventToolEnd      = "tool_end"
	EventTurnEnd      = "turn_end"
	EventRunEnd       = "run_end"
	EventQueued       = "queued"
)

Event type names.

View Source
const EventBuffer = 256

EventBuffer is how many events the loop can run ahead of the consumer of Run or Continue before it blocks; Agent delivers every event synchronously and has no buffer.

View Source
const OutcomeUnknownReason = "not run again: outcome unknown"

OutcomeUnknownReason is the reason an answer built with OutcomeUnknown carries until Answer.WithReason gives another.

View Source
const WithheldCallOutput = "Error: not run: the response that made this call was withheld"

WithheldCallOutput is the text of the output the loop appends for a function call of a response an Config.OutputGuard withheld with an error wrapping ErrGuard: the call completed in the stream before the message the guard stopped on, and the loop closes it, never having dispatched it, so the transcript holds no call without an output and the next prompt goes ahead. The text is fixed and never the guard's error, which may say what the guard kept from the model.

Variables

View Source
var (
	// ErrRunning is returned when Prompt, Continue, Resume, SetConfig or
	// SetTranscript is called while a run is active.
	ErrRunning = errors.New("agentturn: agent is already running")
	// ErrInputRequired is returned when the last run left calls
	// unanswered, deferred to the caller or cut off by an abort or a
	// failure; answer them through [Agent.Resume], or open the next
	// [Agent.Prompt] with their outputs. The typed errors of tools/agent
	// and tools/a2a match it with errors.Is, so a host can ask "does any
	// sub-agent need input" once.
	ErrInputRequired = errors.New("agentturn: pending tool calls must be resumed before continuing")
	// ErrNotPending is returned when Resume was given an answer for a
	// call that is not pending, left a pending call unanswered, or was
	// called with nothing pending, and when a Prompt opens with an
	// output for a call that is not pending.
	ErrNotPending = errors.New("agentturn: output does not answer a pending call")
	// ErrAmbiguousCall is returned when Resume was asked to approve a
	// call that may already have run, one [PendingCall.MayHaveRun]
	// reports, whose tool does not say it can run again: agenttool's
	// replay is unknown, or keyed and the key of the dispatch it would
	// repeat is not known, or keyed and the approval changes the
	// arguments under that key, or keeps them under another. Such a
	// call must not run again; answer
	// it with [OutcomeUnknown] so the model can check before it asks
	// again, or, when the host accepts the risk, approve it with
	// [Answer.WithRunAgain].
	ErrAmbiguousCall = errors.New("agentturn: a call that may have run cannot run again")
	// ErrCallAnswered is returned when Resume was asked to approve a
	// call pending as [PendingAnswered] or [PendingRejected]: a record
	// answered it without running it again, or refused it, and only its
	// output may follow.
	ErrCallAnswered = errors.New("agentturn: an answered call is owed its output and nothing else")
)

Errors returned by Agent.

View Source
var (
	// ErrNoModel is returned when Config.Model is nil.
	ErrNoModel = errors.New("agentturn: config has no model")
	// ErrCannotContinue is returned when the transcript does not end
	// with a user message or a function call output, so there is
	// nothing for the model to answer.
	ErrCannotContinue = errors.New("agentturn: transcript must end with a user message or a function call output to continue")
	// ErrNoPrompt is returned when Run was called with no prompt items.
	ErrNoPrompt = errors.New("agentturn: no prompt items")
	// ErrGuard is what a guard wraps to end the run as a policy stop
	// rather than a failure: ReasonStopped with StopGuard, the error on
	// RunEnd.Err. BeforeTurn, BeforeModelCall, OutputGuard and
	// ShouldStopAfterTurn each read it so.
	ErrGuard = errors.New("agentturn: guard stopped the run")
)

Errors returned before a run starts. Every error the package produces, sentinel or wrapped, begins with "agentturn:".

View Source
var ErrNoInvoker = errors.New("agentturn: no loop on the context to invoke a tool through")

ErrNoInvoker is returned by Invoke outside a tool call of a loop.

View Source
var ErrTriggerExtra = errors.New("agentturn: trigger extra cannot be written")

ErrTriggerExtra is returned for a Trigger whose Extra cannot be written beside kind, ref and source: it names one of the three, or a value does not encode as JSON. Agent.Prompt, Agent.Continue, Agent.Resume, Agent.Queue and Agent.Deliver refuse such a trigger on their context before anything happens, and Run and Continue end with it before any other event.

Functions

func CanContinue added in v0.0.4

func CanContinue(t Transcript) bool

CanContinue reports whether the model has something to answer: the transcript ends with a user message or a function call output.

func ChainBeforeModelCall added in v0.0.7

func ChainBeforeModelCall(fns ...func(context.Context, *openresponses.Request) error) func(context.Context, *openresponses.Request) error

ChainBeforeModelCall runs each hook on the request in order, so each sees what the ones before it left. The first error stops the chain and is returned, which ends the run with a ModelBlocked event as a single hook's error does.

func ChainBeforeToolCall added in v0.0.7

func ChainBeforeToolCall(fns ...func(context.Context, ToolCallInfo) (*ToolDecision, error)) func(context.Context, ToolCallInfo) (*ToolDecision, error)

ChainBeforeToolCall folds the decisions of each hook, in order, into one: the strictest action wins, Block over Defer over Allow, which is deny over ask over allow as a permission policy reads it. A Block ends the chain, since nothing after it can make the call run; a Defer does not, so a later layer still sees the call and can refuse it. Among decisions of one action the first reason and decider stand; a decision that takes the fold to a stricter action brings its own, because the reason has to explain the decision that stands and a block's reason is what the model reads. The first note stands, and Terminate is set when any decision sets it. Arguments a decision rewrites are passed to the hooks after it, so each sees what the call will actually run with and the last rewrite is what the tool receives. The first error stops the chain and fails the turn.

A nil result from every hook is a nil decision, which allows the call as a single hook's nil does.

func ChainBeforeTurn added in v0.0.7

ChainBeforeTurn runs each hook in order and appends what they return, in that order, so every layer contributes its items to the turn. The first error stops the chain and is returned; the items of the hooks that already ran are dropped with the turn.

func ChainOutputGuard added in v0.0.7

ChainOutputGuard runs each guard on the message in order, each seeing what the ones before it left: a guard that returns a replacement hands that replacement to the next. The last replacement is what reaches the transcript, and a chain in which no guard replaced anything keeps the model's own message. The first error stops the chain and is returned, so one wrapping ErrGuard stops the run as a policy and any other fails it.

func ChainShouldStopAfterTurn added in v0.0.7

func ChainShouldStopAfterTurn(fns ...func(context.Context, TurnInfo) (bool, error)) func(context.Context, TurnInfo) (bool, error)

ChainShouldStopAfterTurn runs each hook in order until one stops the run: the first true ends the run, and the hooks after it do not run, since there is no turn left for them to judge. The first error stops the chain and is returned, so an error wrapping ErrGuard from any hook ends the run as a guard stop with that error on RunEnd.Err. A layer that must see every turn, a meter for one, belongs in a subscriber rather than here.

func ChainTransform added in v0.0.10

func ChainTransform(fns ...func(context.Context, Transcript) (Transcript, error)) func(context.Context, Transcript) (Transcript, error)

ChainTransform runs each transform in order, each on what the one before it returned, and returns the last one's transcript, so a product's own shaping, a redaction or a filter, runs before the compact transform and the fold is over what the product shaped. The first error stops the chain and is returned, which fails the turn as a single transform's error does. Each is given a copy of the slice, as the loop gives one, so none can reach the working transcript through another's result.

A request a product transform changed is one the record cannot rebuild, so its response carries no hash, as with one transform. A fold after it is still recorded: compact names the first item it kept, and a session recorder finds that item's entry by the item rather than by the fold's index, which counts the shaped transcript. A transform before the fold that replaces items rather than keeping, dropping or adding them leaves the fold nothing the recorder wrote, and the recorder writes the fold as one it could not place rather than name the wrong entry; the run goes on. A fold that kept nothing names no item, and after a transform that dropped items its index is not the recorder's: keep at least one item.

func ContextWithAskedCall added in v0.0.16

func ContextWithAskedCall(ctx context.Context, call AskedCall) context.Context

ContextWithAskedCall returns ctx carrying call for AskedCallFrom. Ask uses it; a front that asks through another route puts the call on the context the same way.

func ContextWithDeciders added in v0.0.7

func ContextWithDeciders(ctx context.Context, by map[string]string) context.Context

ContextWithDeciders attaches who decided the answer to each pending call, by call ID, in the session format's terms ("human", "policy", "agent"), in place of any an outer context attached; an empty map clears them. Agent.Resume does it from the Answer.By of the answers it was given, so a subscriber writing the record of an output the caller supplied, which raises no tool_start to carry a decision, can say who wrote it. A host driving the low-level Run with the outputs as prompts attaches it itself. The loop reads nothing from it.

func ContextWithOrigins added in v0.0.16

func ContextWithOrigins(ctx context.Context, origins map[string]string) context.Context

ContextWithOrigins attaches where the output answering each pending call was taken from, by call ID, as ContextWithReasons attaches why, in place of any an outer context attached; an empty map clears them. Agent.Resume does it from the Answer.Origin of the answers it was given as outputs, so a subscriber writing the record of an output a record held rather than one produced now can tie it to the entry it repeats. The loop reads nothing from it.

func ContextWithReasoningModels added in v0.0.15

func ContextWithReasoningModels(ctx context.Context, m ReasoningModels) context.Context

ContextWithReasoningModels attaches m, over any an outer context attached, for Run and Continue, whose transcript the loop has not seen, and for an Agent's run over what the agent already knows. The run reads a copy and adds the reasoning its own model produces.

func ContextWithReasons added in v0.0.11

func ContextWithReasons(ctx context.Context, reasons map[string]string) context.Context

ContextWithReasons attaches why each pending call was answered as it was, by call ID, as ContextWithDeciders attaches who decided, in place of any an outer context attached; an empty map clears them. Agent.Resume does it from the Answer.Reason of the answers it was given as outputs, so a subscriber writing the record of an output for a call that may have run can say why the call was not run again. The loop reads nothing from it.

func ContextWithReservedCallIDs added in v0.0.12

func ContextWithReservedCallIDs(ctx context.Context, ids ...string) context.Context

ContextWithReservedCallIDs attaches call IDs a call the model makes in a run started with ctx must not take, beside those the agent reserved with WithReservedCallIDs and those an outer context attached: the IDs a record holds that the run's transcript does not, such as a child session reopened under the call that made it, whose new agent knows none of the calls its earlier runs made. The session package's Recorder.ChildContext attaches them for a child run.

func ContextWithRunID added in v0.0.5

func ContextWithRunID(ctx context.Context, runID string) context.Context

ContextWithRunID attaches a run ID to ctx. The loop does this with its own for everything a run calls, the transform, the hooks, the model and the tools, so a tool that composes another agent, and whatever observes that child, can tell which run it was called from.

func ContextWithTranscript added in v0.0.4

func ContextWithTranscript(ctx context.Context, t Transcript) context.Context

ContextWithTranscript attaches a transcript to ctx. The loop does this with its working transcript before running a turn's hooks and tools, so a tool that composes another agent can seed it from the conversation without the host threading anything through.

func ContextWithTrigger added in v0.0.6

func ContextWithTrigger(ctx context.Context, t Trigger) context.Context

ContextWithTrigger attaches a Trigger to ctx. A run started with that context, through Run, Continue or an Agent, carries it on its RunStart, so a recorder can write what caused the run without the loop learning what a cron job or a channel is.

func Continue

func Continue(ctx context.Context, t Transcript, cfg Config) iter.Seq[Event]

Continue drives the loop from the transcript as it stands, which must satisfy CanContinue and answer every function call it holds. Events arrive as for Run.

func DeciderFromContext added in v0.0.7

func DeciderFromContext(ctx context.Context, callID string) string

DeciderFromContext returns who the caller named as the decider of the answer for callID, or "" when nobody was named.

func DefaultBackoff added in v0.0.4

func DefaultBackoff(attempt int, err error) time.Duration

DefaultBackoff is the Retry.Backoff used when none is set: the Retry-After header of an openresponses error when it names a number of seconds, otherwise 500ms doubled per retry and capped at 30s, without jitter.

func DefaultRetryable added in v0.0.4

func DefaultRetryable(err error) bool

DefaultRetryable is the Retry.Retryable used when none is set: an openresponses error with status 408, 409, 429 or 5xx, a stream that ended before its terminal event, and transport failures (net.Error, an unexpected EOF, a reset or refused connection). Everything else, a 4xx in particular, is final.

func Hidden added in v0.0.7

func Hidden(item openresponses.Item) openresponses.Item

Hidden marks item as part of the model's context that a renderer should hide: a stream rule's interrupt report, an advisory, a notice a keyword added, a subagent's result. The model still sees it, the transcript holds it and a request carries it; only its item events say it is hidden, and a session recorder writes its entry with visible false, which is the format's word for exactly this.

Use it wherever the loop appends an item the caller supplied: Agent.Prompt, Agent.Steer, Agent.FollowUp, the prompts of Run and the items Config.BeforeTurn returns. The loop unwraps it as it appends, so nothing downstream has to know the wrapper exists; a hidden item still in a queue, which Agent.State hands back, is wrapped, so a host that re-queues what it read keeps the mark and one that type-switches on it should call Unhide first.

An item the filter in force hides from the model is a different thing: it is out of the model's context, a recorder writes it as a custom entry, and marking it hidden adds nothing.

func Invoke added in v0.0.7

func Invoke(ctx context.Context, name string, args json.RawMessage) (agenttool.Result, error)

Invoke runs one of the turn's tools as if the model had asked for it under the call in flight: Config.BeforeToolCall decides, tool_start and tool_end are emitted with Parent naming the call that made it, Config.AfterToolCall may override the result, and a session recorder writes it. It is what a tool that lets its code reach the agent's other tools, a code-execution kernel over a loopback bridge, calls instead of holding an agenttool.Set of its own, where the policy, the events and the record would all be absent.

The result is the one the model would have seen, with the error beside it: a tool that failed, a name no tool has, arguments that are not an object, or a call the hook refused, whose Reason is the error. A nested call cannot be handed to the caller, since it belongs to a tool that is running, so one the hook defers is put to the user through the agenttool.Elicitor on ctx, when there is one, with Ask: the question names the call, and the elicitor's context carries it as an AskedCall, its ID, name, whole arguments and the deferral, for AskedCallFrom. An accept runs it and a decline refuses it, and tool_start carries that answer as the decision, by "human". Without an elicitor, or on a cancel or a failure to ask, the deferral refuses the call. Nothing is appended to the transcript, so a nested call costs no items and a Terminate on its result means nothing to the loop.

The call it is made under comes from agenttool.CallFrom, which agenttool.New puts on every typed tool's context; a tool that implements the interface itself and wants the parent named passes agenttool.WithCall. Outside a loop, Invoke returns ErrNoInvoker.

A nested call gets an idempotency key of its own, fresh on every Invoke: nothing derives it from the key of the call that made it, so a parent run again after a restart invokes with new keys, and a keyed tool reached this way deduplicates nothing across the parent's attempts; see the open question in RFC 0001.

Call it from a tool, with the context the tool was given, and not from a hook or a subscriber: BeforeToolCall and AfterToolCall take one call at a time, and a subscriber is called while the agent holds delivery, so either would wait for something it is itself holding.

func OriginFromContext added in v0.0.16

func OriginFromContext(ctx context.Context, callID string) string

OriginFromContext returns where the caller said the output answering callID was taken from, or "" when it said nothing.

func PendingCalls added in v0.0.6

func PendingCalls(pending []PendingCall) []*openresponses.FunctionCall

PendingCalls returns the calls of pending, in order.

func ReasonFromContext added in v0.0.11

func ReasonFromContext(ctx context.Context, callID string) string

ReasonFromContext returns the reason the caller gave for the answer to callID, or "" when none was given.

func Run

func Run(ctx context.Context, t Transcript, prompts openresponses.Items, cfg Config) iter.Seq[Event]

Run appends prompts to the transcript and drives the loop until the agent goes idle. Events are yielded in order and the RunEnd is the single terminal: it is always the last event, its Reason says how the run ended and its Err carries the failure when Reason is ReasonError, the context error when Reason is ReasonAborted, and nil otherwise. A misuse before the run starts (ErrNoPrompt, ErrCannotContinue, ErrNoModel, ErrTriggerExtra, a duplicate tool name, or ErrInputRequired for a transcript with a function call that neither the transcript nor the prompts' leading outputs answer) yields one RunEnd with ReasonError and no other event.

The loop runs ahead of the consumer by up to EventBuffer events and applies backpressure after that. Breaking out of the loop cancels the run and blocks until it has wound down.

The transcript is not modified; the items the run appended are on the RunEnd.

func RunContext added in v0.0.10

func RunContext(ctx context.Context) (context.Context, bool)

RunContext returns the context of the run a tool call, a hook, the transform or the model call serves: the values of the context the run was started with, the run ID among them, cancelled when the run is aborted, through Agent.Abort, Agent.AbortCause or the cancellation of the context given to Prompt, and not when the batch ends or the run ends by itself. It is what a tool derives background work from that must outlive its call and not the run's abort: a task that returns at once and reports later, a watcher, a detached child. Once the run has ended, Agent.Abort reaches the agent's next run and not this one, so work that must be stoppable after that keeps a cancel of its own. Config.ToolRecorder is on it, so a job that writes the handle of what it started with agenttool.WriteRecord reaches the record. The call it serves is not: a root run's context carries no call, and a child run's, one a tool started on the context of its own call as tools/agent does, carries the call that started the child. So a job names its call with agenttool.WithCall(rc, call), as tools/agent does for a detached child, and one in a child that does not is taken for the work of the parent's call. Where a record written after the run has ended is filed is the recorder's to say; a session recorder files it at the leaf of the session of the run the job came from, the child's when the child was given its session ID with session.Recorder.ChildContext and the recorder's own otherwise, and names the call when that session holds it. The transcript, the invoker, the tool elicitor and the steer signal of the call belong to its batch and are not on it; a job that needs one carries it from the call's context. It reports false outside a loop. Each run registers it with the context it was started with until that context ends or the run is aborted, so a host that prompts every run with one long-lived cancellable context keeps one small registration per finished run until then.

func RunIDFromContext added in v0.0.5

func RunIDFromContext(ctx context.Context) string

RunIDFromContext returns the ID of the run whose hook or tool call the context belongs to, or "" outside a loop.

func Steered added in v0.0.10

func Steered(ctx context.Context) <-chan struct{}

Steered returns a channel that is closed when an item is steered into the run the tool serves, with Agent.Steer or Agent.Queue, after its batch began or before it and not yet drained, so a tool that only waits, a sleep, a poll, a wait on another agent, selects on it and returns early with what it has; the steered item then joins the run after the batch, as ever. The loop does not cut the tool: hearing the steer is the tool's choice. It returns nil outside an agent's run, and a nil channel never closes.

func Unhide added in v0.0.7

func Unhide(item openresponses.Item) (openresponses.Item, bool)

Unhide returns the item Hidden wrapped and whether it was hidden. An item that was never hidden comes back unchanged.

func VisibleFilter

func VisibleFilter(visible ...string) func(Transcript) Transcript

VisibleFilter returns a filter like DefaultFilter that keeps the listed extension item types.

Types

type Agent

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

Agent is the stateful loop: a transcript, queues, subscribers and run control over Run. One run at a time; a second Prompt while one is active returns ErrRunning. The zero Agent has no model and is idle; use New.

Subscribers are called synchronously, in registration order, for every event, so every event is a barrier: the loop does not move to the next phase until each subscriber has returned, a tool that raises an event with Invoke waits for them, and so does a caller queueing an item with Agent.Steer. One event is delivered at a time, whichever goroutine raised it, so a subscriber is never entered from two goroutines at once. A subscriber that steers or prompts the agent from inside an event does so with the context it was handed, which carries the delivery it already holds, or from another goroutine. A subscriber that returns an error ends the run with ReasonError. Every event is delivered with a context whose cancellation is lifted: Agent.Abort reaches the model stream and the running tools, and the events that follow it, the tool_end of a cut-off call and the run_end among them, still reach a subscriber that writes durable state with a context it can use. A subscriber that fails while the run is being aborted ends it with ReasonAborted and its error joined to the context error on RunEnd.Err.

func New

func New(cfg Config, opts ...Option) *Agent

New builds an agent.

func (*Agent) Abort

func (a *Agent) Abort()

Abort cancels the active run, if any. The model stream and running tools see the cancellation through their context and the run ends with ReasonAborted and context.Canceled on RunEnd.Err. The queues are untouched: anything steered or queued and not yet appended goes to the next run.

func (*Agent) AbortCause added in v0.0.7

func (a *Agent) AbortCause(cause error)

AbortCause is Agent.Abort with a reason: cause is what context.Cause reports to everything the run called, and what the run ends with on RunEnd.Err, so a recorder writes it as the run's end and a product can count why its runs were cut. A stream rule that matched, an advisor that raised a blocker, a coordinator that cancelled a job and a user pressing Esc are four things a session otherwise records identically as "context canceled". A nil cause is Agent.Abort.

A cause that a caller wants errors.Is(err, context.Canceled) to keep matching should wrap it; the tools of the run see context.Canceled from their own context either way, since that is what ctx.Err reports.

func (*Agent) Config

func (a *Agent) Config() Config

Config returns the agent's configuration.

func (*Agent) Continue

func (a *Agent) Continue(ctx context.Context) (*RunEnd, error)

Continue runs from the transcript as it stands, which must satisfy CanContinue, and returns as Agent.Prompt does.

func (*Agent) Deliver added in v0.0.11

func (a *Agent) Deliver(ctx context.Context, items ...openresponses.Item) (joined bool, end *RunEnd, err error)

Deliver hands items to the model, for an input that arrives on its own time: a background task's result, a detached child's answer. The items are queued as Agent.Queue queues a steer, with the Trigger on ctx, and Deliver waits until a model call has seen them or no run will take them. They reach the turn that takes them as InputDeliver among TurnStartInfo.Inputs, where a steered item reads InputSteer, so a Config.BeforeTurn hook that acts on a user's message tells a delivery from a steer, which the transcript's tail, where an output delivered after a steer looks like a resume's, does not say.

A run in flight drains them after its batch, or when it would otherwise end, and Deliver returns joined true once the request that follows the drain has gone out. A run that decides to stop after its turn, on its turn limit, a stop hook, a guard in ShouldStopAfterTurn or a terminating result, decides before it drains and leaves them queued, as does a run past its last drain, after its final turn_end, while State.Running still reads true. Deliver then waits for that run to end and, as with an idle agent, starts a run on ctx that takes the items before its first model call, as Agent.Continue would after a steer, and returns that run's end as Prompt returns it, joined false. The stop is not repeated for the delivered items: a host that wants it to stick reads the reason on the end returned and on the run it asked for, or aborts the run Deliver started.

A run that drained the items and then ended before a model call, on a guard before the call, an abort or a failure, has them in its transcript unanswered: Deliver returns joined false with the end of the last run, which is that one unless another began and ended as well before Deliver looked, and starts nothing, since what stopped the run is the host's to look at. When no run can be started the items stay queued, and the error says why: ErrInputRequired while calls are pending, the run having stopped for input, whose Resume then takes them after its batch; ErrCannotContinue when the items do not end in something a model can answer; or ctx's error when it ended while Deliver waited.

Called from inside the agent's run, from a tool, a hook, a subscriber or a child run one of its tools made, with the context it was handed, Deliver does not wait, since the run waits on the caller: it returns joined true when the run will still drain the items, whose stop decisions then apply as they do to any steer, and ErrRunning when the run is past its last drain, when they wait for the next run. A job running on RunContext, and anything called with a context not derived from the run's, waits as any caller does.

func (*Agent) FollowUp

func (a *Agent) FollowUp(items ...openresponses.Item)

FollowUp queues items to be injected when the run would otherwise end, so the agent keeps going instead of going idle. The queue has the same life, the same Queued event and the same caveat about subscribers as Agent.Steer.

func (*Agent) Prompt

func (a *Agent) Prompt(ctx context.Context, items ...openresponses.Item) (*RunEnd, error)

Prompt appends items and runs until idle. It returns when the run_end subscribers have returned, with the RunEnd that says how the run ended: done, stopped, input_required with the pending calls, or aborted with the context error on RunEnd.Err. The error is set only when the run could not start (ErrNoPrompt, ErrRunning, ErrInputRequired, ErrNotPending, ErrNoModel) or ended with ReasonError, in which case it is RunEnd.Err and the RunEnd is returned alongside. A Trigger attached to ctx with ContextWithTrigger is carried on the run's RunStart.

While calls are pending, a prompt that opens with a function_call_output for each of them is accepted: the outputs are appended with their item events ahead of the message, so the next model call sees the answers and the message together, which is what a front wants after an abort when the user's next line is the next prompt. A leading output for a call that is not pending returns ErrNotPending; a prompt that leaves a pending call unanswered returns ErrInputRequired.

func (*Agent) Queue added in v0.0.10

func (a *Agent) Queue(ctx context.Context, mode QueueMode, items ...openresponses.Item) error

Queue is Agent.Steer or Agent.FollowUp, as mode says, for an input with a provenance of its own: the Trigger attached to ctx with ContextWithTrigger rides on each item's Queued report, so a recorder writes what brought the input in, a second person steering or a scheduled firing that overlapped a run, rather than only what started the run it joins. Nothing else is read from ctx, and it never blocks on delivery. A mode other than QueueSteer queues a follow-up. A trigger whose Extra cannot be written is refused with ErrTriggerExtra, and nothing is queued.

func (*Agent) ReserveCallIDs added in v0.0.12

func (a *Agent) ReserveCallIDs(ids ...string) error

ReserveCallIDs adds to the call IDs a call the model makes must not take, as WithReservedCallIDs does for a new agent: after Agent.SetTranscript to a session's context, with every call ID in the session. The IDs reserved earlier stay reserved. It returns ErrRunning while a run is active.

func (*Agent) Resume

func (a *Agent) Resume(ctx context.Context, answers ...Answer) (*RunEnd, error)

Resume answers the calls the last run left pending and continues, whether they were deferred to the caller or cut off by an abort or a failure. Every pending call must have exactly one answer, and no answer may name a call that is not pending. An answer is an output or an approval: a caller that refuses a call answers it with the refusal as text, which the model then sees; a caller that approves a deferred call lets the loop run it. Answer.By says who decided, for the record.

An approved call runs with the idempotency key the answer carries, else that of the dispatch it repeats, the last time it was handed to its tool, else a new one, and with the arguments the answer carries, else the ones that dispatch ran with, else its own. A call that may already have run, pending as PendingAborted, as PendingUnknown since the loop cannot say, or held after its dispatch (PendingCall.MayHaveRun), is ambiguous, and Resume applies agenttool's rule for running it again: it approves the call when its tool's replay for the arguments it would run with is safe, or keyed and either run with the arguments and under the key of the dispatch it repeats, which must be known, or with new arguments under a key the answer chose, a new operation; otherwise it returns ErrAmbiguousCall and runs nothing. Answer.WithRunAgain is the only way past it. An approval that passes the rule without a reason of its own carries the rule's, RunAgainSafeReason or RunAgainKeyedReason, which a session recorder writes on the proceed it records for the call. An agent seeded with WithPending from a record knows which calls never started, and those are not checked, and which were answered or refused without running: those take an output, and an approval of one returns ErrCallAnswered.

The outputs are appended with their item events first, then the notes of the answers that carry one, as user messages. The approved calls then run as one batch as the loop runs any batch, with BeforeToolCall skipped for a call the approval decides, one held or one that may have run. A call pending as PendingUndispatched was decided by nothing, so its approval is put to BeforeToolCall, with the approved batch as the batch: a Block or a Defer applies as it would in a run, and a call it defers leaves the run ending with ReasonInputRequired once the batch is in. tool_start, tool_update and tool_end are emitted with Turn 0, Sequential and MaxParallelTools apply, AfterToolCall runs, the outputs are appended in the calls' transcript order and their notes after them, and anything steered in meanwhile follows. A batch whose results set Terminate ends the run with ReasonStopped without calling the model, and so does any answer built with Refuse. Otherwise the model is called and the run returns as Agent.Prompt does. With nothing pending, Resume returns ErrNotPending.

func (*Agent) SetConfig added in v0.0.4

func (a *Agent) SetConfig(cfg Config) error

SetConfig replaces the configuration for the next run: model, instructions, reasoning, tools, hooks, all of it. It returns ErrRunning while a run is active. Subscribers and the queues are kept, so anything steered or queued under the old configuration goes to the next run under the new one; a session recorder attached to the agent sees the change as a config delta on the next turn.

The transcript is kept too, reasoning items included, and the loop owns what a change of model means for them: a reasoning item carries a signature only its own provider accepts, so each request leaves out the ones a model with another Config.ModelName produced, which the agent attributed as its runs went (see ReasoningModels). The transcript and a record of it keep them, and a configuration that switches back to that model is sent them again. A configuration with an empty ModelName names no model, so nothing is attributed to it and nothing is left out of its requests. A session recorder cannot write a request that leaves items out of the middle of its history, so the responses after such a change carry no request hash.

func (*Agent) SetPending added in v0.0.11

func (a *Agent) SetPending(pending []PendingCall) error

SetPending says why the calls the transcript leaves without an output are pending, and with what key and arguments they were handed to their tools, as WithPending does for a new agent: after Agent.SetTranscript to a branch of a session, with what the session package's Pending reads there. Each is matched to a pending call by its call ID, with the same name and arguments, and ignored when it matches none. It returns ErrRunning while a run is active.

func (*Agent) SetTranscript added in v0.0.4

func (a *Agent) SetTranscript(t Transcript) error

SetTranscript replaces the transcript, as when switching to another branch of a session, or when a handoff trims what the receiving configuration sees, between runs and once. It returns ErrRunning while a run is active. A session recorder writing the agent is moved to match with its Rebase: to the entry of the last item kept, for a trim that keeps a prefix, whose responses then carry hashes; a trim from the middle runs as well and leaves the next responses without one. A handoff to another model need not trim the previous model's reasoning items: the loop leaves them out of each request, as Agent.SetConfig says, for every item it attributed, which the new transcript keeps when it holds the same item. A transcript rebuilt from elsewhere has no attribution until WithReasoningModels or ContextWithReasoningModels gives it. The pending calls are derived from the new transcript as WithTranscript derives them, except that a call the agent already had pending, the same call under the same ID, keeps its reason, key and arguments: a held call stays held. Any other is PendingUnknown, so an approval of it is held to the replay rule as for any call that may have run, until Agent.SetPending says what a record knows of it. Whatever the old transcript was waiting on and the new one does not hold is forgotten, and whatever the new one is waiting on must be answered through Agent.Resume. Queued Steer and FollowUp items are kept and go to the next run on the new transcript; a host that does not want them there reads them from Agent.State first. The call IDs the old transcript holds stay reserved, as Agent.ReserveCallIDs reserves them, since a session that recorded them holds them on the branch the agent left.

func (*Agent) State

func (a *Agent) State() State

State returns a snapshot.

func (*Agent) Steer

func (a *Agent) Steer(items ...openresponses.Item)

Steer queues items to be injected after the current tool batch, before the next model call. When the agent is idle they are consumed by the next run before its first model call, after its prompt, or after the batch of calls a Resume approved.

Each item is reported to the subscribers as a Queued event, so a host writing what it accepted has it before the run appends it and can tell an item it accepted from one a run produced. The report is not the accept: the item is queued when this returns, and the event is delivered by whichever goroutine owns delivery, at its next event. A run in flight reports it before its next event; an idle agent reports it at the start of the next run, so a host that must not lose an input writes it before calling here rather than from the event. A run in flight takes it unless it is past its last drain, after its final turn_end or once it has decided to stop, when it waits for the next run like an item steered into an idle agent; a host whose input arrives on its own time and must reach the model calls Agent.Deliver instead. A run that stops after a turn, on its turn limit, a stop hook or a terminating result, decides so before it drains, so what was steered during that turn is not appended to it: the next run takes it, after its prompt, so a host that wants it answered first calls Agent.Continue rather than Prompt.

It never blocks on the delivery barrier, so steering from inside a subscriber is safe: the item is queued and the event follows on the next one.

The queues live in memory: an item accepted here is in no record until a run appends it or a subscriber writes it, and it survives Agent.Abort, [SetConfig] and [SetTranscript] but not the process. A session recorder writes each Queued report as a queued entry and hands the inputs a resumed session owes back with its Requeue; a host without one reads the queues back from Agent.State and queues them again after a restart.

A steered item is appended with its own item events, which reach every subscriber. A subscriber that steers in reaction to an event the steered item itself produces, an item_end during a run for instance, feeds the run forever, and nothing reports it: the loop cannot tell a reaction from a fresh input. Steer from a front, a monitor or another goroutine, or from a subscriber only on events it can tell apart from its own items, such as a tool_end or a specific item type it never steers.

func (*Agent) Subscribe

func (a *Agent) Subscribe(fn func(context.Context, Event) error) (unsubscribe func())

Subscribe registers fn for every event and returns a function that removes it. Subscribing during a run takes effect from the next event.

func (*Agent) WaitForIdle

func (a *Agent) WaitForIdle(ctx context.Context) error

WaitForIdle blocks until no run is active or ctx is done. An agent that has never run is idle.

type Answer added in v0.0.5

type Answer struct {
	// CallID names the pending call.
	CallID string
	// Output, when set, answers the call without running it: a refusal
	// as text, or the result of a call the caller ran itself.
	Output *openresponses.FunctionCallOutput
	// Args, for an approval, replaces the arguments the tool receives,
	// as ToolDecision.Args does. nil keeps the call's own.
	Args json.RawMessage
	// Note is what the user said when answering: it is appended as a
	// user message after the outputs of every answer of the Resume, so
	// the model reads the result and the note together, in that order,
	// in the same turn.
	Note string
	// Terminate ends the run with ReasonStopped and StopRefused once
	// every answer is in, without calling the model, for a refusal that
	// should end the turn so the user can say what to do instead. The
	// outputs are still appended, so the transcript stays a valid input.
	Terminate bool
	// By names who decided this answer, for the record: the session
	// format knows "human" for a person at a prompt, "policy" for a
	// rule that answered on its own and "agent" for another model. It
	// rides on the ToolDecision the loop synthesises for an approval,
	// and a session recorder writes it on the decision for an answer of
	// either kind. There is no default: a policy engine answers through
	// Resume as often as a person does, so an answer that names nobody
	// is recorded as an anonymous decision rather than guessed at.
	By string
	// Reason says why the call was answered so. For an approval it
	// rides on the ToolDecision as By does, and a session recorder
	// writes it on the proceed decision. An approval of a call that may
	// have run that gives none carries the rule that let it run again:
	// [RunAgainSafeReason], [RunAgainKeyedReason] or [RunAgainReason]. For an output answering a call
	// that may have run, it rides on the run's context with
	// [ContextWithReasons], and a session recorder writes it on the
	// answer decision before the output: "not run again: replay
	// unknown", say. An output for a call that did not run is its own
	// reason, since it is what the model sees, and this is not recorded.
	Reason string
	// Origin, for an output taken from a record rather than produced
	// now, names where it was taken from, in the terms of the library
	// that read it: agentturn/session names the entry of the output the
	// answer repeats, on a branch a rebase left or in the session this
	// one forks. The loop carries it unread, on the run's context with
	// [ContextWithOrigins] as it carries Reason, so a session recorder
	// can tie the output it writes to the one it repeats and to the
	// child session that produced it. Empty for any other answer.
	Origin string
	// IdempotencyKey, for an approval, is the key the tool receives in
	// place of the one the loop would give it: the pending call's, for
	// a call that may have run, and a new one otherwise. A key names one
	// operation, so a key other than that of the dispatch the approval
	// repeats makes the call a new operation: for a keyed call that may
	// have run, Resume takes one only with new arguments, or with
	// [Answer.WithRunAgain]. A host that resumes after a restart and
	// wants the operation repeated leaves it empty, or sets the key of
	// that dispatch, which a session recorder wrote on it.
	IdempotencyKey string
	// RunAgain, for an approval, says the host accepts running a call
	// that may have run although its tool does not say that is safe.
	// It is the only way past the replay rule [Agent.Resume] applies,
	// and the proceed is recorded with [RunAgainReason] when the
	// answer gives no reason of its own.
	RunAgain bool
}

Answer resolves one pending call for Agent.Resume: an output the caller produced, or an approval that runs the call inside the loop. Build one with Output, Approve, ApproveWith, Refuse or OutcomeUnknown, attach what the user said with Answer.WithNote and who said it with Answer.WithBy.

func Approve added in v0.0.5

func Approve(callID string) Answer

Approve runs the pending call with the arguments it is pending with, PendingCall.Args, which a decision that held it or a hand-off it may have run in gave it, else those the model gave.

func ApproveWith added in v0.0.5

func ApproveWith(callID string, args json.RawMessage) Answer

ApproveWith runs the pending call with args in place of the model's and of any PendingCall.Args.

func OutcomeUnknown added in v0.0.11

func OutcomeUnknown(callID string) Answer

OutcomeUnknown answers a pending call that may have run and must not run again with the error agenttool's replay rule asks for, so the model sees that the call may or may not have taken effect and can check before it asks again. It carries OutcomeUnknownReason.

func Output added in v0.0.5

Output answers a pending call with out.

func Refuse added in v0.0.6

Refuse answers a pending call with out and ends the run instead of calling the model: Output with Terminate set.

func (Answer) WithBy added in v0.0.7

func (a Answer) WithBy(by string) Answer

WithBy returns the answer with by attached: who decided it, in the session format's terms ("human", "policy", "agent").

func (Answer) WithIdempotencyKey added in v0.0.11

func (a Answer) WithIdempotencyKey(key string) Answer

WithIdempotencyKey returns the approval with key as the key its tool receives.

func (Answer) WithNote added in v0.0.6

func (a Answer) WithNote(note string) Answer

WithNote returns the answer with note attached.

func (Answer) WithOrigin added in v0.0.16

func (a Answer) WithOrigin(origin string) Answer

WithOrigin returns the output answer with origin attached: where the output was taken from, in the terms of the library that read it, for an output a record held rather than one produced now.

func (Answer) WithReason added in v0.0.11

func (a Answer) WithReason(reason string) Answer

WithReason returns the answer with reason attached: why an approval runs the call, or why an output answers a call that may have run without running it again.

func (Answer) WithRunAgain added in v0.0.11

func (a Answer) WithRunAgain() Answer

WithRunAgain returns the approval with Answer.RunAgain set: the call runs even if it may have run and its tool does not say it can run again, whatever its side effect.

type AskedCall added in v0.0.16

type AskedCall struct {
	// Parent is the ID of the call whose tool made this one, and empty
	// for a call the model made.
	Parent string
	CallID string
	Name   string
	// Args are the arguments the call would run with, whole, where the
	// question's text quotes the first 500 bytes.
	Args json.RawMessage
	// Decision is the hook's deferral as it stood when the question was
	// asked; its Reason is the rule that asked.
	Decision *ToolDecision
}

AskedCall is a call the loop, or a front, is putting to the user through the agenttool.Elicitor on the elicitor's context: a nested call the hook deferred, whose Parent is the call that made it, or a call a front asks about itself, with no Parent (front/a2a asks about a call to the agent's own tool that its hook held, since its caller answers only calls to the tools it owns). A front's elicitor reads it with AskedCallFrom to show choices of its own, or to turn an answer into a rule from Name and Args, where the question's text alone would have to be parsed. The question is still filed under the invoking call, which stays on the context as agenttool.CallFrom.

func AskedCallFrom added in v0.0.16

func AskedCallFrom(ctx context.Context) (AskedCall, bool)

AskedCallFrom returns the call the elicitor on ctx is being asked about, when Ask put one there.

type Config

type Config struct {
	// Name is how the agent names itself when composed: the tool name in
	// tools/agent, the server name in front/mcp, the agent card in
	// front/a2a.
	Name string
	// Description is one paragraph about the agent, used the same way.
	Description string

	// Model streams responses. Required.
	Model Model
	// ModelName is the model field of every request.
	ModelName string
	// Instructions is the instructions field of every request.
	Instructions string

	// Tools the model may call. The loop reads it at the start of each
	// turn.
	Tools []agenttool.Tool
	// ToolProvider, when set, supplies the tools for each turn in place
	// of Tools, so a source whose tool list changes, such as a remote
	// MCP server, is picked up on the next turn. It is called once per
	// turn, before the model call, and the same list serves the turn's
	// tool batch, so a call resolves against the tools the model was
	// offered.
	//
	// A provider that returns a snapshot offers a change one turn late
	// when the change is still in flight as the turn starts: a tool
	// whose result announces a new tool returns before the refresh that
	// fetches it has completed. A provider that must offer the change
	// on the very next call waits for it here, bounded by ctx, as
	// mcpclient's Await does:
	//
	//	cfg.ToolProvider = func(ctx context.Context) []agenttool.Tool {
	//		ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	//		defer cancel()
	//		_ = remote.Await(ctx)
	//		return remote.Tools()
	//	}
	ToolProvider func(ctx context.Context) []agenttool.Tool

	// Reasoning is the reasoning field of every request: effort and
	// summary. The zero value leaves the request's own.
	Reasoning openresponses.ReasoningConfig
	// Text is the text field of every request: output format and
	// verbosity. The zero value leaves the request's own.
	Text openresponses.TextConfig

	// Request is the base of every request the loop sends: tool_choice,
	// parallel_tool_calls, max_output_tokens, temperature, truncation,
	// include, safety_identifier, prompt_cache_key, service_tier and
	// every other member are copied from it. The loop owns input, tools,
	// store, stream and previous_response_id, and ModelName,
	// Instructions, Reasoning and Text take precedence over the same
	// members here when set. The loop adds nothing else: the run and
	// turn IDs are on every event, not on the request, so the settings
	// the model sees change only when this config does.
	Request openresponses.Request

	// BeforeTurn runs at the start of each turn, before the request is
	// built, and returns items the loop appends to the transcript with
	// their item events, as it appends a queued message: the time, a
	// reminder due, the state of the environment, whatever the model
	// needs this turn. The items are then facts about the transcript, so
	// a recorded session rebuilds the request they were part of, which
	// injection through Transform cannot give. nil or no items appends
	// nothing. A guard on the request itself belongs in BeforeModelCall.
	// An error wrapping [ErrGuard] is how a policy, a cost limit or a
	// deadline, stops the run before the turn's model call: the run ends
	// with ReasonStopped, StopGuard and the error on RunEnd.Err, and the
	// turn has no turn_start. Any other error ends it with ReasonError.
	//
	// It is one field and several layers want it. Assigning it twice
	// keeps the second assignment and loses the first with no error and
	// no sign, so a product with more than one layer joins them with
	// [ChainBeforeTurn], which appends what each returns in order.
	BeforeTurn func(context.Context, TurnStartInfo) (openresponses.Items, error)

	// BeforeModelCall runs on the fully built request of each turn, just
	// before it is sent and before turn_start reports it. It may change
	// anything. A change to Input is one the record cannot describe, so
	// a session recorder writes that call without a request hash; a
	// change to a setting is recorded as a config delta. A hook that
	// refuses the call ends the run with ReasonError after a
	// [ModelBlocked] event carrying the request as built; an error that
	// wraps [ErrGuard] is a policy stopping the run before the call,
	// which raises the same ModelBlocked and ends the run with
	// ReasonStopped and StopGuard. A guard that
	// calls a model is on the critical path of every first token, since
	// the request is not final until the hook returns.
	//
	// This is the most contested field in the package: a memory that
	// re-renders its block into the instructions and a guard that
	// inspects what is about to be sent both want it, and assigning it
	// twice keeps the second assignment silently. Join them with
	// [ChainBeforeModelCall], in the order they must run: a hook that
	// edits the request before one that inspects it.
	BeforeModelCall func(context.Context, *openresponses.Request) error

	// OutputGuard runs on each assistant message as the stream completes
	// it, after output_item.done and before the message is appended to
	// the transcript and delivered as item_end, and may replace it with
	// another message, a placeholder for one that must not reach the
	// user, the transcript or the record. nil keeps the message. The
	// deltas of the original have already been delivered as item_update,
	// so a front that must not show withheld text renders on item_end.
	// Function calls, reasoning and every other output item never reach
	// it, so a replay still has what it needs. A message the model
	// speaks before a function call reaches it too, with nothing yet
	// saying a call follows; see [OutputInfo]. An error wrapping
	// [ErrGuard] withholds the message and ends the run as a policy
	// stop: the message is not appended and has no item_end, the loop
	// reads the rest of the response for its usage and raises a
	// [ResponseEnd] with Withheld set, and the run ends with
	// ReasonStopped, StopGuard, RunEnd.Withheld and the error on
	// RunEnd.Err. A function call the same response added to the
	// transcript before the message, which the loop does once a
	// message or a call of the response has opened with
	// output_item.added, is never dispatched, and
	// the loop appends an output with the fixed text
	// [WithheldCallOutput] for it before the run ends, so the
	// transcript holds no call without an output and the next prompt
	// goes ahead. On a stream that sends output_item.done alone, the
	// calls before the message are still held and are dropped with
	// it. The guard's error is never that output's
	// text. Any other error fails the run with ReasonError. A guard that keeps
	// or replaces the message and then wants the run to end returns an
	// error wrapping [ErrGuard] from ShouldStopAfterTurn, which sees the
	// turn with TurnInfo.Final set.
	// Several guards are joined with [ChainOutputGuard], each seeing
	// what the one before it left.
	OutputGuard func(context.Context, OutputInfo) (*openresponses.Message, error)

	// Retry is the policy for transient model failures. The zero value
	// retries nothing; see [Retry].
	Retry Retry

	// ToolExecution selects parallel (default) or sequential batches.
	ToolExecution ExecutionMode
	// MaxParallelTools bounds a parallel batch; zero means
	// agenttool.DefaultMaxParallel.
	MaxParallelTools int
	// ToolRecorder, when set, is agenttool.Executor.Recorder for every
	// batch: installed on each call's context, and on [RunContext] for
	// the work a call leaves running, so a tool that writes a record
	// with agenttool.WriteRecord, while it runs or from a job it
	// started, reaches the host without the host threading a context
	// through Prompt. A session
	// recorder offers one as session.Recorder.RecordFunc. nil leaves a
	// recorder already on the run's context in place and installs
	// nothing else, so under nil a tool's WriteRecord is a no-op.
	ToolRecorder agenttool.RecordFunc
	// ToolElicitor, when set, is installed on the context of every tool
	// call with agenttool.ContextWithElicitor, so a question a tool asks
	// the user mid-call, an MCP server's elicitation through mcpclient
	// among them, reaches the host with the call on its context. A
	// session recorder wraps one with session.Recorder.Elicitor so the
	// question and the answer are written under the call. The loop asks
	// it too, with [Ask], about a nested call the hook defers, with the
	// call on its context for [AskedCallFrom]. nil leaves an
	// elicitor already on the run's context in place, and a tool with
	// none asks nobody: an agent run from inside a tool served by
	// agenttool's mcpserver, whose call context carries the elicitor
	// that asks the MCP client, then puts its tools' questions to that
	// client.
	ToolElicitor agenttool.Elicitor
	// MaxTurns stops a run after this many turns with ReasonStopped and
	// StopMaxTurns when its last turn called tools or anything is
	// queued, which stays queued for the next run; a last turn that
	// called none with nothing queued ends the run done. Zero means no
	// limit.
	MaxTurns int

	// Filter drops app-only items before each model call and returns
	// the conversation the model sees. nil means [DefaultFilter].
	Filter func(Transcript) Transcript
	// Transform runs before each model call on the whole transcript and
	// may return a shorter or otherwise edited one for that call only:
	// prune, compact, inject context. It receives a copy of the slice
	// and must not mutate the items. The working transcript is not
	// replaced. A one-shot change to the history, as when another agent
	// takes over the conversation and should see less of it, belongs in
	// [Agent.SetTranscript] between runs: a transform that drops items
	// does so on every call, the receiver's own tool outputs included.
	Transform func(context.Context, Transcript) (Transcript, error)

	// BeforeToolCall runs once per call, in the model's order, before any
	// call of the batch executes. A nil decision allows the call.
	// Several policies are joined with [ChainBeforeToolCall], which
	// folds their decisions deny over ask over allow; assigning the
	// field twice keeps only the second policy.
	//
	// One call at a time reaches it, a nested call made with [Invoke]
	// from a tool's own goroutine included, so a policy may keep state
	// without a lock of its own. Calling Invoke from inside the hook
	// waits for the hook to return, which it cannot do.
	BeforeToolCall func(context.Context, ToolCallInfo) (*ToolDecision, error)
	// AfterToolCall runs when a call completes and may replace its
	// result. A nil override keeps the result. An override replaces the
	// result before tool_end is delivered and before the output is
	// appended, so no subscriber and no recorder sees what the tool
	// returned: bytes cut here exist nowhere afterwards. A cap on tool
	// output therefore belongs in the tool, the only place the whole
	// output exists, which can cut the middle so both ends survive and
	// leave the full bytes where the model can read them, naming that
	// place in the text it returns; a Transform, which shapes one call
	// and never replaces the transcript, is the other placement that
	// keeps the record whole. This hook is for a policy on the result
	// the model sees, not for saving space. One result at a time
	// reaches it, whichever goroutine finished the call, as for
	// BeforeToolCall.
	//
	// The next call of the call's chain, every later call of a serial
	// batch or the next call naming the same agenttool.Resource, is not
	// dispatched until the hook has returned, so state the hook changes
	// or captures, a checkpoint or a recorded move of the workspace,
	// falls between the two. A call in another chain may run alongside
	// it.
	AfterToolCall func(context.Context, ToolResultInfo) (*ToolOverride, error)
	// ShouldStopAfterTurn ends the run after a turn even when the model
	// requested tools: true ends it with ReasonStopped and StopHook. An
	// error wrapping [ErrGuard] ends it with ReasonStopped, StopGuard
	// and the error on RunEnd.Err, so a policy that stops a run is told
	// apart from a failure; any other error ends it with ReasonError.
	//
	// A guard chain and a token budget both want this field. Join them
	// with [ChainShouldStopAfterTurn], which stops at the first hook
	// that stops the run, so the error on RunEnd.Err is that hook's and
	// says which one fired.
	ShouldStopAfterTurn func(context.Context, TurnInfo) (bool, error)

	// RequestExtra is passed through as Request.Extra on every call.
	RequestExtra map[string]any
}

Config describes an agent. The zero value is not usable: Model is required.

func (Config) BaseRequest

func (c Config) BaseRequest(ctx context.Context) openresponses.Request

BaseRequest returns the request every turn starts from: Config.Request with the loop-owned transport members set (store false, stream true, no previous_response_id), ModelName, Instructions, Reasoning and Text applied over it, RequestExtra merged over its Extra, and the tool definitions of the moment. Only Input is added by the loop; a recorder uses this to write the initial settings.

func (Config) ResolveTools added in v0.0.4

func (c Config) ResolveTools(ctx context.Context) []agenttool.Tool

ResolveTools returns the tools of the moment: ToolProvider's answer when it is set, Tools otherwise. The loop consults it once per turn, before the model call; a front that builds its own request uses it so the provider fallback lives in one place.

type Event

type Event interface {
	EventType() string
}

Event is one step of a run. Concrete types are RunStart, TurnStart, ModelRetry, ModelBlocked, ItemStart, ItemUpdate, ItemEnd, ResponseEnd, ToolStart, ToolDispatch, ToolUpdate, ToolEnd, TurnEnd and RunEnd, and Queued, which belongs to no run. Decoded values are pointers, so switch on *ItemUpdate and so on.

type ExecutionMode

type ExecutionMode int

ExecutionMode selects how a batch of tool calls runs.

const (
	// ExecParallel runs the calls of a batch concurrently, bounded by
	// [Config.MaxParallelTools], unless a tool in the batch is
	// agenttool.Sequential.
	ExecParallel ExecutionMode = iota
	// ExecSequential runs every batch one call at a time in the model's
	// order.
	ExecSequential
)

type InputMode added in v0.0.16

type InputMode string

InputMode says how an item among a turn's inputs arrived.

const (
	// InputPrompt is an item the run was prompted with: [Agent.Prompt]'s
	// items, or [Run]'s prompts.
	InputPrompt InputMode = "prompt"
	// InputResume is an output or a note an [Agent.Resume] appended, or
	// one of the leading outputs a Prompt opened with for the pending
	// calls.
	InputResume InputMode = "resume"
	// InputSteer is an item drained from the steer queue: [Agent.Steer],
	// or [Agent.Queue] with [QueueSteer].
	InputSteer InputMode = "steer"
	// InputFollowUp is an item drained from the follow-up queue:
	// [Agent.FollowUp], or [Agent.Queue] with [QueueFollowUp].
	InputFollowUp InputMode = "follow_up"
	// InputDeliver is an item handed in with [Agent.Deliver], which
	// queues it as a steer; a hook tells the two apart here.
	InputDeliver InputMode = "deliver"
	// InputHook is an item [Config.BeforeTurn] appended.
	InputHook InputMode = "hook"
	// InputTool is a function_call_output the loop appended for a call
	// it ran, the previous batch's or a Resume's approved batch's, and
	// the note a decision attached after the batch's outputs.
	InputTool InputMode = "tool"
	// InputContinued is an item that was already in the transcript,
	// after its last model output, when the run started: what an
	// [Agent.Continue] answers, such as the outputs of the run before
	// it. Function call outputs, user and developer messages and
	// namespaced custom items count; anything the model produced ends
	// the tail.
	InputContinued InputMode = "continued"
)

type ItemEnd

type ItemEnd struct {
	RunID string
	Turn  int
	Item  openresponses.Item
	// ResponseID is the ID of the response that produced the item, and
	// empty for an item the loop appended itself. It is empty too for a
	// model's item that completed before the stream named its response,
	// one that sends no response.created or response.in_progress: the
	// response's ID arrives on its ResponseEnd. The session recorder
	// holds such an item while the model call is in flight and writes
	// it, in order, with the response's ID once the response ends.
	ResponseID string
	// Hidden is set for an item the caller marked with [Hidden]: it is
	// in the model's context and a renderer should not show it. A
	// session recorder writes its entry with visible false.
	Hidden bool
	// Trigger, for an item the run was prompted with, is the run's
	// [Trigger], so a recorder can write how the input arrived; it is
	// zero for every other item, a queued input included, whose own
	// trigger rode on its [Queued] report, and an output [Agent.Resume]
	// appends for a pending call, whose decision says who gave it.
	Trigger Trigger
	// ModelCallID is the call ID the model gave a function call the
	// loop gave an ID of its own, because the model's named a call in
	// the transcript or one the agent reserved. It is empty for every
	// other item, and for a call the model gave no ID.
	ModelCallID string
}

ItemEnd carries a completed item. For an assistant item it is emitted only after output_item.done; partial items never arrive here. The item is in the transcript when this event is delivered. A function call the model gave no call ID, or one another call already has, carries an ID of the loop's own, the model's with a random suffix, here as on its ItemStart and every ItemUpdate, and ModelCallID keeps the model's: a call ID names one call.

func (*ItemEnd) EventType

func (*ItemEnd) EventType() string

EventType returns "item_end".

type ItemStart

type ItemStart struct {
	RunID string
	Turn  int
	Item  openresponses.Item
	// ResponseID is the ID of the response streaming the item, and empty
	// for an item the loop appended itself. It is empty too while the
	// stream has not named its response, one that sends no
	// response.created or response.in_progress before its items.
	ResponseID string
	// Hidden is set for an item the caller marked with [Hidden]: it is
	// in the model's context and a renderer should not show it. The
	// Item is the item itself, unwrapped.
	Hidden bool
}

ItemStart announces an item entering the transcript: a prompt or queued message, an assistant item as the stream opens it, or a function call output. For an assistant item the Item is the live accumulated value and fills in as updates arrive. A function call the loop gave an ID of its own (see ItemEnd) is the exception: its Item is a copy carrying that ID, taken as the call opened, not the live accumulator, so it does not fill in; each of its ItemUpdate events carries a fresh copy with the same ID, and its ItemEnd the completed call.

An item the model completes before its attempt commits, a reasoning summary before the first token, is announced here and reaches the transcript when the answer begins or the response arrives (see Retry). An attempt that ends without its response drops what it held, so a front that renders from item_start drops what it was rendering when a ModelRetry, or a run_end with an error or an abort, follows with no item_end for it.

func (*ItemStart) EventType

func (*ItemStart) EventType() string

EventType returns "item_start".

type ItemUpdate

type ItemUpdate struct {
	RunID  string
	Turn   int
	Item   openresponses.Item
	Stream openresponses.StreamEvent
	// ResponseID is the ID of the response streaming the item, and
	// empty while the stream has not named its response.
	ResponseID string
}

ItemUpdate carries one wire event for an assistant item. Stream is the openresponses event verbatim, so a front switches on the same concrete types it would use against a remote server. Item is the accumulated item so far.

func (*ItemUpdate) EventType

func (*ItemUpdate) EventType() string

EventType returns "item_update".

type Model

type Model = openresponses.Streamer

Model is anything that can stream an Open Responses response.

type ModelBlocked added in v0.0.6

type ModelBlocked struct {
	RunID   string
	Turn    int
	Request openresponses.Request
	Err     error
}

ModelBlocked reports that Config.BeforeModelCall refused the turn's request, so no call was made: Request is the request as built when the hook ran and Err is the hook's error. It is the last event before the run ends, in place of the turn_start the call would have had, so a recorder can write the call that was refused as distinct from one that was made and failed. The run ends with ReasonError, or with ReasonStopped and StopGuard when Err wraps ErrGuard.

func (*ModelBlocked) EventType added in v0.0.6

func (*ModelBlocked) EventType() string

EventType returns "model_blocked".

type ModelRetry added in v0.0.4

type ModelRetry struct {
	RunID string
	Turn  int
	// Attempt is the number of the attempt that failed, from 1; the
	// next attempt is Attempt+1.
	Attempt int
	Err     error
	Delay   time.Duration
	// Request is what the next attempt will send: the turn's request,
	// or what [Retry.Revise] made of it. A recorder settles on it as it
	// does on turn_start, so a fallback to another model reaches the
	// path as a config delta.
	Request openresponses.Request
}

ModelRetry reports that a model call failed and will be attempted again after Delay, under Config.Retry. It follows the turn_start of the turn; the failed attempt never began an answer, and anything it completed on the way was dropped with it.

func (*ModelRetry) EventType added in v0.0.4

func (*ModelRetry) EventType() string

EventType returns "model_retry".

type Option

type Option func(*Agent)

Option configures a new Agent.

func WithPending added in v0.0.11

func WithPending(pending []PendingCall) Option

WithPending says why the calls the seeded transcript leaves without an output are pending, and with what key they were handed to their tools, from a record that knows more than the transcript does: a session's dispatch entries tell a call that never started from one that may have run. Each is matched to a pending call by its call ID, with the same name and arguments; one that matches none is ignored, and a pending call it does not list stays PendingUnknown. A call it lists as never started, or as deferred before any dispatch, is approved without the replay rule Agent.Resume applies to the others, and one it lists as answered takes only an output. It applies to the transcript WithTranscript gives, whichever option comes first; for one Agent.SetTranscript sets later, Agent.SetPending does the same. The session package's AgentOptions gives both options for a stored session.

func WithReasoningModels added in v0.0.15

func WithReasoningModels(m ReasoningModels) Option

WithReasoningModels says which model produced the reasoning items of the transcript WithTranscript gives, from what the host knows of it, so the loop leaves another model's out of each request as it does for items its own runs produced. A front that rebuilds a conversation from a store or from a caller's input attributes them to the configurations that had it.

func WithReservedCallIDs added in v0.0.12

func WithReservedCallIDs(ids []string) Option

WithReservedCallIDs names call IDs a call the model makes must not take although the transcript does not hold them: the calls a compaction folded out of a session's context, or a trim dropped, are still on its path, those of a branch a rewind or a fork left are still in the session, and a provider that numbers its calls per response repeats their IDs. A call that repeats one runs under an ID of the loop's own, as a call repeating one in the transcript does. The session package's AgentOptions gives every call ID in a stored session.

func WithTranscript

func WithTranscript(t Transcript) Option

WithTranscript starts the agent from an existing transcript, as when resuming a session. Function calls anywhere in it that have no output are pending, with PendingUnknown as their reason since the loop cannot say whether they ran, as they would be after the run that made them: Prompt and Continue return ErrInputRequired until Agent.Resume has answered them, or a Prompt opens with their outputs. Resume holds an approval of such a call to the replay rule, since it may have run; WithPending says which ones did not.

type OutputInfo added in v0.0.6

type OutputInfo struct {
	RunID      string
	Turn       int
	ResponseID string
	// Message is the message as the model produced it.
	Message *openresponses.Message
	// Output holds the items of the response that precede the message,
	// in the order they opened, which is output order on a stream that
	// gives each item an index of its own: a reasoning item, a message
	// the model spoke before this one. Do not mutate them.
	Output openresponses.Items
}

OutputInfo describes an assistant message the stream has completed, for Config.OutputGuard.

Whether the message is the answer is not known when the guard runs: a model may speak before it calls a function, in the same response, and the calls it makes arrive after the message. The guard sees every assistant message as it completes and is not held until the response does, since that would hold every message behind the whole response. A guard that treats a message as final checks TurnInfo.Final in ShouldStopAfterTurn instead, where the turn is complete.

type PendingCall added in v0.0.6

type PendingCall struct {
	Call   *openresponses.FunctionCall
	Reason PendingReason
	// Tool is the tool the call resolved to in the run that made it, so
	// a prompt can show what it asks about, its annotations and where
	// it runs; nil for a call no tool has the name of and for a call the
	// run did not make, one found in a seeded transcript.
	Tool agenttool.Tool
	// Dispatched, for a deferred call, says it was handed to its tool
	// before it was held: a record holds a hold after the call's
	// dispatch, so the call may have run and waits on someone to say
	// whether it runs again. Its approval is held to the replay rule as
	// an aborted call's is, with IdempotencyKey and Args. The loop's
	// own BeforeToolCall defers a call before it is handed over, so
	// only [WithPending] and [Agent.SetPending] set it.
	Dispatched bool
	// IdempotencyKey is the key the call carried the last time it was
	// handed to its tool, for a call that may have run, and empty
	// otherwise: the key of the dispatch an approval repeats. An
	// approval of the call through [Agent.Resume] runs it with this
	// key unless the answer carries its own.
	IdempotencyKey string
	// Args are the arguments the call runs with when it is approved:
	// for a call that may have run, those its last hand-off gave the
	// tool, and for a deferred one, those the decision that held it
	// rewrote them to, so an approval runs what was decided on. They
	// are nil when they are the call's own, or the call was neither
	// handed over nor rewritten. An approval runs the call with them
	// unless the answer carries its own.
	Args json.RawMessage
	// Ran is the output the call has where it ran, when a record shows
	// it dispatched and completed off the path the agent continues: on
	// a branch a rebase left, or in the session this one forks. The
	// loop never sets it; agentturn/session's Pending does, from the
	// record, and its ReplayAnswers answers the call with it rather
	// than running the call again, as a host answering the call itself
	// should. nil for every other call.
	Ran *openresponses.FunctionCallOutput
	// RanWhere says where the call ran, for a call with Ran: the reason
	// an answer giving that output carries.
	RanWhere string
	// Refused is the reason the call was refused, for a call pending as
	// [PendingRejected]: what a reject decision said before the output
	// that carries it was written. The loop never sets it;
	// agentturn/session's Pending does, from the record, and the output
	// such a call is owed is this text, as ReplayAnswers gives it.
	Refused string
}

PendingCall is a function call with no output and the reason it has none.

func (PendingCall) MayHaveRun added in v0.0.11

func (p PendingCall) MayHaveRun() bool

MayHaveRun reports whether the call may have run and an approval of it is held to agenttool's replay rule: it is pending as PendingAborted or PendingUnknown, or deferred after it was dispatched. A call pending as PendingAnswered may have run too, but it is owed an output and no approval, as one pending as PendingRejected is.

type PendingReason added in v0.0.6

type PendingReason string

PendingReason says why a call has no output.

const (
	// PendingDeferred: BeforeToolCall handed the call to the caller and
	// nothing has answered it. The tool did not run.
	PendingDeferred PendingReason = "deferred"
	// PendingAborted: the run was aborted or failed after the call was
	// handed to its tool, in this run or an earlier one. The tool may
	// have run to completion, so its side effect may have happened, and
	// the call is ambiguous: [Agent.Resume] runs it again only as
	// agenttool's replay rule allows.
	PendingAborted PendingReason = "aborted"
	// PendingUndispatched: the loop did not hand the call to its tool,
	// so the tool did not run: a subscriber refused its tool_dispatch,
	// or the run was aborted or failed before the call's turn. A
	// recorder registered before a subscriber that refused has already
	// written the dispatch; see [ToolDispatch]. Nothing has decided to
	// run it, so an approval of it through [Agent.Resume] is put to
	// BeforeToolCall as the call would be in a run.
	PendingUndispatched PendingReason = "undispatched"
	// PendingUnknown: the call was found without an output in a
	// transcript the agent was seeded with, so the loop cannot say
	// whether it ran. A session recorded with dispatch entries can, and
	// [WithPending] seeds the agent with what it says.
	PendingUnknown PendingReason = "unknown"
	// PendingAnswered: a record says the call was answered without
	// being run again, and stopped before the answer's output: a crash
	// between the two. It is owed its output and nothing else, so
	// [Agent.Resume] answers it only with an output and refuses an
	// approval with [ErrCallAnswered]. Only [WithPending] and
	// [Agent.SetPending] give it; a live run never leaves one.
	PendingAnswered PendingReason = "answered"
	// PendingRejected: a record says a decision refused the call before
	// it reached its tool, and stopped before the refusal's output: a
	// crash between the two. The tool did not run, and the call is owed
	// that refusal as its output and nothing else, so [Agent.Resume]
	// answers it only with an output and refuses an approval with
	// [ErrCallAnswered]. Only [WithPending] and [Agent.SetPending] give
	// it; a live run never leaves one.
	PendingRejected PendingReason = "rejected"
)

Pending reasons.

type QueueMode added in v0.0.7

type QueueMode string

QueueMode says which queue an item was accepted into.

const (
	// QueueSteer is [Agent.Steer]: the item joins the run after the
	// current tool batch, before the next model call.
	QueueSteer QueueMode = "steer"
	// QueueFollowUp is [Agent.FollowUp]: the item joins the run when it
	// would otherwise end.
	QueueFollowUp QueueMode = "follow_up"
)

Queue modes.

type Queued added in v0.0.7

type Queued struct {
	RunID string
	Item  openresponses.Item
	// Mode is the queue the item joined. An item handed in with
	// [Agent.Deliver] joins the steer queue and is reported with
	// QueueSteer; the turn that drains it tells it apart as
	// [InputDeliver] in [TurnStartInfo.Inputs] and [TurnStart.Arrived],
	// since how an item arrived is a fact about the turn, and the queue
	// it waited in a fact about the agent.
	Mode QueueMode
	// Hidden is set for an item the caller marked with [Hidden].
	Hidden bool
	// Trigger is what brought the item in, from the context given to
	// [Agent.Queue], zero for [Agent.Steer] and [Agent.FollowUp]: the
	// run start names what started the run, and an input that joins it
	// has a provenance of its own.
	Trigger Trigger
}

Queued reports an item Agent.Steer or Agent.FollowUp accepted into a queue, before any run appends it, so a host writing what it accepted can tell an item it was given from one a run produced. It belongs to no run: the goroutine that owns delivery reports it at its next event, which is why it is not the accept itself and a subscriber's error cannot refuse the item.

RunID names the run that was in flight when the item was accepted, from the moment it was started, and is empty when the agent was idle or the run in flight was past its last drain, after its final turn_end or once it had decided to stop, so the item waits for the next run. A run named here may still stop before it drains the item, which then waits as well.

func (*Queued) EventType added in v0.0.7

func (*Queued) EventType() string

EventType returns "queued".

type Reason

type Reason string

Reason says why a run ended.

const (
	// ReasonDone means the model produced a final answer with no tool
	// calls and no queued follow-ups.
	ReasonDone Reason = "done"
	// ReasonStopped means the loop chose not to call the model again:
	// ShouldStopAfterTurn, a terminating tool result, MaxTurns or a
	// refusal on Resume. RunEnd.Cause says which.
	ReasonStopped Reason = "stopped"
	// ReasonInputRequired means BeforeToolCall deferred one or more
	// calls to the caller; RunEnd.Pending lists them and the run
	// continues once their outputs are appended (see Agent.Resume).
	ReasonInputRequired Reason = "input_required"
	// ReasonAborted means the context was cancelled. RunEnd.Err is
	// context.Cause, the reason [Agent.AbortCause] or the host's own
	// context gave, and context.Canceled when there was none. A tool
	// batch cut off by the abort leaves its calls on RunEnd.Pending.
	ReasonAborted Reason = "aborted"
	// ReasonError means the model, a hook or a subscriber failed;
	// RunEnd.Err says which.
	ReasonError Reason = "error"
)

Run end reasons.

type ReasoningModels added in v0.0.15

type ReasoningModels map[*openresponses.ReasoningItem]string

ReasoningModels says which Config.ModelName produced each reasoning item it holds. A reasoning item carries state only its own provider reads, a signature it issued among it, and another provider refuses it, so the loop leaves out of each request the reasoning items another model produced. The transcript keeps them, and so does a record of it.

The loop fills it as models answer, so a configuration replaced between runs with Agent.SetConfig finds the previous model's items attributed. A transcript the loop has not seen, rebuilt from a store or sent by a caller, has no attribution until a host gives it, with WithReasoningModels or ContextWithReasoningModels. A reasoning item it does not hold is sent, as every one was before the loop attributed them. Items are held by identity: a copy of an item, such as one a transform made, is not the item it copied.

func (ReasoningModels) Attribute added in v0.0.15

func (m ReasoningModels) Attribute(modelName string, items openresponses.Items)

Attribute records modelName as the producer of each reasoning item in items that m does not hold yet. An empty modelName, the model adapter's default, names no model and records nothing. m must not be nil.

func (ReasoningModels) For added in v0.0.15

func (m ReasoningModels) For(modelName string, t Transcript) Transcript

For returns t as modelName is to be sent it: without the reasoning items m attributes to another model. The model's own reasoning items stay, since a provider may require them back, as Anthropic does the thinking block of a turn that called a tool, and so do the ones m does not hold. An empty modelName drops nothing. t itself is returned when nothing is dropped.

type ResponseEnd

type ResponseEnd struct {
	RunID    string
	Turn     int
	Response *openresponses.Response
	// Withheld says OutputGuard withheld a message of the response,
	// which stops the run with [StopGuard]. Response is then the
	// loop's account of it, not the server's: incomplete with
	// content_filter, no error, the response ID and usage the stream
	// gave, the usage of the whole response when its terminal event
	// arrived, since the loop reads the rest of the stream for it, and
	// as output the items the transcript took from it, without the
	// withheld message or anything after it. No turn_end follows; the
	// outputs closing its calls and the run_end do. A server's own
	// content_filter response is not withheld.
	Withheld bool
}

ResponseEnd carries the folded response of a turn, usage included, as soon as the stream ends and before any tool of the turn runs. Its output items have all been delivered with item_end. A response that failed is delivered here too, before the run ends with the error, so a recorder can write it, and so is one a Config.OutputGuard withheld a message of, with Withheld set. The response's function calls the stream completed are the items the transcript holds, the same values item_end delivered, so a consumer that edits one edits the transcript: treat every item an event carries as read-only, as the transcript is. A call the response lists that the stream never completed is a copy carrying the ID the loop decided for it.

func (*ResponseEnd) EventType

func (*ResponseEnd) EventType() string

EventType returns "response_end".

type Retry added in v0.0.4

type Retry struct {
	// MaxAttempts is the number of attempts per turn, the first
	// included. Zero or one means no retry.
	MaxAttempts int
	// Backoff returns how long to wait before the next attempt, given
	// the number of the attempt that failed and its error. nil means
	// [DefaultBackoff].
	Backoff func(attempt int, err error) time.Duration
	// Retryable reports whether err is worth another attempt. nil means
	// [DefaultRetryable].
	Retryable func(error) bool
	// Revise, when set, may change the request the next attempt sends:
	// another model, a lower effort, a smaller max_output_tokens. It is
	// called after the attempt numbered attempt failed with err, with a
	// copy of the request that failed; it may edit that copy in place
	// and return nil, or return a request of its own. The copy shares
	// the slices and maps of the original, so a hook that changes the
	// input or the tools builds a new one rather than appending to
	// what it was given.
	//
	// The revised request is on the [ModelRetry] event, and a session
	// recorder takes its settings, so a fallback to another model is a
	// config delta on the path and the record names the model that
	// answered rather than the one that did not, and writes a record of
	// every attempt that failed. A fallback chain
	// written as a Streamer under the loop still works and still says
	// nothing.
	Revise func(attempt int, req *openresponses.Request, err error) *openresponses.Request
}

Retry says when a failed model call is attempted again. A retry happens inside the turn: the same request is sent again after a delay, a ModelRetry event tells subscribers, and the turn_start and response_end of the turn are delivered once.

Only an attempt that has not committed is retried. An attempt commits when the model begins its answer, which is a message or a function call item opening, or when the server answers with a failed response; after that a failure is final, because the transcript or a recorder may already hold part of the answer. An error event the server sends before the answer opens is a failed attempt like a cut stream, and is retried as one. An item the model completed before that point, the reasoning summary a reasoning model writes before its first token, is held rather than appended: it reaches subscribers as item_start and item_update, so a front renders thinking live, and it is appended with its item_end when the answer begins or when the response arrives. Only an attempt that ends without its response, a transport failure, a cut stream or a failed response, drops what it held. A 503 between the reasoning summary and the first token is therefore retried and leaves nothing in the transcript or the record, where before it was final and left a transcript ending in a reasoning item that no server accepts as input before a user message. Abort cuts a delay short.

type RunEnd

type RunEnd struct {
	RunID  string
	Items  Transcript
	Reason Reason
	// Cause says what stopped the run when Reason is ReasonStopped, and
	// is empty otherwise.
	Cause StopCause
	// Err is set when Reason is ReasonError; to context.Cause of the
	// run's context when Reason is ReasonAborted, which is the cause
	// [Agent.AbortCause] or the host's own context carried and
	// context.Canceled when there was none, wrapping the failure of a
	// subscriber or a hook when one failed for a reason of its own
	// while the run was being aborted; and to the guard's error when
	// Reason is ReasonStopped with Cause StopGuard.
	Err error
	// Pending lists the function calls in the transcript with no
	// function_call_output, in transcript order, each with why: the
	// calls a deferred decision handed to the caller when Reason is
	// ReasonInputRequired, and the calls an abort or a failure cut off
	// before their outputs were appended. It is empty for ReasonDone
	// and ReasonStopped. The transcript is a valid input again once
	// each has an output, which Agent.Resume appends.
	Pending []PendingCall
	// Withheld says the run ended because [Config.OutputGuard]
	// withheld a message, with Reason ReasonStopped and Cause
	// StopGuard: the model's last word was kept from the transcript,
	// so whatever message Items end with was said before it and is no
	// answer, and Answer reports none.
	Withheld bool
}

RunEnd closes a run. Exactly one is emitted per run and nothing follows it. Items are the items the run appended to the transcript. A run refused before it started (ErrNoPrompt, ErrCannotContinue, ErrNoModel) is one RunEnd with ReasonError, an empty RunID and no other event.

func (*RunEnd) Answer added in v0.0.11

func (e *RunEnd) Answer() (string, bool)

Answer returns the run's answer: the text of the assistant message that ends Items, and whether there is one with text. Extension items after it, whose types are namespaced as DefaultFilter tells them, do not count: an adapter may write one once the text is complete. A run whose last other item is anything else did not answer, whatever it said along the way: text before a call is a preamble, and a guard that refuses the next turn leaves the preamble last among the messages but not last among the items. A message that OutputGuard replaced is the replacement, and an empty one is no answer. A run whose message OutputGuard withheld (Withheld) did not answer either, though a message the same response spoke before it may end Items.

func (*RunEnd) EventType

func (*RunEnd) EventType() string

EventType returns "run_end".

type RunStart

type RunStart struct {
	RunID   string
	Source  Source
	Trigger Trigger
}

RunStart opens a run. Source says whether the run answers pending calls or begins from input; Trigger is what the caller attached to the context with ContextWithTrigger, zero when nothing was.

func (*RunStart) EventType

func (*RunStart) EventType() string

EventType returns "run_start".

type Source added in v0.0.6

type Source string

Source says what a run began from, in the terms the session format records: an input, or the answers to calls an earlier run left pending.

const (
	// SourceInput is a run that begins with new input, or with nothing:
	// Prompt, Continue, and the low-level Run and Continue.
	SourceInput Source = "input"
	// SourceResume is a run that begins by answering a call that was
	// pending when it started: Resume, and a Prompt that opens with the
	// outputs of the pending calls.
	SourceResume Source = "resume"
)

Run sources.

type State

type State struct {
	// Transcript is a copy of the slice; items are shared.
	Transcript Transcript
	// Running is true from the run's start until its run_end has been
	// delivered, which is after the run's last drain of the queues: it
	// does not say that a steer will be taken; [Agent.Deliver] does.
	Running bool
	RunID   string
	Turn    int
	// Steering and FollowUps count the queued items; Steered and Queued
	// are the items themselves, copies of the queues, so a host can
	// persist what it accepted and queue it again after a restart.
	Steering  int
	FollowUps int
	Steered   openresponses.Items
	Queued    openresponses.Items
	// Pending lists the calls awaiting outputs, each with why: deferred,
	// cut off by an abort or a failure, or found unanswered in a seeded
	// transcript. Continue refuses until Resume has answered them, and
	// Prompt unless it opens with their outputs.
	Pending []PendingCall
}

State is a snapshot of the agent.

type StopCause added in v0.0.6

type StopCause string

StopCause says what ended a run with ReasonStopped.

const (
	// StopMaxTurns: Config.MaxTurns was reached with tools still being
	// called or items still queued, which the next run takes.
	StopMaxTurns StopCause = "max_turns"
	// StopHook: ShouldStopAfterTurn returned true.
	StopHook StopCause = "hook"
	// StopGuard: ShouldStopAfterTurn, BeforeTurn, BeforeModelCall or
	// OutputGuard returned an error wrapping [ErrGuard]; the error is on
	// RunEnd.Err. Stopped before the model call, the turn has no
	// turn_start; from BeforeModelCall, the request it refused is on a
	// model_blocked. From OutputGuard, the message it ruled on is not
	// appended and has no item_end, though its deltas went out as
	// item_update; the turn's response_end has Withheld set and no
	// turn_end follows, and RunEnd.Withheld is set. A function call the
	// withheld response added to the transcript before the message is
	// answered with a [WithheldCallOutput] output, with its item_end
	// and no tool events, before the run ends, so RunEnd.Pending is
	// empty and the next prompt goes ahead. A call is added once a
	// message or a call of the response has opened with
	// output_item.added; on a stream that sends output_item.done alone
	// the calls before the message are still held when the guard
	// rules, and are dropped with it, in no item_end and not in the
	// transcript.
	StopGuard StopCause = "guard"
	// StopTerminate: every result of the batch set Terminate, so the
	// tools answered on the model's behalf.
	StopTerminate StopCause = "terminate"
	// StopPartialTerminate: some results of the batch set Terminate and
	// others did not. The run ends so the host can act on the call that
	// asked to end it; the other calls ran and their outputs are in the
	// transcript.
	StopPartialTerminate StopCause = "partial_terminate"
	// StopRefused: an answer built with [Refuse] ended the run instead
	// of calling the model.
	StopRefused StopCause = "refused"
)

Stop causes.

type ToolAction added in v0.0.4

type ToolAction int

ToolAction is what BeforeToolCall decides for a call.

const (
	// Allow runs the call. It is the zero value, so a decision that only
	// rewrites Args or sets Terminate allows the call.
	Allow ToolAction = iota
	// Block refuses the call. The model sees ToolDecision.Reason as the
	// error output.
	Block
	// Defer hands the call to the caller instead of running it: the
	// other calls of the batch proceed, no output is appended for this
	// one, and the run ends with ReasonInputRequired listing it. The
	// caller appends the output later and continues.
	Defer
)

type ToolCallInfo

type ToolCallInfo struct {
	RunID string
	Turn  int
	// Call is the function_call item as the model produced it.
	Call *openresponses.FunctionCall
	// Tool is the tool that will run, or nil when no tool has that
	// name; the call then fails with an error output unless the hook
	// blocks it first.
	Tool agenttool.Tool
	// Args are the arguments as raw JSON.
	Args json.RawMessage
	// Batch is every call of the turn in the model's order and Index is
	// this call's position in it. BeforeToolCall runs for each call of
	// the batch, in order, before any call executes, so a hook that
	// defers one call can defer the rest of the batch and hold all of
	// it for the answer.
	Batch []*openresponses.FunctionCall
	Index int
	// Parent is the ID of the call whose tool made this one with
	// [Invoke], as ToolStart.Parent is, and empty for a call the model
	// made. A nested call the hook defers is answered inline, by the
	// elicitor on the invoking tool's context or with a refusal, and
	// never becomes pending: a hook that remembers the calls it defers
	// for a resume keeps nothing for one with a Parent.
	Parent string
}

ToolCallInfo describes a call before it runs.

type ToolDecision

type ToolDecision struct {
	// Action allows, blocks or defers the call.
	Action ToolAction
	// Reason is the message the model sees when Action is Block, and
	// the reason a session recorder writes on the decision whatever the
	// action: which rule raised the prompt for a Defer, which one
	// refused the call for a Block. The model never sees the reason of
	// a deferred call; it is for the record and for the front that asks
	// the user.
	Reason string
	// Terminate hints the loop to stop after the batch, as a tool result
	// would. It composes with Allow and Block.
	Terminate bool
	// Args, when non-nil, replaces the arguments the tool receives.
	Args json.RawMessage
	// By names who decided, for the record: the session format knows
	// "human" for a person the hook waited on, "policy" for a rule it
	// evaluated on its own and "agent" for another model. Empty is read
	// as policy for a decision about a call nothing was holding, and as
	// nobody for the approval of a held call, where [Answer.By] is what
	// says who answered. The loop does not use it.
	By string
	// Note is text the model sees with the result: it is appended after
	// the batch's outputs as a developer message, so the model reads the
	// result and the note together, in that order, in the same turn. It
	// applies to an allowed call; a blocked call carries its Reason.
	Note string
}

ToolDecision is a hook's verdict on a call.

func Ask added in v0.0.16

func Ask(ctx context.Context, call AskedCall) (*ToolDecision, bool)

Ask puts call to the elicitor on ctx, as the loop puts a nested call the hook deferred: with the question "Allow <name> with arguments <args>? <reason>", the arguments clipped at 500 bytes, and the call on the elicitor's context for AskedCallFrom. It returns the decision the answer makes, by "human": Allow with the deferral's reason, or "allowed when asked" when it has none, on an accept, and Block with "declined when asked" and the reason on a decline. It returns nil and false when ctx carries no elicitor, the ask failed or the answer was a cancel; the caller then still holds the deferral it started with. call.Decision must be non-nil.

type ToolDispatch added in v0.0.9

type ToolDispatch struct {
	RunID          string
	Turn           int
	CallID         string
	Name           string
	Parent         string
	IdempotencyKey string
}

ToolDispatch reports that a call has been handed to its tool: it has taken a slot in the batch's bound and its turn in its chain, and the tool is about to run. tool_start says the call was decided; this says it started, which for a batch wider than the bound or serial by a tool's request is later, and for a call cut off in between never. It is where a session recorder writes the dispatch entry, so a call cut off before it reads as never started and one cut off after as possibly run. It is raised on the call's own goroutine, serialised with the run's events, and a subscriber that fails on it stops the call: it ends with that error and the tool does not run, so a dispatch that could not be made durable is never followed by a side effect the record cannot see. The call is then pending as PendingUndispatched, or still as PendingAborted when an earlier run dispatched it. Subscribers are called in registration order, so a subscriber that vetoes a dispatch is registered before the recorder: one registered after it refuses a call whose dispatch is already durable. Parent is set for a nested call.

IdempotencyKey is the key the tool receives on agenttool.Call: the loop mints one for every call it dispatches and keeps it when the call runs again, from the pending call or the Answer that approved it. A recorder writes it with the dispatch, so a host resuming after a restart can hand a keyed tool the key of the dispatch it repeats.

func (*ToolDispatch) EventType added in v0.0.9

func (*ToolDispatch) EventType() string

EventType returns "tool_dispatch".

type ToolEnd

type ToolEnd struct {
	RunID    string
	Turn     int
	CallID   string
	Name     string
	Result   agenttool.Result
	Err      error
	Blocked  bool
	Deferred bool
	// Reason is the decision's reason for a blocked or a deferred call:
	// the rule that refused it, or the one that raised the question a
	// front now asks the user. Empty otherwise.
	Reason string
	// Parent is the ID of the call whose tool made this one with
	// [Invoke], and empty for a call the model made.
	Parent string
}

ToolEnd carries a finished call, in completion order. Err is set when the tool failed, the arguments were invalid or no tool had the name; Blocked is set when BeforeToolCall refused the call. In both cases Result holds the error output the model sees. Deferred is set when BeforeToolCall handed the call to the caller: nothing ran, Result is empty and no output is appended. A call cancelled by an abort ends with the context error as Err; its output is not appended either, and the call is listed on RunEnd.Pending.

func (*ToolEnd) EventType

func (*ToolEnd) EventType() string

EventType returns "tool_end".

type ToolOverride

type ToolOverride struct {
	Result agenttool.Result
	Err    error
}

ToolOverride replaces the result of a call.

type ToolResultInfo

type ToolResultInfo struct {
	RunID  string
	Turn   int
	Call   *openresponses.FunctionCall
	Tool   agenttool.Tool
	Args   json.RawMessage
	Result agenttool.Result
	Err    error
}

ToolResultInfo describes a completed call.

type ToolStart

type ToolStart struct {
	RunID    string
	Turn     int
	CallID   string
	Name     string
	Args     json.RawMessage
	Decision *ToolDecision
	// Parent is the ID of the call whose tool made this one with
	// [Invoke], and empty for a call the model made. A nested call has
	// no function_call item in the transcript: it is the work of the
	// call that made it.
	Parent string
}

ToolStart announces a tool call after preflight, in the model's order. Turn is the turn whose response made the call, and 0 for a call approved through Agent.Resume, whose batch runs before the run's first model call; its tool_dispatch, tool_update and tool_end carry 0 too. Args are the arguments the tool receives, which a decision may have rewritten; the function_call item in the transcript keeps the model's. Decision is what BeforeToolCall returned for the call, nil when there was no hook or it returned nil; for a call approved through Agent.Resume it carries the caller's arguments, when they gave any, and nothing else.

func (*ToolStart) EventType

func (*ToolStart) EventType() string

EventType returns "tool_start".

type ToolUpdate

type ToolUpdate struct {
	RunID   string
	Turn    int
	CallID  string
	Name    string
	Partial agenttool.Result
}

ToolUpdate carries progress from a running tool.

func (*ToolUpdate) EventType

func (*ToolUpdate) EventType() string

EventType returns "tool_update".

type Transcript

type Transcript = openresponses.Items

Transcript is the conversation as the model sees it, plus app-only items that the Config.Filter removes before each call.

func DefaultFilter

func DefaultFilter(t Transcript) Transcript

DefaultFilter drops every item whose type carries a slug prefix such as "agentturn:note", which marks an app-only extension item, and nil items. It is the Filter when Config.Filter is nil.

func TranscriptFromContext added in v0.0.4

func TranscriptFromContext(ctx context.Context) (Transcript, bool)

TranscriptFromContext returns the transcript the loop attached to the context of a hook or a tool call: the working transcript as it stood when the batch started, the calls of the batch included. It is a snapshot; do not mutate it.

type Trigger added in v0.0.6

type Trigger struct {
	Kind   string
	Ref    string
	Source string
	Extra  map[string]any
}

Trigger names what caused a run, in the caller's own terms: a cron job, a channel message, a user's turn. The loop learns nothing from it; it carries the value from ContextWithTrigger to RunStart so a recorder can write it. Kind is the category and Ref the instance; a recorder joins them as "kind:ref" when both are set, and writes the three apart as well. Source names the layer that took the input, a gateway or a scheduler, and is not part of the joined string.

Extra holds the caller's richer facts about the firing, such as when it was due or which attempt it is, each a JSON value under a name of the caller's. A session recorder writes them as members of the trigger object beside kind, ref and source, which is where the format puts them, wherever it writes the trigger: on the run start, on a queued input's entry and as the source of the item that drains it, so a firing queued behind a busy run keeps its slot. The loop reads nothing from Extra, but it refuses, with ErrTriggerExtra, a trigger whose Extra names kind, ref or source, the members the format defines beside it, or holds a value that does not encode as JSON, where the trigger enters: a run started with it and an item queued with it are refused at the call, whether or not a recorder is attached, rather than failing a later run.

func TriggerFromContext added in v0.0.6

func TriggerFromContext(ctx context.Context) Trigger

TriggerFromContext returns the trigger attached to ctx, or the zero Trigger.

func (Trigger) IsZero added in v0.0.6

func (t Trigger) IsZero() bool

IsZero reports whether the trigger names nothing.

func (Trigger) String added in v0.0.6

func (t Trigger) String() string

String returns "kind:ref", or whichever of the two is set.

func (Trigger) Validate added in v0.0.11

func (t Trigger) Validate() error

Validate returns ErrTriggerExtra, naming the member, when Extra cannot be written beside kind, ref and source, and nil otherwise.

type TurnEnd

type TurnEnd struct {
	RunID       string
	Turn        int
	Response    *openresponses.Response
	ToolResults []agenttool.Result
}

TurnEnd closes a turn with the folded response, usage included, and the tool results in the model's order. The response is the one ResponseEnd carried, its items shared with the transcript.

func (*TurnEnd) EventType

func (*TurnEnd) EventType() string

EventType returns "turn_end".

type TurnInfo

type TurnInfo struct {
	RunID    string
	Turn     int
	Response *openresponses.Response
	// ToolResults are the results of this turn's calls in the model's
	// order, empty when the model called no tools.
	ToolResults []agenttool.Result
	// Final is set when the model called no tools, so the response is
	// the run's answer unless a queued follow-up keeps it going.
	Final bool
	// Transcript is the working transcript after the turn. It is the
	// loop's live slice; do not mutate it or keep it past the hook.
	Transcript Transcript
}

TurnInfo describes a finished turn.

type TurnInput added in v0.0.16

type TurnInput struct {
	Item openresponses.Item
	Mode InputMode
	// Trigger is the run's trigger for a prompt's item and the trigger
	// its Queued report carried for a steered, followed-up or delivered
	// item; zero otherwise.
	Trigger Trigger
}

TurnInput is one item among a turn's inputs and how it arrived.

type TurnStart

type TurnStart struct {
	RunID   string
	Turn    int
	Request openresponses.Request
	Inputs  openresponses.Items
	Arrived []TurnInput
}

TurnStart opens a turn and carries the exact request sent to the model. Inputs are the items appended to the transcript since the previous turn's response, or since the run started for the first turn: the tool outputs, the steered and queued messages, the items BeforeTurn added. They are what this turn's response answers, so a front that routes replies to the message that caused them reads them here rather than counting item events between turns. Arrived is the same items with how each came, as TurnStartInfo.Inputs gives them to BeforeTurn, plus the items that hook appended, as InputHook, and, on a run's first turn, the InputContinued items the transcript already ended with when the run started, which Inputs does not include: Inputs keeps its definition, so a consumer that counted on it is unchanged, and a consumer that needs what a Continue answers reads Arrived.

func (*TurnStart) EventType

func (*TurnStart) EventType() string

EventType returns "turn_start".

type TurnStartInfo added in v0.0.6

type TurnStartInfo struct {
	RunID string
	Turn  int
	// Transcript is the working transcript as the turn starts. It is the
	// loop's live slice; do not mutate it or keep it past the hook.
	Transcript Transcript
	// Inputs are the items that joined the transcript for this turn, in
	// transcript order, each with how it arrived: the items appended
	// since the previous turn's response, or since the run started for
	// the first turn, which then also holds, first, the [InputContinued]
	// items the transcript already ended with when the run started. The
	// items this hook returns are not among them, since the hook has
	// not run yet; [TurnStart.Arrived] has them. A hook that must act
	// when a user's message arrives, or once on the turn that answers a
	// handoff, reads the mode here rather than inferring it from the
	// transcript's tail, which a steer followed by a delivered output,
	// or an output delivered as a message, gets wrong.
	Inputs []TurnInput
}

TurnStartInfo describes a turn about to start, for Config.BeforeTurn.

Directories

Path Synopsis
Package compact is the reference compaction Transform for agentturn: when a transcript grows past a token budget, the older part is folded into a shorter form and the recent part is kept verbatim.
Package compact is the reference compaction Transform for agentturn: when a transcript grows past a token budget, the older part is folded into a shorter form and the recent part is kept verbatim.
front
responses
Package responses serves an agent as an Open Responses server: the loop as an openresponses.Adapter.
Package responses serves an agent as an Open Responses server: the loop as an openresponses.Adapter.
a2a module
session module
tools
agent
Package agent wraps an agent configuration as a tool, so one loop can delegate to another in process.
Package agent wraps an agent configuration as a tool, so one loop can delegate to another in process.
a2a module

Jump to

Keyboard shortcuts

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