agent

package
v0.1.3 Latest Latest
Warning

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

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

Documentation

Overview

Package agent provides the runtime core for autonomous LLM agents: a loop that calls a language model, executes the tools it requests, feeds results back, and repeats until the model finishes or a stop condition fires.

It builds on the sibling github.com/rsbin1178/pips/ai package — any ai.LanguageModel (with whatever middleware stack) drives an agent, and conversations are ordinary ai.Message histories.

The core abstraction is Agent, an immutable bundle of model, system prompt, tools, and loop policy. Runs mutate a Session:

calc := agent.NewTool("add", "Add two integers.",
    func(ctx context.Context, args struct {
        A int `json:"a"`
        B int `json:"b"`
    }) (string, error) {
        return strconv.Itoa(args.A + args.B), nil
    })

a, err := agent.New(model, agent.WithTools(calc))
sess := agent.NewSession()
result, err := a.Run(ctx, sess, ai.UserText("What is 2+3?"))

Agent.Stream exposes the same loop as an event sequence (model deltas, tool lifecycle, turn boundaries); breaking out of the range loop cancels the run.

Tool failures — errors, panics, timeouts, undecodable arguments, calls to unknown tools — never abort a run: each becomes an error tool result the model can react to. Every tool call the model issues is answered before the next model call, even on cancellation, so sessions stay resumable.

A WithBeforeTool gate intercepts calls before execution: deny with a reason the model sees, or pause the run for out-of-band approval and resume it later via Session.ResolveToolCalls or Session.ResolvePending. WithInputGuardrail and WithOutputGuardrail validate conversation boundaries, while WithAfterTool inspects and overrides executed results. WithTransformContext reshapes what each model call sees (the context-compaction injection point), and WithPrepareTurn swaps the model, commits a history rewrite, or replaces the run-scoped tool snapshot between turns.

A running loop can be redirected without restarting: Session.Steer injects messages before the next model call, and Session.FollowUp queues work for after the model would otherwise finish. AsTool turns any agent into another agent's tool for sub-agent delegation, and a tool returning ErrTerminate ends the run from inside a tool batch.

WithName labels an agent. Every event and result carries RunMetadata; nested agents inherit a parent run ID through context, and tools can read the current identity with RunMetadataFromContext.

The package has no third-party runtime dependencies.

Example (Run)

Run drives the agent loop to completion and returns the final result.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strconv"

	"github.com/rsbin1178/pips/agent"
	"github.com/rsbin1178/pips/ai"
	"github.com/rsbin1178/pips/ai/openai"
)

func main() {
	model := openai.New("gpt-6-astra", openai.WithAPIKey(os.Getenv("OPENAI_API_KEY")))

	add := agent.NewTool("add", "Add two integers.",
		func(_ context.Context, args struct {
			A int `json:"a"`
			B int `json:"b"`
		},
		) (string, error) {
			return strconv.Itoa(args.A + args.B), nil
		})

	a, err := agent.New(model, agent.WithTools(add))
	if err != nil {
		log.Fatal(err)
	}

	sess := agent.NewSession()

	result, err := a.Run(context.Background(), sess, ai.UserText("What is 21+21?"))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Stop, result.Text())
}
Example (Stream)

Stream exposes the loop as events; breaking out cancels the run.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/rsbin1178/pips/agent"
	"github.com/rsbin1178/pips/ai"
	"github.com/rsbin1178/pips/ai/openai"
)

func main() {
	model := openai.New("gpt-6-astra", openai.WithAPIKey(os.Getenv("OPENAI_API_KEY")))

	a, err := agent.New(model)
	if err != nil {
		log.Fatal(err)
	}

	for ev, err := range a.Stream(context.Background(), agent.NewSession(), ai.UserText("Hello!")) {
		if err != nil {
			log.Fatal(err)
		}

		if event, ok := ev.Payload().(agent.ModelStreamEvent); ok &&
			event.Event.Type == ai.StreamTextDelta {
			fmt.Print(event.Event.Text)
		}
	}
}

Index

Examples

Constants

View Source
const (
	// DefaultMaxTurns bounds a run to 25 model calls unless overridden with
	// [WithMaxTurns]. An unbounded loop is the classic agent failure mode, so
	// the limit is opt-out rather than opt-in.
	DefaultMaxTurns = 25
	// DefaultParallelTools is the per-turn concurrency limit for tools marked
	// with [Parallel].
	DefaultParallelTools = 4
)

Default limits.

Variables

