Documentation
¶
Overview ¶
Package agent is agentkit's batteries-included agent client over an OpenAI-compatible endpoint. It owns the TABLESTAKES every real client needs — so a consumer doesn't re-wrap them:
- the tool-call LOOP (chat → tool_calls → execute → feed back → repeat until the model stops calling tools or a terminal tool fires)
- context COMPACTION + LOD truncation (the Shaper) to fit the window
Slice 3 (not yet wired here; see plan/plan.md) adds message/notification INJECTION, LIFTING (async tool results), queued-message BATCHING, and grammar / JSON-Schema VALIDATION with a fix loop.
What agent does NOT own is ORCHESTRATION — roles, a task DAG, scheduling, when/why to run. That's the harness's job (e.g. autowork3), which drives an agent.Session and implements the small interfaces below. The claude- openai project is another consumer with its own Store impl.
The neutral seam: agent works in terms of llm.Message on the wire and Entry for persistence/shaping. It never imports a host's event model — the host maps its own rows onto Entry and back.
Index ¶
- Variables
- func Budget(contextTokens, reservePct int) int
- func DefaultContextBuilder(ctx context.Context, store Store, sessionID, system string) ([]llm.Message, error)
- func IsResolvedTruth(result string) bool
- func PendingResult(correlationID, toolCallID string, ttlSeconds int) string
- type CharsByFour
- type ClearRequest
- type Compaction
- type CompactionInfo
- type ContextBuilder
- type Entry
- type EntryKind
- type LLMRunner
- type LiftRequest
- type NotificationPreparer
- type PendingNotice
- type PreparerFunc
- type RevalidateStore
- type Revalidator
- type SchemaValidator
- type Session
- type Shaper
- type ShaperPolicy
- type Span
- type Store
- type TokenEstimator
- type TokenUsage
- type ToolDispatcher
- type Tracer
- type TurnResult
- type Validator
- type ValidatorFunc
Constants ¶
This section is empty.
Variables ¶
var ErrSessionClosed = errors.New("agent: session closed by terminal tool")
ErrSessionClosed is the sentinel a terminal-tool dispatcher returns to tell Turn the session was closed and no further chat rounds should fire.
Functions ¶
func Budget ¶
Budget returns the active-context budget for a model: the context-window size scaled down by the reserve percentage. reserve_pct=25 on a 254 000- token model yields 190 500, leaving room for the model's own response and any compaction work.
func DefaultContextBuilder ¶
func DefaultContextBuilder(ctx context.Context, store Store, sessionID, system string) ([]llm.Message, error)
DefaultContextBuilder renders the session's history verbatim in chronological order. The budget-aware Shaper does the same but adds the pristine-tail / LOD / compaction phases on top.
func IsResolvedTruth ¶
IsResolvedTruth reports whether a revalidator's "current truth" is empty — i.e. the condition resolved, so the notice should be cleared. Empty means: blank/whitespace, or a JSON empty value (null, {}, [], "").
func PendingResult ¶
PendingResult is the canonical result string a dispatcher substitutes for a lifted call so the model stops acting on the placeholder and wraps up. The host may use its own wording; this is a sensible default for new consumers.
Types ¶
type CharsByFour ¶
type CharsByFour struct{}
CharsByFour is the v1 default: byte length / 4, rounded up. Cheap, dependency-free, conservative for English+code.
func (CharsByFour) Estimate ¶
func (CharsByFour) Estimate(s string) int
type ClearRequest ¶
type ClearRequest struct {
Clear bool `json:"clear"`
GroupBy string `json:"group_by"`
Key string `json:"key"`
}
ClearRequest is the wire shape a tool result / integration callback returns to retract prior unshown notices for a group key:
{"clear": true, "group_by": "file", "key": "src/main.go"}
GroupBy names the notice field that partitions notices; Key is the value to clear. Both required.
func ParseClearRequest ¶
func ParseClearRequest(result string) (ClearRequest, bool)
ParseClearRequest reports whether a result opted into a clear. ok is true only for a JSON object with clear=true and non-empty group_by + key.
type Compaction ¶
type Compaction struct {
Marker Entry // Kind=KindCompaction, Content=summary, CreatedAt already placed
Subsumes []Entry // the entries folded into Marker (Origin intact)
}
Compaction is what the Shaper hands Store.Compact: a summary marker plus the entries it subsumes. The host writes the marker + flags every subsumed entry (routing by Entry.Origin) in one transaction.
type CompactionInfo ¶ added in v0.2.0
type CompactionInfo struct {
Summary string // the tight summary that replaced the folded prefix
SubsumedCount int // how many entries were folded into the summary
TokensBefore int // estimated active-window tokens before this compaction
TokensAfter int // estimated active-window tokens after
}
CompactionInfo describes a compaction the Shaper performed during a Turn. It is surfaced (via Turn's result and the OnCompaction callback) so the host can persist the summary as a hidden field on the turn and show meta about what was folded. Token counts are the active estimator's numbers.
type ContextBuilder ¶
type ContextBuilder func(ctx context.Context, sessionID string, system string) ([]llm.Message, error)
ContextBuilder materializes the message list shown to the LLM from the session's history. DefaultContextBuilder is the plain merge; a Shaper adds pristine-tail / LOD / compaction on top.
type Entry ¶
type Entry struct {
ID string
Kind EntryKind
Content string
ToolCallID string // correlates KindToolCall / KindToolResult
ToolName string // the tool for KindToolCall / KindToolResult
// Tag is an opaque display label used only when rendering a
// KindNotification (e.g. the host's raw event-type "nudge"). Empty →
// the Kind string is used.
Tag string
// Origin is an opaque host provenance tag. agent never reads it; it is
// carried on entries returned by Store.Context and handed back inside
// Compaction.Subsumes so the host can route subsumed rows to the right
// storage (e.g. autowork3's public-delivery vs private-log streams).
Origin string
CreatedAt int64 // ns; ordering key
}
Entry is one durable conversation record — the neutral shape the Store persists and the Shaper reasons over. A host maps its own rows onto this and back; fields agent doesn't interpret (Tag, Origin) are round-tripped verbatim.
type EntryKind ¶
type EntryKind string
EntryKind classifies a conversation entry for shaping + rendering. The host maps its own event types onto these; anything that isn't one of the first five maps to KindNotification.
const ( KindUser EntryKind = "user" // a human/injected message KindAssistant EntryKind = "assistant" // an LLM reply KindToolCall EntryKind = "tool_call" // the model asked to call a tool KindToolResult EntryKind = "tool_result" // a tool's result KindCompaction EntryKind = "compaction" // a marker that subsumes older entries KindNotification EntryKind = "notification" // system push (nudge, friction, …); tail-kept, budget-neutral )
type LLMRunner ¶
type LLMRunner interface {
ChatStream(ctx context.Context, messages []llm.Message, tools []llm.ToolDef, opts *llm.ChatOpts) (<-chan llm.StreamChunk, error)
}
LLMRunner does one streaming chat round-trip. opts is optional — nil means default behavior. It carries ToolChoice for forcing a tool call (used by protocol-bound roles whose only exit is a structured terminal tool).
type LiftRequest ¶
type LiftRequest struct {
Pending bool `json:"pending"`
CorrelationID string `json:"correlation_id"`
TTLSeconds int `json:"ttl_s"`
}
LiftRequest is the parsed async-tool opt-in.
func ParseLiftRequest ¶
func ParseLiftRequest(result string) (LiftRequest, bool)
ParseLiftRequest reports whether a tool result opted into async semantics. ok is true only when the result is a JSON object with pending=true and a non-empty correlation_id. Cheap-rejects non-JSON without allocating.
type NotificationPreparer ¶
type NotificationPreparer interface {
PrepareNotifications(ctx context.Context, sessionID string) error
}
NotificationPreparer revalidates a session's pending notifications and clears the stale ones, mutating the host's notification store. It runs at the top of every Turn iteration, after the inbox is claimed and before the context is built — so a resolved notice is gone before it is rendered.
func MCPPreparer ¶
func MCPPreparer(store RevalidateStore, rv Revalidator) NotificationPreparer
MCPPreparer builds a NotificationPreparer over the convention. A revalidation error on one notice is skipped (fail-open: keep the notice) so a flaky tool can't wedge the Turn.
type PendingNotice ¶
PendingNotice is one revalidatable notice: its group field + the value.
type PreparerFunc ¶
PreparerFunc adapts a function to NotificationPreparer.
func (PreparerFunc) PrepareNotifications ¶
func (f PreparerFunc) PrepareNotifications(ctx context.Context, sessionID string) error
type RevalidateStore ¶
type RevalidateStore interface {
// PendingNotices lists the session's unshown notices carrying a group key.
PendingNotices(ctx context.Context, sessionID string) ([]PendingNotice, error)
// Clear retracts the notice for (groupBy, key) in the session.
Clear(ctx context.Context, sessionID, groupBy, key string) error
}
RevalidateStore is the host's notice substrate.
type Revalidator ¶
type Revalidator interface {
Revalidate(ctx context.Context, groupBy, key string) (result string, ok bool, err error)
}
Revalidator calls an integration's masked revalidator tool. ok is false when no tool is configured for groupBy (nothing to check → keep the notice); otherwise result is the tool's raw output ("" = resolved).
type SchemaValidator ¶
type SchemaValidator struct {
// contains filtered or unexported fields
}
SchemaValidator is the lightweight default Validator, built from the same tool defs the session advertises. It is deliberately dependency-free and conservative — it catches the common structured-output failures without a full JSON-Schema engine:
- arguments must be a JSON object
- every `required` property named in the tool's schema must be present and non-null
- a present property whose schema declares a primitive `type` (string/number/integer/boolean/array/object) must not be the wrong JSON kind
Unknown tools pass (the host may dispatch tools it didn't declare). For stricter guarantees a consumer plugs its own Validator (e.g. a real JSON-Schema library) — the interface is the seam.
func NewSchemaValidator ¶
func NewSchemaValidator(tools []llm.ToolDef) *SchemaValidator
NewSchemaValidator indexes the required-keys + declared types from each tool def's parameters schema. Tool defs whose parameters aren't a standard object schema simply contribute no constraints.
func (*SchemaValidator) ValidateArgs ¶
func (sv *SchemaValidator) ValidateArgs(toolName, argsJSON string) error
type Session ¶
type Session struct {
ThreadID string // opaque grouping label (stamped on spans + the chat trace id)
SessionID string
System string // baseline system prompt
Store Store
Runner LLMRunner
Build ContextBuilder // nil → DefaultContextBuilder over Store
Tools []llm.ToolDef
Dispatch ToolDispatcher
// ChatOpts forwards LLM-call options on every round-trip. Nil OK. Used
// by protocol-bound roles to force tool_choice=required.
ChatOpts *llm.ChatOpts
// OnAssistantToken, if set, receives streamed content chunks for
// SSE / live broadcast.
OnAssistantToken func(string)
// OnCompaction, if set, fires whenever the Shaper folds history mid-Turn —
// the same info Turn returns in TurnResult.Compactions. A host persists the
// summary as a hidden field on the turn and/or shows meta about it.
OnCompaction func(CompactionInfo)
// OnUsage, if set, fires after each chat round with the running token tally
// (cumulative Total + current Active window). Mirrors TurnResult.Usage.
OnUsage func(TokenUsage)
// Estimate counts tokens for the Active-window figure. Nil → Default()
// (chars/4). Set to the same estimator the Shaper uses for consistency.
Estimate TokenEstimator
// MaxTurns caps the loop (default 100 — generous, since a role may chain
// read tool calls before its terminal output; better to pay extra
// round-trips than wedge the pipeline). Tests set this small.
MaxTurns int
// ForcedTerminalTool, when non-empty, names the single tool the caller
// pinned via ChatOpts.ToolChoice as the session's only legitimate exit.
// If MaxTurns elapses without the model ever invoking it, Turn returns a
// diagnostic naming the tool — distinguishing "ran out of budget" from
// "provider silently ignored tool_choice and never called the right
// tool". Empty for roles where every tool is acceptable.
ForcedTerminalTool string
// Preparer, if set, revalidates + clears stale pending notifications
// before each iteration's context is built (the prepareNotifications-
// BeforeSend seam). Nil = no preparation.
Preparer NotificationPreparer
// SpanPrefix names the span namespace (default "agent"): spans are
// "<prefix>.Turn" and "<prefix>.streamChat". A host sets this to keep
// its existing observability labels stable.
SpanPrefix string
// Now is overridable in tests.
Now func() int64
// Tracer optionally captures spans. Nil = no tracing.
Tracer Tracer
// contains filtered or unexported fields
}
Session bundles everything needed to run one agent conversation through the unified turn loop. The host constructs it per unit of work and calls Turn. The turn model is event-driven: async arrivals reach the model at two seams only — the return of a tool result, or the end of an assistant turn — so Turn claims the pending inbox at the top of every iteration and re-checks after an idle response.
func (*Session) Inject ¶
Inject appends an entry to this session's log so the NEXT Turn renders it — notification / message injection. ID and CreatedAt are filled if zero. Injection into a different session's inbox is a host concern (broadcast / delivery); this is the self-inbox primitive.
func (*Session) Turn ¶
func (s *Session) Turn(ctx context.Context) (result TurnResult, err error)
Turn runs the unified loop and returns a TurnResult: the model's final reply, any compactions the Shaper performed to stay in budget, and the running token tally (cumulative Total + current Active window).
Queued-message BATCHING is inherent here: ClaimPending marks ALL pending inbox arrivals shown at the top of an iteration, and build() renders every non-subsumed entry — so N messages that queued between activations are seen in ONE turn, not one turn each.
type Shaper ¶
type Shaper struct {
Store Store
Runner LLMRunner
Estimate TokenEstimator
Policy ShaperPolicy
// SpanPrefix names the span namespace (default "agent"): the build span
// is "<prefix>.Shaper.Build". A host sets this to keep its labels stable.
SpanPrefix string
Tracer Tracer // optional; nil = no spans
}
Shaper builds the LLM message list with a three-step algorithm:
- pristine tail: the last N text messages + M tool-call exchanges are always included verbatim regardless of size.
- LOD render: older entries whose content exceeds the policy threshold get truncated stubs (entry-id pointer + head). No writes — pure render-time transformation; the source entry stays intact.
- Compaction: if LOD-truncated context still exceeds budget, summarize the oldest contiguous prefix of "older" entries into a compaction marker via Store.Compact. Build re-runs with the marker substituted.
type ShaperPolicy ¶
type ShaperPolicy struct {
BudgetTokens int
PreserveLastMessages int
PreserveLastToolCalls int
LODTruncateAboveChars int
// LODHeadroomTokens is the runway kept below BudgetTokens. The Shaper
// restructures (LOD, then compaction) once the estimated context would
// exceed BudgetTokens-LODHeadroomTokens — NOT at the hard budget. Keeping
// headroom means the prompt prefix is rewritten (and the KV cache
// invalidated) in one decisive pass with room to spare, instead of eagerly
// re-truncating a little more every turn. 0 → defaultLODHeadroom (10k);
// negative → no headroom (restructure only at BudgetTokens).
LODHeadroomTokens int
}
ShaperPolicy captures per-model context-window policy. A host builds it from its model row at session-resolve time.
type Span ¶
Span is one open span. Set attaches an attribute (chainable). Exactly one of End / EndError closes it.
type Store ¶
type Store interface {
// ClaimPending marks the session's pending inbox arrivals as shown and
// returns how many there were. The loop uses the count to decide whether
// a re-prompt is warranted; the entries themselves surface via Context.
ClaimPending(ctx context.Context, sessionID string, at int64) (int, error)
// Append persists one entry (an assistant reply, a tool result, …).
Append(ctx context.Context, sessionID string, e Entry) error
// Context returns the session's non-subsumed entries (any order; the
// engine sorts). The host merges whatever internal streams it has into
// this single list.
Context(ctx context.Context, sessionID string) ([]Entry, error)
// Compact records a compaction marker + flags the subsumed entries, in
// one transaction.
Compact(ctx context.Context, sessionID string, c Compaction) error
}
Store is the minimal conversation persistence the loop needs. A host (autowork3's event tables; the claude-openai project's own storage; an in-memory slice in tests) implements it. No host event types leak here.
type TokenEstimator ¶
TokenEstimator estimates the token count of a string for budget calculations. The default is a deliberately conservative chars/4 heuristic that overcounts most real tokenizers; the context shaper relies on this conservatism for safety. Real tokenizers plug in behind the same interface.
type TokenUsage ¶ added in v0.2.0
type TokenUsage struct {
// Total is cumulative prompt+completion tokens billed across every chat
// round of this Session's lifetime (what you paid). A host that reuses one
// Session across a whole conversation sees the running total; one that makes
// a fresh Session per Turn should persist + sum these itself.
Total int
// Active is the token count of the CURRENT live window — the built
// (compacted + LOD) context the model sees now, INCLUDING any compaction
// summary. This is the underlying-session size, distinct from Total.
Active int
}
TokenUsage is the session-level token accounting surfaced every Turn.
type ToolDispatcher ¶
ToolDispatcher executes a tool call and returns the string the model will see as the tool result. Errors meant to reach the model (unknown tool, bad args) should be formatted into the result; return a non-nil error only to abort the whole turn. Returning ErrSessionClosed tells the loop to stop after the result is persisted — used by terminal tools so the loop doesn't keep prompting an already-closed session under tool_choice=required.
func ValidatingDispatcher ¶
func ValidatingDispatcher(inner ToolDispatcher, v Validator) ToolDispatcher
ValidatingDispatcher returns a ToolDispatcher that validates arguments before delegating to inner. On validation failure it returns the fix instruction as the (non-error) result — keeping the session active so the model can retry — and never calls inner. A nil validator is a pass-through.
type Tracer ¶
type Tracer interface {
// Start opens a span named `name` and returns it plus a child context
// carrying the span, so nested Starts chain into a tree.
Start(ctx context.Context, name string) (Span, context.Context)
}
Tracer optionally captures spans for the turn loop, chat round-trips, and context builds. A host adapts its own tracing (e.g. autowork3's internal/ trace) onto this. Nil Tracer = no tracing, zero overhead.
type TurnResult ¶ added in v0.2.0
type TurnResult struct {
Reply string
Compactions []CompactionInfo // usually empty or one; more if a huge context folds in stages
Usage TokenUsage
}
TurnResult is what Turn returns: the model's final reply, whatever the loop did to the context to keep it in budget, and the running token tally.
type Validator ¶
Validator gates a tool call before dispatch. A nil error means accept; a non-nil error's message is fed back to the model as the tool result (the fix instruction).
type ValidatorFunc ¶
ValidatorFunc adapts a function to Validator.
func (ValidatorFunc) ValidateArgs ¶
func (f ValidatorFunc) ValidateArgs(toolName, argsJSON string) error