Documentation
¶
Overview ¶
Package agentloop is a small reasoning-loop engine for LLM agents that think by writing JavaScript instead of calling named tools.
Each turn the model emits one fenced ```javascript block defining `function run(args) { ... }`. The loop executes it in a sandboxed goja runtime (package sandbox) and threads its return value into the next turn as `args` — full fidelity, server-side, never serialised back into the prompt. The model sees only its own log() output plus a compact structural digest of what it returned. A run finishes when the script calls answer(result).
This design — return-threading instead of a growing tool-call transcript — keeps the prompt small even for many-step runs: stale code is elided from history (only the most recent turn's script is kept verbatim) and large data never appears in the context window twice.
A minimal wiring looks like:
client, _ := llm.NewOpenAI(llm.ConfigFromEnv())
caps := agentloop.DefaultCapabilities(client, "")
loop := agentloop.New(agentloop.Config{
LLM: client,
Sessions: mySessionStore,
Steps: myStepStore,
SandboxBuilder: &agentloop.DefaultSandboxBuilder{Capabilities: caps},
})
result, err := loop.Run(ctx, agentloop.RunRequest{
SessionID: "session-1",
Message: "What's 2+2, and say it back as markdown?",
})
See examples/cli for a complete runnable program with in-memory stores.
Index ¶
- Constants
- Variables
- func ComposeSystemPrompt(persona, sandboxAPI string) string
- func EstimateTokens(s string) int
- func ExtractDoneMarker(s string) (done bool, final string)
- func ExtractJSBlock(s string) string
- func TextEmissionSystemPrompt() string
- type BuildContext
- type CallTokens
- type Capability
- type CompactResult
- type CompactionCheckpoint
- type Compactor
- type Config
- type ContextBudget
- type DefaultSandboxBuilder
- type FinalizeSummary
- type Loop
- type Profile
- type RunEvent
- type RunRequest
- type RunResult
- type RunStep
- type RunSummary
- type SandboxBuilder
- type Scope
- type Session
- type SessionStore
- type StepStore
- type SummarizingCompactor
- type TokenEstimator
- type TokenUsage
Constants ¶
const DefaultCompactPrompt = `` /* 902-byte string literal not displayed */
DefaultCompactPrompt is the instruction SummarizingCompactor sends when Prompt is empty. It is exported so an application can extend it rather than rewrite it from scratch.
const DefaultMergePrompt = `` /* 972-byte string literal not displayed */
DefaultMergePrompt is the instruction sent when several partial summaries have to be folded into one, i.e. when the transcript did not fit a single summarization call. Overridable via SummarizingCompactor.MergePrompt.
It is a separate instruction from DefaultCompactPrompt because the input is a different kind of thing: already-summarized prose rather than a transcript of turns, where the risk is concatenating superseded facts rather than losing specifics.
const HistoryWindow = 80
HistoryWindow is the default cap on how many prior steps the loop replays into the LLM's context window (override via Config.HistoryWindow). Roughly the last several user turns of full reasoning trails before the oldest start to drop — beyond this the prompt gets expensive and the model loses the user's actual question in the noise.
const MaxIterations = 20
MaxIterations is the default cap on LLM round-trips a single Run will make (override via Config.MaxIterations).
const RunTimeout = 5 * time.Minute
RunTimeout is the default wall-clock cap on a single Run call (override via Config.RunTimeout).
const StepTypeSummary = "summary"
StepTypeSummary marks a compaction checkpoint in a session's step trace: a RunStep whose Content is the summary to replay in place of the steps it covers, and whose ToolArgs carries a CompactionCheckpoint saying where the retained history resumes.
The steps a checkpoint covers stay in the trace — this changes only what is replayed to the model, never what a human reviewing the session can see.
Variables ¶
var ErrEmptyResponse = errors.New("agentloop: model returned an empty response")
ErrEmptyResponse is returned when the model yields no content across the allowed retries. Distinct from a normal completion so callers can treat it as a retryable failure instead of silently finishing with an empty answer.
Functions ¶
func ComposeSystemPrompt ¶
ComposeSystemPrompt builds the full system prompt the loop sends to the LLM for a session: the text-emission contract, then the optional persona, then the sandbox's primitive documentation. Persona AFTER protocol, sandbox API LAST so declarations sit close to the user message.
func EstimateTokens ¶ added in v0.4.0
EstimateTokens approximates how many tokens s occupies, without a tokenizer: byte length over a fixed divisor.
It is intentionally crude. A real tokenizer is per-model, pulls in a vocabulary, and would have to be kept in step with providers this package deliberately knows nothing about — while the decision it feeds ("does one more message fit?") tolerates a wide margin, since the reserve absorbs the error. Supply ContextBudget.Estimate when you have the real thing and want the context filled tighter.
func ExtractDoneMarker ¶
ExtractDoneMarker reports whether s contains a terminating DONE marker, and if so returns the answer that follows it. The marker must be on its own line — an inline "DONE" inside prose doesn't prematurely terminate the run. answer() is the documented way to finish; this is a defensive fallback for a model that emits the legacy marker instead.
func ExtractJSBlock ¶
ExtractJSBlock returns the contents of the first ```javascript or ```js fenced block, or "" if none is present. Whitespace inside the block is trimmed — some models add a blank line right after the fence and the intent is unaffected.
func TextEmissionSystemPrompt ¶
func TextEmissionSystemPrompt() string
TextEmissionSystemPrompt is the workflow contract the loop layers on top of the sandbox's primitive documentation. It defines the run(args)→return protocol: each turn the model emits one fenced ```javascript block defining `function run(args)`, whose return value the loop threads into the next turn as `args` (full fidelity, server-side, never serialised into the prompt — the model sees only a structural shape digest of it plus its own log() output). The run finishes when the script calls answer(result).
Kept free of primitive listings — those come from the registered packs via sandbox.Sandbox.SystemPrompt() and are appended by the loop under "## Sandbox API".
The runtime notes are empirically grounded: goja executes modern JS syntax (arrows, const/let, template literals, destructuring, spread, optional chaining), but has NO event loop — Promise/async code parses and then its continuations silently never run, which is why the prompt bans them outright rather than saying "unsupported".
Types ¶
type BuildContext ¶
type BuildContext struct {
// Ctx is the per-run context. Capabilities should honour
// cancellation — a long-running primitive (fetch, ai()) must abort
// when the run's deadline fires.
Ctx context.Context
// Scope is the tenant boundary. Capabilities that touch
// application data must filter on it.
Scope Scope
// SessionID identifies the session this run extends.
SessionID string
// MessageID is the inbound message that started this session, if
// any (mirrors Session.MessageID — carried here too so a capability
// doesn't need the Session value itself).
MessageID string
// UserID is the invoking user, empty for system-initiated runs.
UserID string
// EnabledCapabilities is the session's capability allowlist. nil
// means "default-all"; a non-nil slice (possibly empty) means "only
// load capabilities whose Name appears here." AlwaysOn capabilities
// load regardless.
EnabledCapabilities *[]string
}
BuildContext is the per-run bag of dependencies each capability's Build receives. Application-specific dependencies (a database handle, an accumulator slice, …) that a capability needs should be closed over when the Capability is constructed, not threaded through here — see DefaultCapabilities for the pattern.
type CallTokens ¶
CallTokens is the per-LLM-call token count carried on execute_js_result and response events, for fine-grained reporting.
type Capability ¶
type Capability struct {
// Name is the stable identifier a per-session allowlist can
// reference (see BuildContext.EnabledCapabilities).
Name string
// Description is shown in a capability catalog / skill listing.
Description string
// AlwaysOn skips the enabled-capabilities allowlist filter — for
// capabilities nothing should be able to disable without making the
// runtime unusable (e.g. require()).
AlwaysOn bool
// Build runs at session-start with the per-run BuildContext. Empty
// returns are fine: a capability with a missing optional dependency
// (no LLM key configured, say) should return (nil, nil) so the
// session can proceed without it.
Build func(BuildContext) ([]sandbox.Pack, error)
}
Capability is the seam between the loop and application-supplied packs — a named, optionally-gated unit of sandbox functionality a SandboxBuilder composes into a session's sandbox.
func DefaultCapabilities ¶
func DefaultCapabilities(llmClient llm.Client, model string) []Capability
DefaultCapabilities is the general-purpose bundle most agents want: require() (always on), require('http'), require('markdown'), fetch() / htmlToMarkdown(), and — when llmClient is non-nil — ai() / aiJSON(). model is the model passed to every ai()/aiJSON() sub-call; empty uses the client's own default.
Passing llmClient == nil is valid: the "ai" capability's Build then returns (nil, nil) and the session simply has no ai()/aiJSON() primitive, rather than failing to start.
type CompactResult ¶ added in v0.4.0
type CompactResult struct {
// Messages is the history to send, chronological.
Messages []llm.Message
// Tokens is what producing it cost, zero for an implementation that
// makes no provider call.
Tokens TokenUsage
// Summary, when non-empty, is the exact message content to replay
// in place of the folded-away history on FUTURE runs. Setting it
// (together with RetainedFrom) is what makes a compaction durable:
// the loop persists it as a StepTypeSummary checkpoint, and the
// next Run rehydrates from that instead of summarizing again.
//
// Leave it empty to compact for this run only. That costs a
// summarization per Run, so an implementation that can express its
// result as "one message replacing a prefix" should set it.
Summary string
// RetainedFrom is the index, in the history passed to Compact, of
// the first message kept verbatim — everything before it is what
// Summary stands for. Ignored when Summary is empty, and a value
// outside (0, len(history)] disables persistence: a compaction that
// folded nothing away has no checkpoint worth writing.
RetainedFrom int
}
CompactResult is one compaction attempt's output.
type CompactionCheckpoint ¶ added in v0.4.0
type CompactionCheckpoint struct {
// RetainFromStep is the StepIndex the retained history resumes at.
// Every earlier step is represented by the summary instead of being
// replayed, so a session that has been compacted once does not pay
// to summarize the same turns again on the next Run.
RetainFromStep int32 `json:"retain_from_step"`
}
CompactionCheckpoint is the ToolArgs payload of a StepTypeSummary step.
type Compactor ¶ added in v0.4.0
type Compactor interface {
Compact(ctx context.Context, history []llm.Message) (CompactResult, error)
}
Compactor shrinks a session's rehydrated history before the loop starts its turns, so a long conversation survives as a summary instead of being silently truncated.
Compact receives the prior conversation in chronological order, WITHOUT the current user message (which the loop appends afterwards and always sends verbatim). It returns the history to actually send. Returning the input unchanged is always valid and is what an implementation should do when there is nothing worth compacting — the loop makes no assumption that the result is shorter.
Tokens must report what the implementation spent, and must be populated even when Compact returns an error: a provider call that produced an unusable summary is still billable, and a Run's reported usage would otherwise understate what the session cost.
type Config ¶
type Config struct {
// LLM is the per-Run chat client.
LLM llm.Client
// Sessions persists session metadata. Required.
Sessions SessionStore
// Steps persists the per-turn trace. Required.
Steps StepStore
// SandboxBuilder constructs the sandbox for a Run. Required.
SandboxBuilder SandboxBuilder
// Policy gates side-effecting primitives. Optional; nil installs
// sandbox.DefaultPolicy (conservative: deny by default).
Policy sandbox.PolicyChecker
// Model is the default chat model when the session has none pinned.
// Optional; falls back to the LLM client's own default when empty.
Model string
// MaxIterations caps LLM round-trips per Run. Zero uses the package
// default.
MaxIterations int
// RunTimeout is the wall-clock cap per Run. Zero uses the package
// default.
RunTimeout time.Duration
// HistoryWindow caps how many prior steps are rehydrated into the
// LLM context. Zero uses the package default.
//
// With a Compactor configured this becomes how much of the past the
// loop CONSIDERS rather than how much it sends: everything loaded
// is handed to the Compactor, which decides what survives verbatim.
// Raising it is how a session gets long-term memory — the prompt
// stays bounded by the compactor, not by this.
HistoryWindow int
// Compactor folds the older part of a long session's history into a
// summary before the run's turns begin, so early context survives
// in compressed form instead of falling off the end of
// HistoryWindow unnoticed.
//
// A compaction that reports a CompactResult.Summary is persisted as
// a StepTypeSummary checkpoint, and later runs rehydrate from that
// rather than summarizing the same turns again — see
// rehydrateHistory. The covered steps remain in the trace; only
// what is replayed to the model changes.
//
// Optional; nil keeps the default behaviour — history is truncated
// to HistoryWindow and the overflow is simply gone. Compaction is
// best-effort: a Compactor that fails warns and the run proceeds on
// the uncompacted history.
Compactor Compactor
// ContextBudget bounds each turn's prompt in TOKENS, as a hard
// backstop under HistoryWindow and Compactor — both of which count
// MESSAGES and so cannot tell a forty-token turn from a
// four-thousand-token one. Optional; the zero value is disabled and
// preserves the prior behaviour exactly.
//
// Enabling it is what turns a context overflow from a provider
// error (or a silent server-side truncation) into a deliberate,
// observable trim: the oldest turns are dropped, the model is told
// how many, and a "budget_trimmed" event reports it. Set
// MaxTokens to the window the model is actually SERVED with — for a
// local runtime that is the server's configured context size, not
// the figure on the model card.
ContextBudget ContextBudget
// Now is a clock seam for tests. Nil uses time.Now.
Now func() time.Time
// TracerProvider produces spans for each Run — one root span per
// call plus child spans for sandbox build, each turn, its LLM call,
// and its JS execution. Optional; nil installs a no-op tracer, so
// leaving this unset costs a few allocations and produces no spans.
// Wire in an OTel SDK TracerProvider (e.g. configured with an OTLP
// exporter pointed at a Jaeger collector) to observe Run calls in
// production — nothing else in this package needs to change.
TracerProvider trace.TracerProvider
// Redactor strips known secret values out of every surface this
// package writes free text to: RunEvent (Content and Args, before
// OnEvent sees it), RunResult.FinalText, persisted RunStep.Content
// (via Steps.Append), and error messages recorded on a span.
// Optional; nil is a safe no-op (see redact.Redactor) — the
// defense-in-depth case this exists for is a script that logs a
// fetched credential (log(secret("KEY")), or a fetch() response
// that echoes one back) and would otherwise carry it into
// whatever OnEvent forwards to, the persisted trace, or a trace
// backend. Build one with redact.FromSecrets over the secret
// values your capabilities can return this session.
//
// One trade-off: setting this suppresses "response_chunk" events
// (live token-by-token streaming). A secret can split across two
// chunk boundaries with neither chunk containing the whole value to
// match against, so per-chunk redaction can't be made safe — the
// complete, redacted text still arrives via the terminal "response"
// event instead.
Redactor *redact.Redactor
}
Config wires the dependencies the loop needs.
type ContextBudget ¶ added in v0.4.0
type ContextBudget struct {
// MaxTokens is the model's context window. Zero disables budgeting
// entirely — the loop then behaves exactly as it did before, with
// the prompt bounded only by HistoryWindow and the Compactor.
//
// Set it to the SERVED window, which for a locally hosted model is
// what the server was started with (llama.cpp's --ctx-size, Ollama's
// num_ctx), not what the model card advertises.
MaxTokens int
// Reserve is how much of MaxTokens is held back for the completion.
// Zero uses MaxTokens/8, clamped to [512, 4096] and never more than
// half the window.
//
// It doubles as the request's output cap: when a budget is enabled
// and CompletionRequest.MaxTokens would otherwise be unset, the
// loop sends Reserve. Otherwise the reserve is a wish rather than a
// guarantee — nothing would stop the model from generating past the
// room left for it.
Reserve int
// Estimate overrides EstimateTokens. Supply a real tokenizer to
// fill the window tighter; the default's error is absorbed by
// Reserve.
Estimate TokenEstimator
}
ContextBudget bounds a turn's prompt in TOKENS rather than messages. The zero value is disabled, which is why adding one to an existing Config changes nothing until MaxTokens is set.
When enabled, the loop trims each turn's request immediately before sending it — after compaction, after stale-code elision, and after this run's own turns have grown the history. That placement is the point: history grows DURING a Run (every run() block and execution result appends to it), so a check performed once at rehydrate time would pass and then overflow three turns later.
The system prompt and the most recent message are never dropped; the oldest conversational turns go first, and a marker records how many.
type DefaultSandboxBuilder ¶
type DefaultSandboxBuilder struct {
// Capabilities is the full set this builder can install; each
// Build call filters it down via EnabledCapabilities.
Capabilities []Capability
// EnabledCapabilities is the allowlist passed through to every
// capability's BuildContext. nil means "all enabled".
EnabledCapabilities *[]string
// MaxLogBytes caps one turn's log() output. Zero uses
// sandbox.DefaultMaxLogBytes; negative removes the cap.
//
// Worth lowering for a small context window: a turn's logs are
// replayed in every later prompt of the session, so this is a
// per-turn cost that compounds. Profile.MaxLogBytes derives a value
// from a model's window.
MaxLogBytes int
}
DefaultSandboxBuilder is the simplest SandboxBuilder: it composes a fixed Capabilities list into a fresh sandbox.Sandbox for every Run, filtered by EnabledCapabilities (nil = all enabled). Applications whose capability set varies per scope/session (e.g. a per-tenant allowlist pulled from a database) should implement SandboxBuilder themselves — its Build method is a good starting point to copy.
func (*DefaultSandboxBuilder) Build ¶
func (b *DefaultSandboxBuilder) Build(ctx context.Context, sess Session, scope Scope, onEvent sandbox.OnEvent) (*sandbox.Sandbox, func(), error)
Build implements SandboxBuilder. A capability whose Build fails is logged and skipped — via a "warning" sandbox.Event when onEvent is non-nil, and always via slog — rather than aborting the whole session: one flaky capability shouldn't deny the user their turn.
type FinalizeSummary ¶
type FinalizeSummary struct {
Status string
PromptTokens int32
CompletionTokens int32
StepCount int32
DurationMs int32
// DataBytesCarried sums, over every LLM call of this run, the bytes
// of threaded working state held server-side minus the shape digest
// actually sent.
DataBytesCarried int64
}
FinalizeSummary is what the loop hands to Finalize at the end of a run (success or failure).
type Loop ¶
type Loop interface {
Run(ctx context.Context, req RunRequest) (RunResult, error)
}
Loop is the reasoning-loop contract. One call to Run drives a multi-turn rehydrate-execute-respond cycle for one user message, finishing when the model calls answer() (or emits a legacy DONE marker, or answers with no code fence at all).
type Profile ¶ added in v0.4.0
type Profile struct {
// ContextBudget bounds each turn's prompt. Goes in Config.
ContextBudget ContextBudget
// HistoryWindow is how many steps the loop rehydrates. Goes in
// Config.
HistoryWindow int
// CompactTrigger and CompactKeep configure a SummarizingCompactor,
// in messages.
CompactTrigger int
CompactKeep int
// SummarizeBudget bounds one summarization call. Set it on
// SummarizingCompactor.Budget when the summarizer runs on the same
// model — the common case for a local setup. Point the compactor at
// a larger hosted model and this is the field to override.
SummarizeBudget ContextBudget
// MaxLogBytes caps one turn's log() output. Goes in
// DefaultSandboxBuilder.MaxLogBytes.
MaxLogBytes int
// ProjectInlineBytes is how much of each instruction file belongs in
// the prompt. Goes in projectctx.Loader.InlineBytes — which this
// package cannot set for you, since nothing in the core imports
// projectctx.
ProjectInlineBytes int
}
Profile is a coherent set of context settings derived from one model's context window.
It is a starting point, not a constraint: read the fields, change what your workload justifies. Every value it produces is one you could have written by hand.
func ProfileFor ¶ added in v0.4.0
ProfileFor derives context settings for a model with the given context window, in tokens.
Use the window the model is actually SERVED with: for a local runtime that is the server's configured context size (llama.cpp's --ctx-size, Ollama's num_ctx), not the figure on the model card. A window the server will not honour produces a profile that fits nothing.
A non-positive window returns the zero Profile, whose Apply is a no-op — so "I don't know the window" degrades to today's defaults rather than to a guess.
func (Profile) Apply ¶ added in v0.4.0
Apply writes the profile's loop-level settings into cfg: everything Config itself owns, plus the compactor's message counts and budget when cfg.Compactor is a *SummarizingCompactor.
It does NOT touch the sandbox builder or the project-instruction loader, which are separate objects the application constructs — pass Profile.MaxLogBytes and Profile.ProjectInlineBytes to those yourself:
p := agentloop.ProfileFor(8192)
docs, _ := projectctx.Loader{InlineBytes: p.ProjectInlineBytes}.Load(cwd)
cfg := agentloop.Config{
LLM: client, Sessions: sessions, Steps: steps,
SandboxBuilder: &agentloop.DefaultSandboxBuilder{
Capabilities: caps,
MaxLogBytes: p.MaxLogBytes,
},
Compactor: &agentloop.SummarizingCompactor{LLM: client},
}
p.Apply(&cfg)
Apply overwrites rather than merges: it is the profile's settings that end up in cfg, not a blend. Call it BEFORE any hand-tuning you want to survive. A zero Profile (from ProfileFor(0)) writes nothing.
type RunEvent ¶
type RunEvent struct {
Type string `json:"type"`
Content string `json:"content,omitempty"`
Tool string `json:"tool,omitempty"`
Args map[string]any `json:"args,omitempty"`
Summary *RunSummary `json:"summary,omitempty"`
Tokens *CallTokens `json:"tokens,omitempty"`
}
RunEvent is one observability emission the loop streams to RunRequest.OnEvent in real time.
The Type discriminator names what fields are populated:
user user turn persisted; Content = message
execute_js agent emitted a JS block; Content = the JS source
execute_js_result a JS block finished; Content = textual result
sandbox_event a primitive emitted observability; Args carries
the underlying sandbox.Event fields
data_update the agent's carried data changed; Args = new value
compacted history was summarized before the run's turns;
Args = {"messages_before", "messages_after"}
budget_trimmed a turn's prompt exceeded Config.ContextBudget and
the oldest turns were dropped to fit; Args =
{"messages_dropped", "estimated_tokens",
"allowance"}
response final markdown answer; Content = the answer
response_chunk streamed token from a final-text turn;
Content = the chunk (no Args)
warning non-fatal degradation; Content = human-readable detail
error a step errored; Content = human-readable error
done terminal event; Summary = aggregate RunSummary
Tokens is populated on `response` and `execute_js_result` events to attribute LLM cost back to the step that incurred it.
type RunRequest ¶
type RunRequest struct {
// SessionID identifies the agent session this run extends. The loop
// loads prior steps from StepStore using this ID; new steps are
// appended under the same ID.
SessionID string
// Scope is the tenant boundary the run executes under. Passed
// through to the PolicyChecker and each Capability's Build.
Scope Scope
// UserID is the invoking user, empty for system-initiated runs.
UserID string
// Message is the user turn that triggered the run. The loop appends
// it to history before the first LLM call.
Message string
// Context is optional per-run context (e.g. a webhook payload,
// prefetched) folded into the system prompt for this run only. Not
// persisted as a step — the user turn in the trace stays the raw
// Message.
Context string
// OnEvent receives every observability emission as it happens. Nil
// is acceptable — events still land in the step trace.
OnEvent func(RunEvent)
}
RunRequest is the input to Loop.Run.
type RunResult ¶
type RunResult struct {
// RunID is the session ID this run extended (mirrors RunRequest.SessionID).
RunID string
// FinalText is the agent's last `response` step content. Empty when
// Status != "completed".
FinalText string
// Steps is the number of steps persisted by this Run call.
Steps int
// Status is one of "completed" | "error" | "max_iterations".
Status string
// Tokens is the aggregate prompt + completion token usage across
// every LLM call this Run made.
Tokens TokenUsage
// SystemPrompt is the fully composed system prompt sent to the
// model, for an inspectable turn trace. Empty if the run failed
// before composing it.
SystemPrompt string
// DataBytesCarried is the context-economy measurement: bytes of
// threaded working state withheld from prompts, summed per LLM
// call, net of the shape digests sent.
DataBytesCarried int64
}
RunResult is the summary populated when Loop.Run returns.
type RunStep ¶
type RunStep struct {
SessionID string
StepIndex int32
StepType string
Content string
ToolArgs json.RawMessage
DurationMs int32
PromptTokens int32
CompletionTokens int32
CreatedAt time.Time
}
RunStep is one persisted row in the session's trace. StepType is the discriminator: user, execute_js, execute_js_result, response, error, summary. rehydrateHistory (history.go) only replays user / response / execute_js / execute_js_result back to the LLM — error rows stay in the trace but don't feed back.
A StepTypeSummary row is a compaction checkpoint rather than a turn: its Content is replayed in place of the steps it covers, and its ToolArgs holds a CompactionCheckpoint naming where retained history resumes. Store implementations need do nothing special for it — it is an ordinary append — but they must preserve ToolArgs verbatim, since losing that marker turns the checkpoint into an unusable row.
type RunSummary ¶
type RunSummary struct {
SessionID string `json:"session_id"`
Steps int `json:"steps"`
Tokens struct {
Prompt int32 `json:"prompt"`
Completion int32 `json:"completion"`
} `json:"tokens"`
// DataBytesCarried is the run's context-economy measurement: bytes
// of threaded working state withheld from prompts, summed per LLM
// call, net of the shape digests sent.
DataBytesCarried int64 `json:"data_bytes_carried,omitempty"`
}
RunSummary rides the terminal "done" event — the same numbers RunResult carries, for a caller that only subscribes to the event stream.
type SandboxBuilder ¶
type SandboxBuilder interface {
// Build returns the sandbox + a cleanup func the loop defers.
Build(ctx context.Context, sess Session, scope Scope, onEvent sandbox.OnEvent) (*sandbox.Sandbox, func(), error)
}
SandboxBuilder produces the sandbox for one Run. The loop calls it once per Run with the per-run scope so capabilities can resolve scoped state.
type Scope ¶
Scope is the tenant boundary a run executes under. Every capability's Build receives it and should scope its reads/writes accordingly. ProjectID is optional — leave it empty when your application has no sub-workspace narrowing.
type Session ¶
type Session struct {
ID string
Model string // optional; loop falls back to Config.Model when empty
SystemPrompt string // persona section appended to the platform prompt
Data json.RawMessage
// MessageID is the inbound message that started this session, if any
// — set once at session creation and read back here so a
// SandboxBuilder can hand it to capabilities that need it. Empty for
// a session with no originating message.
MessageID string
}
Session is the minimum the loop needs to drive a Run.
type SessionStore ¶
type SessionStore interface {
// Get returns the session for sessionID. An UNKNOWN id is NOT an
// error — implementations may auto-create a fresh session shell,
// because the loop treats "no session yet" as normal.
Get(ctx context.Context, sessionID string) (Session, error)
// Exists reports whether a session row already exists, WITHOUT
// creating one.
Exists(ctx context.Context, sessionID string) (bool, error)
UpdateData(ctx context.Context, sessionID string, snapshot json.RawMessage) error
Finalize(ctx context.Context, sessionID string, summary FinalizeSummary) error
}
SessionStore exposes the per-session metadata the loop reads and the lifecycle hooks it writes.
type StepStore ¶
type StepStore interface {
Append(ctx context.Context, step RunStep) error
LastN(ctx context.Context, sessionID string, n int) ([]RunStep, error)
}
StepStore persists the per-turn trace of a session.
LastN's ordering is load-bearing: it must return the most recent n steps in CHRONOLOGICAL order (oldest first) — the loop replays this straight into the LLM context window.
type SummarizingCompactor ¶ added in v0.4.0
type SummarizingCompactor struct {
// LLM performs the summarization. Required; a nil client makes
// Compact a no-op that returns the history unchanged, so a missing
// provider degrades to today's truncation rather than failing runs.
//
// Pointing this at a small, cheap model is usually right — the work
// is extractive, and it keeps the per-Run cost noted above low.
LLM llm.Client
// Model overrides the client's default for the summarization call.
Model string
// Trigger is the history length, in messages, past which compaction
// runs at all. Zero uses the package default (60).
Trigger int
// Keep is how many of the most recent messages stay verbatim. Zero
// uses the package default (20). Clamped to half of Trigger when it
// would otherwise leave nothing to summarize.
Keep int
// Prompt overrides DefaultCompactPrompt.
Prompt string
// MergePrompt overrides DefaultMergePrompt, the instruction used
// when partial summaries have to be folded together. Unused when
// the transcript fits a single call.
MergePrompt string
// Budget bounds ONE summarization call against the window of the
// model doing the summarizing — which is not the loop's model, and
// is often deliberately smaller and cheaper. Its MaxTokens is that
// window and its Reserve the room left for the summary itself.
//
// Zero uses an internal default sized to the bound this replaced,
// which suits a hosted summarizer. Set it when the summarizer is
// locally hosted: it is the difference between chunking to fit and
// sending one call the server will reject.
Budget ContextBudget
}
SummarizingCompactor folds the older part of a long history into one summary message, keeping the most recent turns verbatim.
A session long enough to need compaction can be long enough to overflow the context of the call meant to fix that, so the transcript is summarized in CHUNKS that each fit one call and the partial summaries are then folded together — repeatedly, until one remains. Nothing is dropped to make the transcript fit: a stretch of the session that a single-call summarizer would have elided is summarized like any other. The only surviving elision is for one individual message too large for a whole call on its own.
Budget is what sizes a chunk. Left unset it defaults to roughly the old single-call bound, which is the right answer against a hosted model; point it at a small local model's window and the same transcript is simply summarized in more, smaller pieces.
Cost: a summarization happens when the rehydrated history passes Trigger. Because the loop persists each result as a checkpoint (see CompactResult.Summary), that is once per Trigger-worth of NEW conversation rather than once per Run — a session compacted at turn 60 does not pay again until it has grown back past Trigger. Trigger is the knob: raise it to pay less often and send more verbatim history, lower it for the reverse. Chunking multiplies the calls one compaction makes, so a small Budget against a large Trigger is the combination that costs most.
func (*SummarizingCompactor) Compact ¶ added in v0.4.0
func (c *SummarizingCompactor) Compact(ctx context.Context, history []llm.Message) (CompactResult, error)
Compact implements Compactor.
type TokenEstimator ¶ added in v0.4.0
TokenEstimator reports how many tokens a string occupies.
type TokenUsage ¶
TokenUsage is the per-Run aggregate.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package agentloopmem provides in-memory implementations of agentloop.StepStore and agentloop.SessionStore.
|
Package agentloopmem provides in-memory implementations of agentloop.StepStore and agentloop.SessionStore. |
|
Package agentloopsql is a SQLite-backed agentloop.SessionStore and agentloop.StepStore: the durable counterpart to agentloopmem, for a CLI or a single-host service that wants sessions to outlive the process.
|
Package agentloopsql is a SQLite-backed agentloop.SessionStore and agentloop.StepStore: the durable counterpart to agentloopmem, for a CLI or a single-host service that wants sessions to outlive the process. |
|
Package agentlooptest provides reusable conformance harnesses for the agentloop store contracts.
|
Package agentlooptest provides reusable conformance harnesses for the agentloop store contracts. |
|
Package browser gives an agentloop sandbox a `browser` global that drives a real browser: navigate, click, type, read text, and — via Set-of-Marks — enumerate the page's interactive elements as numbered ids the model can act on without ever producing a CSS selector or a pixel coordinate.
|
Package browser gives an agentloop sandbox a `browser` global that drives a real browser: navigate, click, type, read text, and — via Set-of-Marks — enumerate the page's interactive elements as numbered ids the model can act on without ever producing a CSS selector or a pixel coordinate. |
|
chrome
module
|
|
|
cmd
|
|
|
agentloop
command
Command agentloop drives an agentloop.Loop from the terminal.
|
Command agentloop drives an agentloop.Loop from the terminal. |
|
Package eval is a small LLM-judged eval harness: a Suite of Cases (input + judge criteria), each dispatched through an agentloop.Loop and scored by a judge llm.Client on a 0-10 scale.
|
Package eval is a small LLM-judged eval harness: a Suite of Cases (input + judge criteria), each dispatched through an agentloop.Loop and scored by a judge llm.Client on a 0-10 scale. |
|
Package evalmem provides an in-memory eval.Store — for local dev, CI, and one-shot eval runs.
|
Package evalmem provides an in-memory eval.Store — for local dev, CI, and one-shot eval runs. |
|
examples
|
|
|
cli
command
Command cli is a minimal, runnable demo of agentloop: in-memory session/step stores, the default capability bundle (require, http, fetch, markdown, ai), and one Run call against an OpenAI-wire-compatible LLM.
|
Command cli is a minimal, runnable demo of agentloop: in-memory session/step stores, the default capability bundle (require, http, fetch, markdown, ai), and one Run call against an OpenAI-wire-compatible LLM. |
|
Package ext holds optional sandbox.Pack implementations that are generically useful but don't belong in the core sandbox package.
|
Package ext holds optional sandbox.Pack implementations that are generically useful but don't belong in the core sandbox package. |
|
Package llm is the LLM client interface agentloop's ai()/aiJSON() sandbox capability calls against, and what drives the agent loop's own turn-taking.
|
Package llm is the LLM client interface agentloop's ai()/aiJSON() sandbox capability calls against, and what drives the agent loop's own turn-taking. |
|
Package pool provides SandboxPool, an agentloop.SandboxBuilder that reuses one long-lived *sandbox.Sandbox per session across every Run, instead of paying goja.New() + pack-Register cost (which for some packs includes compiling JS module wrappers — e.g.
|
Package pool provides SandboxPool, an agentloop.SandboxBuilder that reuses one long-lived *sandbox.Sandbox per session across every Run, instead of paying goja.New() + pack-Register cost (which for some packs includes compiling JS module wrappers — e.g. |
|
Package projectctx discovers project-level instruction files — AGENTS.md and friends — from a checkout on disk, and renders them into a section an application can fold into an agentloop run's system prompt.
|
Package projectctx discovers project-level instruction files — AGENTS.md and friends — from a checkout on disk, and renders them into a section an application can fold into an agentloop run's system prompt. |
|
Package redact strips known secret values from text before it leaves the runtime.
|
Package redact strips known secret values from text before it leaves the runtime. |
|
Package sandbox is agentloop's JS executor — a goja sandbox the model writes run(args) → return turns against, one capability per registered Pack.
|
Package sandbox is agentloop's JS executor — a goja sandbox the model writes run(args) → return turns against, one capability per registered Pack. |
|
Package skills discovers SKILL.md files in a project and exposes each one to an agentloop run as a retrievable document.
|
Package skills discovers SKILL.md files in a project and exposes each one to an agentloop run as a retrievable document. |