View Source
var (
	// ErrRunActive means the session already has a run in progress. A
	// [Session] serializes runs; wait for the active one to finish.
	ErrRunActive = errors.New("agent: session already has an active run")
	// ErrPendingToolCalls means the session tail contains tool calls that have
	// no results yet (a previous run stopped with [StopPaused], or a stream
	// was abandoned mid-turn). Resolve them with [Session.ResolveToolCalls] or
	// [Session.ResolvePending] before starting another run.
	ErrPendingToolCalls = errors.New("agent: session has unresolved tool calls")
	// ErrToolCallNotPending means a supplied tool resolution references a call
	// that is not currently awaiting a result.
	ErrToolCallNotPending = errors.New("agent: tool call is not pending")
	// ErrInvalidToolResolution means a supplied resolution has an empty or
	// duplicate tool-call ID.
	ErrInvalidToolResolution = errors.New("agent: invalid tool resolution")
	// ErrInvalidEvent classifies invalid process-local event metadata or
	// payloads. Construct events with [NewEvent] to validate and snapshot them.
	ErrInvalidEvent = errors.New("agent: invalid event")
	// ErrEventWireFormat reports an attempt to marshal or unmarshal [Event].
	// Agent events are process-local; use an application-owned, versioned
	// projection for durable or remote transport.
	ErrEventWireFormat = errors.New("agent: event has no wire format")
	// ErrGuardrail classifies input and output validation failures. Extract a
	// [GuardrailError] with [errors.As] for the phase, name, and cause.
	ErrGuardrail = errors.New("agent: guardrail rejected the run")
	// ErrSubagentPaused means an [AsTool] child requested durable approval,
	// which the tool result contract cannot preserve across invocations.
	ErrSubagentPaused = errors.New("agent: subagent paused with pending tool calls")
	// ErrTerminate is a control sentinel (in the spirit of [io.EOF]) a tool
	// returns alongside its parts to ask the run to stop after the current
	// tool batch:
	//
	//	return agent.TextResult("final answer"), agent.ErrTerminate
	//
	// The parts are recorded as a successful result. The run stops with
	// [StopTerminated] only when every result in the batch carries the hint
	// (failed or denied calls never do); queued follow-ups still run first.
	ErrTerminate = errors.New("agent: tool requested run termination")
)

Sentinel errors returned by Agent.Run, Agent.Stream, and Session methods. Match them with errors.Is.

Functions

func ReportProgress

func ReportProgress(ctx context.Context, parts ...ai.Part)

ReportProgress publishes a partial-result update from inside a running tool's Exec, surfacing as an EventToolUpdated event to the run's event consumers. Updates are advisory: delivery is best-effort (slow consumers drop updates rather than block the tool), and calls outside a run are no-ops.

func TextResult

func TextResult(s string) []ai.Part

TextResult wraps s as single-part tool result content. It is a convenience for hand-written Tool implementations; NewTool applies it automatically.

Types

type Agent

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

Agent binds a language model to a system prompt, a tool set, and loop policy. It is immutable after construction and safe for concurrent use; run-scoped state lives in the Session passed to each run.

Resilience is layered at the model, not the agent: wrap the model with ai.Chain and the ai middleware packages before passing it in.

func New

func New(model ai.LanguageModel, opts ...Option) (*Agent, error)

New returns an agent bound to model. It fails when model is nil or the configured tools have duplicate or empty names.

func (*Agent) Name

func (a *Agent) Name() string

Name returns the agent's configured name, or "" when unnamed.

func (*Agent) Run

func (a *Agent) Run(ctx context.Context, sess *Session, msgs ...ai.Message) (*RunResult, error)

Run appends msgs to the session and drives the agent loop to completion: call the model, execute requested tools, feed results back, repeat. It blocks until the run terminates cleanly (see StopReason) or fails; on failure the returned result carries what completed before the error.

Mid-run, Session.Steer injects messages before the next model call and Session.FollowUp queues work for after the model would otherwise finish.

Run uses ai.LanguageModel.Generate, so retry middleware on the model is fully effective. Use Agent.Stream for incremental output.

func (*Agent) Stream

func (a *Agent) Stream(ctx context.Context, sess *Session, msgs ...ai.Message) iter.Seq2[Event, error]

Stream is Agent.Run as an event sequence: it yields run, turn, and tool lifecycle events plus model deltas as they happen. Breaking out of the loop cancels the run; the session keeps everything appended up to that point, and any tool calls left unanswered surface through Session.Pending.

A clean termination ends with an EventRunCompleted carrying the RunCompleted payload and StopReason (after StopPaused, resolve Session.Pending and run again); failures yield a non-nil error as the final element.

type CandidateAnswerDecision

type CandidateAnswerDecision struct {
	Retry *ModelRequestUpdate
	Err   error
}

CandidateAnswerDecision accepts the candidate when both fields are zero. Err rejects it and aborts the run. Retry rejects it without committing the assistant message and constrains exactly the next model request.

type CandidateAnswerInfo

type CandidateAnswerInfo struct {
	RunInfo
	Session []ai.Message
	Message ai.Message
}

CandidateAnswerInfo is the read-only view passed to a WithCandidateAnswer hook before a no-Tool assistant answer is committed.

type CandidateDiscarded

type CandidateDiscarded struct {
	Turn int
}

CandidateDiscarded reports a provisional no-tool answer rejected before session commit. Turn is one-based; rejected content is never retained.

type ConcurrencySafe

type ConcurrencySafe interface {
	// Concurrent reports whether the tool may execute in parallel.
	Concurrent() bool
}

ConcurrencySafe is an optional interface a Tool may implement to declare that it can run in parallel with other concurrency-safe tools of the same turn. Tools that do not implement it (or report false) run serially, each acting as a barrier within the turn's batch. Wrap an existing tool with Parallel instead of implementing this by hand.

type Event

type Event struct {
	// RunID correlates every event in one invocation. ParentRunID links a
	// nested invocation to the run whose tool or callback started it. Agent is
	// the configured [Agent.Name], and Time is the UTC emission time.
	RunID       string
	ParentRunID string
	Agent       string
	Time        time.Time
	// contains filtered or unexported fields
}

Event is one immutable, process-local increment of an agent run. The envelope carries correlation metadata shared by every variant; Payload carries exactly one semantic variant. Events are safe to retain. Consumers must treat the returned payload as read-only.

Event intentionally has no JSON wire format. Durable or remote consumers must define a versioned projection appropriate to their disclosure and compatibility requirements.

func NewEvent

func NewEvent(meta RunMetadata, occurredAt time.Time, payload EventPayload) (Event, error)

NewEvent constructs a validated event and snapshots all mutable payload data. occurredAt is normalized to UTC. It returns an error matching ErrInvalidEvent when metadata or payload invariants are invalid.

func (Event) MarshalJSON

func (Event) MarshalJSON() ([]byte, error)

MarshalJSON prevents the process-local event union from being mistaken for a stable wire protocol.

func (Event) Payload

func (e Event) Payload() EventPayload

Payload returns the event's sealed semantic payload. The returned value is owned by this event snapshot and must be treated as read-only.

func (Event) Type

func (e Event) Type() EventType

Type returns the discriminator for the event's concrete payload. It returns the zero EventType for an invalid zero Event.

func (*Event) UnmarshalJSON

func (*Event) UnmarshalJSON([]byte) error

UnmarshalJSON prevents decoding an unspecified wire representation into an Event. Use an application-owned, versioned projection instead.

func (Event) Validate

func (e Event) Validate() error

Validate verifies the event envelope and payload invariants. All failures match ErrInvalidEvent. It does not replace independent validation of a complete model message or model stream state machine.

type EventPayload

type EventPayload interface {
	// contains filtered or unexported methods
}

EventPayload is the sealed union of semantic Event variants. Only the concrete value types declared in this package are valid payloads.

type EventType

type EventType string

EventType identifies the semantic payload carried by an Event.

const (
	// EventRunStarted opens a run.
	EventRunStarted EventType = "run_started"
	// EventTurnStarted opens one model turn.
	EventTurnStarted EventType = "turn_started"
	// EventModelStream carries one normalized model stream event. It is only
	// produced by [Agent.Stream], never [Agent.Run].
	EventModelStream EventType = "model_stream"
	// EventMessageCommitted reports a message after it has been appended to
	// the session.
	EventMessageCommitted EventType = "message_committed"
	// EventCandidateDiscarded reports a provisional answer rejected before
	// session commit. Candidate content is deliberately absent.
	EventCandidateDiscarded EventType = "candidate_discarded"
	// EventToolStarted opens one tool-call lifecycle.
	EventToolStarted EventType = "tool_started"
	// EventToolUpdated carries a best-effort progress update from a running
	// tool.
	EventToolUpdated EventType = "tool_updated"
	// EventToolCompleted closes one tool-call lifecycle, including denials and
	// synthesized failures.
	EventToolCompleted EventType = "tool_completed"
	// EventTurnCompleted closes one model turn.
	EventTurnCompleted EventType = "turn_completed"
	// EventRunCompleted closes a cleanly terminated run.
	EventRunCompleted EventType = "run_completed"
)

Event types, in the order a run can produce them. Failed runs return an iterator error instead of producing EventRunCompleted.

type GuardrailError

type GuardrailError struct {
	Phase GuardrailPhase
	Name  string
	Cause error
}

GuardrailError reports a named input or output guardrail rejection. Its error chain matches both ErrGuardrail and Cause when Cause is non-nil.

func (*GuardrailError) Error

func (e *GuardrailError) Error() string

Error implements error.

func (*GuardrailError) Unwrap

func (e *GuardrailError) Unwrap() []error

Unwrap exposes the guardrail class and underlying cause.

type GuardrailPhase

type GuardrailPhase string

GuardrailPhase identifies where a conversation guardrail rejected a run.

const (
	// GuardrailInput validates newly supplied messages before transcript
	// mutation or model I/O.
	GuardrailInput GuardrailPhase = "input"
	// GuardrailOutput validates a candidate assistant answer (a response with
	// no tool calls) before it is committed to the transcript.
	GuardrailOutput GuardrailPhase = "output"
)

Guardrail phases.

type InputGuardrailInfo

type InputGuardrailInfo struct {
	RunMetadata
	Session []ai.Message
	Input   []ai.Message
}

InputGuardrailInfo is the read-only input validation snapshot. Session is the existing transcript and Input contains the messages supplied to this run; treat both slices and their parts as read-only.

type MessageCommitted

type MessageCommitted struct {
	Turn    int
	Message ai.Message
}

MessageCommitted reports a message after it has been appended to the session. Turn is one-based.

type ModelRequestUpdate

type ModelRequestUpdate struct {
	Tools        []Tool
	ToolChoice   ai.ToolChoice
	SystemSuffix string
}

ModelRequestUpdate constrains exactly one subsequent model request. It is run-local: applying it never mutates the Agent or the persistent Session. A non-nil Tools slice replaces the declaration and execution snapshot for that request only.

type ModelStreamEvent

type ModelStreamEvent struct {
	Turn  int
	Event ai.StreamEvent
}

ModelStreamEvent carries one normalized model stream increment. Turn is one-based. The embedded stream event remains provisional until a MessageCommitted payload is emitted.

type Option

type Option func(*config)

Option configures an Agent.

func WithAfterTool

func WithAfterTool(fn func(ctx context.Context, info ToolResultInfo) *ToolResultOverride) Option

WithAfterTool installs a hook that runs after each executed tool call, before its result is recorded and emitted. The returned ToolResultOverride replaces result fields (nil keeps everything). The hook only sees calls that actually executed — denials, unknown tools, undecodable arguments, and cancellations skip it. It runs serially on the run's goroutine; a panic converts the result into an error result.

func WithBeforeTool

func WithBeforeTool(fn func(ctx context.Context, info ToolCallInfo) ToolDecision) Option

WithBeforeTool installs a gate consulted before each tool call executes. Gates run serially in call order on the run's goroutine. See ToolDecisionAction for the available verdicts.

func WithCandidateAnswer

func WithCandidateAnswer(
	fn func(context.Context, CandidateAnswerInfo) CandidateAnswerDecision,
) Option

WithCandidateAnswer installs a hook for no-Tool assistant answers before they are committed. The hook may accept, abort, or discard and retry with a one-request constraint. Streaming events are provisional; a retry emits an EventCandidateDiscarded event so consumers can clear the rejected draft.

func WithFollowUpMode

func WithFollowUpMode(m QueueMode) Option

WithFollowUpMode sets how queued follow-up messages are drained (default QueueDrainOne).

func WithInputGuardrail

func WithInputGuardrail(
	name string,
	fn func(context.Context, InputGuardrailInfo) error,
) Option

WithInputGuardrail appends a named validator that runs once per invocation, before new messages are committed or a model is called. Validators run in option order; the first non-nil error rejects the run with a GuardrailError. Use WithBeforeTool for tool side-effect policy.

func WithMaxTokens

func WithMaxTokens(n int) Option

WithMaxTokens stops the run with StopBudget once its cumulative input plus output tokens reach n. Zero (the default) means unlimited.

func WithMaxTurns

func WithMaxTurns(n int) Option

WithMaxTurns caps the number of model calls per run (default DefaultMaxTurns). Zero or negative means unlimited — combine with another stop condition to avoid unbounded loops.

func WithName

func WithName(name string) Option

WithName assigns a stable human-readable name used in run metadata and events. It does not affect model prompts or tool names.

func WithOnEvent

func WithOnEvent(fn func(ctx context.Context, ev Event)) Option

WithOnEvent installs a callback receiving every run event, for both Agent.Run and Agent.Stream (Run produces no EventModelStream events). The callback runs on the run's goroutine; keep it fast and non-blocking.

func WithOutputGuardrail

func WithOutputGuardrail(
	name string,
	fn func(context.Context, OutputGuardrailInfo) error,
) Option

WithOutputGuardrail appends a named validator for every candidate assistant answer (a response with no tool calls). It runs before that message is committed. Queued steering or follow-up can extend the run after a validated answer. During Agent.Stream, model deltas are provisional and may already have been observed before validation rejects them.

func WithParallelTools

func WithParallelTools(limit int) Option

WithParallelTools caps how many Parallel-marked tools of one turn execute concurrently (default DefaultParallelTools). One serializes everything.

func WithPrepareTurn

func WithPrepareTurn(fn func(ctx context.Context, info RunInfo) TurnUpdate) Option

WithPrepareTurn installs a hook that runs after each completed turn, before the loop decides whether to continue. The returned TurnUpdate can swap the model for subsequent turns or rewrite the session history (the commit point for context compaction).

func WithRequest

func WithRequest(fn func(*ai.Request)) Option

WithRequest installs an escape hatch applied to each ai.Request just before it is sent, after the agent has set Messages and Tools. The configured system prompt is the leading ai.SystemMessage in Messages. Use it for generation parameters, reasoning configuration, or provider options:

agent.WithRequest(func(req *ai.Request) {
    req.Temperature = ai.Ptr(0.2)
})

func WithSteeringMode

func WithSteeringMode(m QueueMode) Option

WithSteeringMode sets how queued steering messages are drained (default QueueDrainOne).

func WithStopWhen

func WithStopWhen(cond func(RunInfo) bool) Option

WithStopWhen stops the run with StopWhen once cond reports true. It is checked after each completed turn.

func WithSystem

func WithSystem(s string) Option

WithSystem sets the system prompt sent on every model call.

func WithToolTimeout

func WithToolTimeout(d time.Duration) Option

WithToolTimeout bounds each tool execution; on expiry the call becomes an error tool result and the run continues. Zero (the default) means no per-tool deadline.

func WithTools

func WithTools(tools ...Tool) Option

WithTools adds tools to the agent. Tool names must be unique and non-empty; New fails otherwise.

func WithTransformContext

func WithTransformContext(fn func(ctx context.Context, msgs []ai.Message) ([]ai.Message, error)) Option

WithTransformContext installs a transform applied to the session snapshot before each model call — a non-destructive injection point for context pruning or augmentation. The session itself is untouched; use WithPrepareTurn or Session.Replace to rewrite history permanently. A returned error terminates the run with that error.

type OutputGuardrailInfo

type OutputGuardrailInfo struct {
	RunInfo
	Message ai.Message
}

OutputGuardrailInfo is the read-only answer validation snapshot. Message is the candidate assistant answer; treat its parts as read-only.

type QueueMode

type QueueMode int

QueueMode controls how many queued messages a drain point injects (see Session.Steer and Session.FollowUp).

const (
	// QueueDrainOne injects only the oldest queued message per drain point,
	// leaving the rest queued for later points.
	QueueDrainOne QueueMode = iota
	// QueueDrainAll injects every queued message at each drain point.
	QueueDrainAll
)

Queue modes.

type RunCompleted

type RunCompleted struct {
	Turns int
	Stop  StopReason
	Usage ai.Usage
}

RunCompleted closes a cleanly terminated run. Turns is the number of model calls made, Stop is the clean termination reason, and Usage is cumulative.

type RunInfo

type RunInfo struct {
	RunMetadata
	// Turns is the number of model calls made so far in this run.
	Turns int
	// Usage is the token usage accumulated by this run.
	Usage ai.Usage
	// Response is the most recent model response.
	Response *ai.Response
}

RunInfo is a read-only snapshot of run progress, passed to the WithStopWhen condition after each turn.

type RunMetadata

type RunMetadata struct {
	RunID       string
	ParentRunID string
	Agent       string
}

RunMetadata identifies one agent invocation and its place in a nested run tree. ParentRunID is empty for a root run.

func RunMetadataFromContext

func RunMetadataFromContext(ctx context.Context) (RunMetadata, bool)

RunMetadataFromContext returns the current run metadata. Tools and event observers can use it to correlate work without depending on a tracing SDK.

type RunResult

type RunResult struct {
	RunMetadata
	// Stop is why the run terminated. It is empty when the run failed with an
	// error.
	Stop StopReason
	// Turns is the number of model calls made.
	Turns int
	// Usage is the token usage accumulated across the run's model calls. The
	// session separately accumulates usage across runs (see [Session.Usage]).
	Usage ai.Usage
	// Response is the final model response, nil when the run failed before
	// the first model call completed.
	Response *ai.Response
	// Pending holds the tool calls awaiting resolution when Stop is
	// [StopPaused]; it is nil otherwise.
	Pending []ai.ToolCallPart
}

RunResult is the outcome of a completed run.

func (*RunResult) Text

func (r *RunResult) Text() string

Text returns the text of the final model response, or "" when there is none.

type RunStarted

type RunStarted struct{}

RunStarted opens a run and carries no variant-specific data.

type Session

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

Session holds the conversation state an agent runs against: the message history and the token usage accumulated across runs. It is safe for concurrent use, but only one run may be active at a time (ErrRunActive).

Sessions serialize to a stable JSON envelope (reusing the ai.Message format), so they can be persisted and resumed across processes. The zero value is not usable; construct with NewSession.

func NewSession

func NewSession(msgs ...ai.Message) *Session

NewSession returns a session seeded with the given messages (for example a restored history), oldest first.

func (*Session) Append

func (s *Session) Append(msgs ...ai.Message)

Append adds messages to the history. Runs snapshot the history at each turn, so appending during an active run affects the next model call, not the in-flight one.

func (*Session) ClearFollowUps

func (s *Session) ClearFollowUps()

ClearFollowUps discards all queued follow-up messages.

func (*Session) ClearSteering

func (s *Session) ClearSteering()

ClearSteering discards all queued steering messages.

func (*Session) FollowUp

func (s *Session) FollowUp(msgs ...ai.Message)

FollowUp queues messages that run only after the agent would otherwise stop cleanly: when a turn produces no tool calls and no steering is queued, follow-ups are injected and the loop continues instead of finishing. Safe to call from any goroutine.

func (*Session) HasQueued

func (s *Session) HasQueued() bool

HasQueued reports whether any steering or follow-up messages are queued.

func (*Session) MarshalJSON

func (s *Session) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. The running flag is transient and not serialized.

func (*Session) Messages

func (s *Session) Messages() ai.Messages

Messages returns a copy of the conversation so far, oldest first. The message structs are copies; treat their Parts as read-only.

func (*Session) Pending

func (s *Session) Pending() []ai.ToolCallPart

Pending returns the tool calls at the session tail that have no matching results, in call order. A non-empty result means the conversation cannot continue until they are resolved (see Session.ResolvePending).

func (*Session) Replace

func (s *Session) Replace(msgs ...ai.Message)

Replace swaps the entire message history for msgs. It is the commit point for context compaction: rewrite the history out-of-band (or from a WithPrepareTurn hook) and the next model call sees the new form. Usage accounting and queued messages are unaffected.

func (*Session) ResolvePending

func (s *Session) ResolvePending(ctx context.Context, fn func(ctx context.Context, call ai.ToolCallPart) ([]ai.Part, error)) error

ResolvePending answers all of the session's pending tool calls: fn is invoked for each call in order, and the outcomes are appended as a single tool-result message. An fn error becomes an error tool result carrying the error text (return an error to reject a call), so the model learns the outcome either way. It is a no-op when nothing is pending and fails with ErrRunActive during an active run.

Example

A gate pauses risky calls for out-of-band approval; ResolvePending answers them and a second Run continues the conversation.

package main

import (
	"context"
	"log"
	"os"

	"github.com/rsbin1178/pips/agent"
	"github.com/rsbin1178/pips/ai"
	"github.com/rsbin1178/pips/ai/openai"
)

func main() {
	model := openai.New("gpt-6-astra", openai.WithAPIKey(os.Getenv("OPENAI_API_KEY")))

	deploy := agent.NewTool("deploy", "Deploy to production.",
		func(_ context.Context, _ struct{}) (string, error) {
			return "deployed", nil
		})

	a, err := agent.New(model,
		agent.WithTools(deploy),
		agent.WithBeforeTool(func(_ context.Context, info agent.ToolCallInfo) agent.ToolDecision {
			if info.Name == "deploy" {
				return agent.ToolDecision{Action: agent.ToolDecisionPause}
			}

			return agent.ToolDecision{}
		}),
	)
	if err != nil {
		log.Fatal(err)
	}

	sess := agent.NewSession()

	result, err := a.Run(context.Background(), sess, ai.UserText("Ship it."))
	if err != nil {
		log.Fatal(err)
	}

	if result.Stop == agent.StopPaused {
		// Approval happens outside the runtime, then the calls are resolved.
		err := sess.ResolvePending(context.Background(), func(_ context.Context, _ ai.ToolCallPart) ([]ai.Part, error) {
			return agent.TextResult("approved and deployed"), nil
		})
		if err != nil {
			log.Fatal(err)
		}

		if _, err := a.Run(context.Background(), sess); err != nil {
			log.Fatal(err)
		}
	}
}

func (*Session) ResolveToolCalls

func (s *Session) ResolveToolCalls(resolutions ...ToolResolution) error

ResolveToolCalls answers any subset of the session's pending tool calls. Resolutions are validated atomically and appended in the original call order, regardless of argument order. Calls omitted from resolutions remain pending and survive session serialization. It fails with ErrRunActive during a run, ErrInvalidToolResolution for empty/duplicate IDs, or ErrToolCallNotPending for a stale/unknown ID. An empty resolution list is a no-op.

func (*Session) Steer

func (s *Session) Steer(msgs ...ai.Message)

Steer queues messages for injection into the running loop: they are appended at the start of the next turn, before the next model call, letting a user redirect the agent mid-run. Queued messages survive until a run drains them (see WithSteeringMode); pending steering also keeps the loop going when the model would otherwise finish. Safe to call from any goroutine, with or without an active run.

func (*Session) UnmarshalJSON

func (s *Session) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler, replacing the session's state.

func (*Session) Usage

func (s *Session) Usage() ai.Usage

Usage returns the token usage accumulated across all runs on this session.

type StopReason

type StopReason string

StopReason is why a run terminated cleanly. Runs that fail (model error, context cancellation) return a Go error instead; a RunResult carried alongside a non-nil error has an empty StopReason.

const (
	// StopEndTurn means the model completed its turn without requesting any
	// tool calls — the natural end of an agent loop.
	StopEndTurn StopReason = "end_turn"
	// StopMaxTurns means the configured turn limit was reached (see
	// [WithMaxTurns]).
	StopMaxTurns StopReason = "max_turns"
	// StopBudget means the cumulative token budget was exhausted (see
	// [WithMaxTokens]).
	StopBudget StopReason = "budget"
	// StopPaused means a [WithBeforeTool] gate requested a pause. The
	// unexecuted calls are in [RunResult.Pending]; resolve them with
	// [Session.ResolveToolCalls] or [Session.ResolvePending] and run again to
	// continue.
	StopPaused StopReason = "paused"
	// StopWhen means the [WithStopWhen] condition reported true.
	StopWhen StopReason = "stop_when"
	// StopTerminated means every tool result in the final batch carried the
	// [ErrTerminate] hint, ending the run at the tools' request.
	StopTerminated StopReason = "terminated"
)

Stop reasons.

type Tool

type Tool interface {
	// Decl returns the declaration advertised to the model.
	Decl() ai.Tool
	// Exec runs the call and returns the result content. A nil part slice
	// with a nil error is a valid empty result.
	Exec(ctx context.Context, call ToolCall) ([]ai.Part, error)
}

Tool is a capability the model may invoke during a run. Decl describes the tool to the model; Exec performs the call. Implementations must be safe for concurrent use when they also implement ConcurrencySafe.

Exec errors do not abort the run: the runtime converts them (along with panics, timeouts, and undecodable arguments) into error tool results that are fed back to the model, which typically corrects course. Most tools are easier to write with NewTool than by implementing the interface directly.

func AsTool

func AsTool(a *Agent, name, description string) Tool

AsTool exposes an agent as a tool of another agent, the primitive for sub-agent delegation: the outer model calls the tool with a prompt, the inner agent runs it to completion in a fresh Session, and its final text becomes the tool result.

Each invocation is isolated (new session), so the tool is safe for Parallel marking and for concurrent calls. Cancellation propagates through ctx. An inner-agent failure surfaces as an error tool result the outer model can react to.

func NewTool

func NewTool[Args any](name, description string, fn func(ctx context.Context, args Args) (string, error)) Tool

NewTool defines a tool from a typed Go function. The argument schema is derived from Args with ai.SchemaFor (json tags name fields, description tags document them), and the model's JSON arguments are decoded into Args before fn runs. Use struct{} for a tool that takes no arguments.

NewTool panics when a schema cannot be derived from Args — like regexp.MustCompile, it is meant for declarations whose type is fixed at compile time. Implement Tool directly for dynamic schemas.

Example

NewTool derives the argument schema from an ordinary Go struct.

package main

import (
	"context"
	"fmt"
	"strconv"

	"github.com/rsbin1178/pips/agent"
)

func main() {
	add := agent.NewTool("add", "Add two integers.",
		func(_ context.Context, args struct {
			A int `json:"a"`
			B int `json:"b"`
		},
		) (string, error) {
			return strconv.Itoa(args.A + args.B), nil
		})

	decl := add.Decl()
	fmt.Println(decl.Name, decl.InputSchema.Type, decl.InputSchema.Properties["a"].Type)
}
Output:
add object integer

func NewToolParts

func NewToolParts[Args any](name, description string, fn func(ctx context.Context, args Args) ([]ai.Part, error)) Tool

NewToolParts is NewTool for tools whose results are multi-modal (for example an image-producing tool) rather than plain text.

func Parallel

func Parallel(t Tool) Tool

Parallel marks a tool as safe for parallel execution within a turn (see ConcurrencySafe). Only mark tools whose Exec is free of shared mutable state, such as read-only lookups.

func ProviderTool

func ProviderTool(tool ai.Tool) Tool

ProviderTool wraps a provider-executed ai.Tool (such as Gemini's GoogleSearch or OpenAI's WebSearch) as an agent.Tool. Provider-executed tools are managed server-side by the model provider, so their Exec method is a no-op.

type ToolCall

type ToolCall struct {
	// ID is the provider-assigned call identifier.
	ID string
	// Name is the tool being called.
	Name string
	// Args is the raw JSON arguments object produced by the model.
	Args ai.JSON
}

ToolCall is the invocation passed to Tool.Exec.

type ToolCallInfo

type ToolCallInfo struct {
	ToolCall
	// Turn is the turn (1-based) that produced the call.
	Turn int
	// BatchIndex is this call's zero-based position in the model response.
	BatchIndex int
	// BatchSize is the total number of calls in the model response. Gates may
	// use it to reject interaction-owning calls before a leading Pause hides
	// the remaining suffix.
	BatchSize int
}

ToolCallInfo is the read-only view of a tool call passed to gates.

type ToolCompleted

type ToolCompleted struct {
	Turn   int
	Call   ai.ToolCallPart
	Result ai.ToolResultPart
}

ToolCompleted closes the lifecycle of Call in the one-based Turn. Result includes denials and synthesized failures through Result.IsError.

type ToolDecision

type ToolDecision struct {
	Action ToolDecisionAction
	// Reason is sent to the model when Action is [ToolDecisionDeny]. Empty
	// falls back to a generic denial message.
	Reason string
	// UpdatedInput replaces the JSON arguments for an allowed call before the
	// next gate and before execution. Callers must validate it for their tool
	// protocol; nil leaves the original arguments unchanged.
	UpdatedInput ai.JSON
}

ToolDecision is a gate's verdict on one tool call.

func DenyTool

func DenyTool(reason string) ToolDecision

DenyTool returns a ToolDecisionDeny decision carrying the given reason.

type ToolDecisionAction

type ToolDecisionAction int

ToolDecisionAction is what a WithBeforeTool gate tells the runtime to do with a tool call.

const (
	// ToolDecisionAllow lets the call execute.
	ToolDecisionAllow ToolDecisionAction = iota
	// ToolDecisionDeny blocks the call; [ToolDecision.Reason] is fed back to
	// the model as an error tool result and the run continues.
	ToolDecisionDeny
	// ToolDecisionPause stops the run before executing this call. The call and every
	// later call in the same turn become [RunResult.Pending]; resolve them
	// with [Session.ResolvePending] and run again to continue.
	ToolDecisionPause
)

Gate actions. The zero value allows the call, so a gate only needs to return non-zero decisions for the calls it wants to intercept.

type ToolResolution

type ToolResolution struct {
	ToolCallID string
	Content    []ai.Part
	IsError    bool
}

ToolResolution supplies an out-of-band result for one pending tool call. Content is returned to the model; IsError marks a rejection or failed approval as a tool error rather than a run error.

type ToolResultInfo

type ToolResultInfo struct {
	ToolCall
	// Turn is the turn (1-based) that produced the call.
	Turn int
	// Result is the executed outcome (IsError reflects execution failures;
	// terminate hints are visible via [ToolResultOverride], not here).
	Result ai.ToolResultPart
}

ToolResultInfo is the read-only view passed to a WithAfterTool hook: the call that ran and the result it produced, before any override.

type ToolResultOverride

type ToolResultOverride struct {
	// Content replaces the result content.
	Content []ai.Part
	// IsError replaces the error flag.
	IsError *bool
	// Terminate replaces the tool's termination hint (see [ErrTerminate]).
	Terminate *bool
}

ToolResultOverride replaces fields of an executed tool result from a WithAfterTool hook. Each field is a full replacement; nil (or a nil pointer) keeps the executed value. There is no deep merge.

type ToolStarted

type ToolStarted struct {
	Turn int
	Call ai.ToolCallPart
}

ToolStarted opens the lifecycle of Call in the one-based Turn.

type ToolUpdated

type ToolUpdated struct {
	Turn   int
	Call   ai.ToolCallPart
	Update []ai.Part
}

ToolUpdated carries a best-effort progress Update for Call in the one-based Turn.

type TurnCompleted

type TurnCompleted struct {
	Turn  int
	Usage ai.Usage
}

TurnCompleted closes the one-based Turn. Usage is cumulative for the run.

type TurnStarted

type TurnStarted struct {
	Turn int
}

TurnStarted opens one model turn. Turn is one-based.

type TurnUpdate

type TurnUpdate struct {
	// Err aborts the run before another model call. Hook composition stops at
	// the first error so an application can make a failed context rewrite an
	// explicit run failure instead of continuing with stale messages.
	Err error
	// Model, when non-nil, serves all subsequent model calls of this run.
	Model ai.LanguageModel
	// ReplaceMessages, when non-nil, replaces the session history via
	// [Session.Replace] — the commit point for context compaction.
	ReplaceMessages []ai.Message
	// Tools, when non-nil, replaces the complete tool snapshot used for all
	// subsequent model calls and executions in this run. The replacement is
	// validated before it takes effect, so a declaration can never be shown to
	// the model without the matching executable implementation. This enables
	// deferred tool discovery without mutating the Agent shared by other runs.
	Tools []Tool
	// NextRequest, when non-nil, constrains exactly the next model request.
	// Unlike Tools, it does not replace the run's persistent tool snapshot.
	NextRequest *ModelRequestUpdate
}

TurnUpdate adjusts the run between turns, returned by a WithPrepareTurn hook. The zero value changes nothing.

Directories

Path Synopsis
Package bundle loads bounded local Bundles and activates their declarative resources through an extension.Runtime.
Package bundle loads bounded local Bundles and activates their declarative resources through an extension.Runtime.
Package catalog composes explicitly registered agent tools into policy-gated snapshots.
Package catalog composes explicitly registered agent tools into policy-gated snapshots.
Package continuation provides durable, application-driven execution across bounded worker runs.
Package continuation provides durable, application-driven execution across bounded worker runs.
Package extension composes trusted, application-compiled Agent extensions into immutable runtime generations.
Package extension composes trusted, application-compiled Agent extensions into immutable runtime generations.
Package goal provides evidence-based completion policies for continuation executions.
Package goal provides evidence-based completion policies for continuation executions.
Package harness provides the stateful orchestration layer over the agent runtime: persistent session trees with branching, automatic context compaction, branch summaries, and skill/template resources — the pieces an agent application needs beyond a single run.
Package harness provides the stateful orchestration layer over the agent runtime: persistent session trees with branching, automatic context compaction, branch summaries, and skill/template resources — the pieces an agent application needs beyond a single run.
Package loop provides fixed and dynamic activation policies for continuation executions.
Package loop provides fixed and dynamic activation policies for continuation executions.
Package agentmcp bridges tools from Model Context Protocol servers into the agent runtime using the official Go MCP SDK.
Package agentmcp bridges tools from Model Context Protocol servers into the agent runtime using the official Go MCP SDK.
Package observability derives vendor-neutral traces and aggregate metrics from agent events.
Package observability derives vendor-neutral traces and aggregate metrics from agent events.
otel
Package otel adapts pips agent events to OpenTelemetry traces and metrics.
Package otel adapts pips agent events to OpenTelemetry traces and metrics.
Package team provides durable coordination for a flat team of independent Agent sessions.
Package team provides durable coordination for a flat team of independent Agent sessions.

Jump to

Keyboard shortcuts

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