Documentation
¶
Overview ¶
Package agent defines the lifecycle contract for a running agent and the shared status tracker used by concrete backends.
Index ¶
- Variables
- func AppendUserEnv(base []string, userEnvs []*spec.Env) ([]string, []string)
- func BuildLockedEnv(opts EnvOptions) []string
- func DeleteSessionRPC(ctx context.Context, conn *grpc.ClientConn, sessionID string) error
- type Agent
- type EnvOptions
- type ExecResult
- type ImageInfo
- type Mount
- type ObjectPromptRequest
- type ObjectPrompter
- type PromptEvent
- type PromptRequest
- type Prompter
- type Provenance
- type Provider
- type Registry
- type ResolvedConfig
- type ResolvedTool
- type Runtime
- type SessionDeleter
- type SessionInfo
- type SessionLister
- type SessionMessage
- type SessionReader
- type Status
- type StatusObserver
- type StatusTracker
- type StreamPrompter
- type WorkspaceView
Constants ¶
This section is empty.
Variables ¶
var ErrNotImplemented = errors.New("not implemented")
ErrNotImplemented is returned by Registry methods that aren't yet supported on a given backend. Used by the Docker registry stub during the Docker-executor rollout (BuildTarget returns nil, other write methods that depend on the containerd snapshotter being in place may return this until step 5 lands).
var ErrRefNotFound = errors.New("ref not found")
ErrRefNotFound is returned by Registry.Resolve / Registry.Inspect when the requested ref is not known to the backend. Use errors.Is(err, executor.ErrRefNotFound) to test for it; concrete implementations may wrap it with backend-specific context.
Functions ¶
func AppendUserEnv ¶
AppendUserEnv appends user-declared envs onto a base built by BuildLockedEnv (and any provider-cred entries the caller has already added). Entries are appended in declaration order; later duplicates win, matching os/exec's "last one wins" semantics.
Reserved keys (the locked-env keys, plus *_API_KEY / *_API_BASE suffixes used for provider creds) are filtered defensively. The returned skipped slice carries the offending keys so the caller can log them once. spec.Validate rejects these at build time; runtime filtering here is a safety net for malformed agent.yaml.
func BuildLockedEnv ¶
func BuildLockedEnv(opts EnvOptions) []string
BuildLockedEnv returns the curated environment the runtime should see. The runtime — and every tool subprocess descended from it — inherits this set; nothing from the host's $PATH / $HOME / $SSH_AUTH_SOCK / $AWS_* leaks through. Callers append per-provider credential entries (<PROVIDER>_API_KEY / <PROVIDER>_API_BASE) on top of this base.
Keys produced:
- PATH — strings.Join(BinDirs, ":")
- HOME — <AgentRoot>/home
- XDG_CONFIG_HOME — <AgentRoot>/home/.config
- XDG_CACHE_HOME — <AgentRoot>/home/.cache
- XDG_DATA_HOME — <AgentRoot>/home/.local/share
- TMPDIR — <AgentRoot>/tmp
- LANG — C.UTF-8
- OTTERS_AGENT_ROOT — <AgentRoot>
func DeleteSessionRPC ¶
DeleteSessionRPC drops sessionID from the runtime's session store. Idempotent on the runtime side; success is reported even when the session was already gone.
Types ¶
type Agent ¶
type Agent interface {
UUID() uuid.UUID
Runtime() *Runtime
Prepare(ctx context.Context) error
Run(ctx context.Context) error
Start(ctx context.Context) error
Stop(ctx context.Context) error
Remove(ctx context.Context) error
// Exec runs a BIN command in this agent's spawn env: same image,
// same BIN namespace on PATH, agent workspace as cwd. Implementations
// MUST cleanly terminate the underlying execution when ctx is
// cancelled — no orphaned processes / no zombie containers.
// Returns when the underlying execution exits or is killed.
//
// `bin` is a name (e.g. "sh", "jq") resolved via PATH inside the
// spawn env, NOT a host filesystem path. Unknown names surface as
// ExecResult.Err so the caller can distinguish "BIN not declared in
// the agent's image" from "BIN ran and exited non-zero."
Exec(ctx context.Context, bin string, args []string, stdin string) ExecResult
StatusObserver
}
Agent defines the lifecycle contract for a running agent.
Prepare materializes the workspace synchronously without starting the runtime process. It's idempotent and safe to call concurrently; repeat calls return the same error. Callers who want init errors to surface before any Run goroutine is spawned should call Prepare first.
Run starts the runtime subprocess, blocking until it exits or ctx is cancelled. Run calls Prepare internally if the workspace has not yet been materialized; initialization errors are returned directly and also surfaced via Status (StatusInitError / StatusPullError / StatusModelError).
Start re-runs a previously-stopped agent on the already-materialized workspace. Blocks until the subprocess exits or ctx is cancelled, same contract as Run. Returns an error if the agent is already running or has been removed. Reuses the same workspace and loopback address.
Stop signals the running agent to exit and blocks until Run has returned or ctx is cancelled. Calling Stop on a non-running agent is a no-op.
Remove deletes the agent's on-disk state. Callers must Stop first and wait for Run to return before Remove; Remove transitions Status through StatusRemoving → StatusRemoved on success.
type EnvOptions ¶
type EnvOptions struct {
// AgentRoot is the path the agent considers its root — the
// directory under which HOME, TMPDIR, etc. live. For system
// it's the host-side materialised tree path; for docker it's
// the container-internal "/workspace".
AgentRoot string
// BinDirs are joined with ":" to form $PATH. For system
// there is one entry (<AgentRoot>/usr/bin); for docker there
// is one entry per BIN image mount.
BinDirs []string
}
EnvOptions configure the locked-down environment that Executor implementations hand to spawned runtimes (and through them, every tool subprocess).
The shape is intentionally explicit so backends with different in-agent path layouts can still call one shared builder:
- system executor: AgentRoot is the host path of the chroot-style materialised tree; BinDirs is <AgentRoot>/usr/bin (a single entry).
- docker executor: AgentRoot is the container-side path ("/workspace"); BinDirs lists the per-BIN image-mount directories ("/opt/bins/ping", "/opt/bins/jq", …).
type ExecResult ¶
type ExecResult struct {
Stdout, Stderr string
ExitCode int
// Err is set for spawn failures (BIN not in PATH inside the
// sandbox, container gone, sandbox unavailable) and for
// ctx-cancellation (ctx.Err() flows through). Nil when the BIN
// ran to completion, regardless of its exit code.
Err error
// Handle is a backend-specific identifier the daemon uses for
// boot-time ghost cleanup: a PID for the system backend, a
// container ID for docker. Empty when the spawn never happened.
Handle string
}
ExecResult is the captured outcome of one BIN invocation via Agent.Exec. Stdout / Stderr are best-effort: if cancellation truncates the run, what was captured up to that point is returned alongside ctx.Err() in Err.
type ImageInfo ¶
type ImageInfo struct {
Ref string
Digest string
MediaType string // OCI artifactType from the manifest, or the Docker config media type
Size int64
CreatedUnix int64
Description string
Source string
// Labels are OCI image-config labels (Dockerfile LABEL
// directives or oras-side equivalents). Populated when the
// backend can read them cheaply; may be empty.
Labels map[string]string
// Annotations are manifest-level annotations. Same population
// caveat.
Annotations map[string]string
}
ImageInfo is the metadata an Executor's Registry exposes for a stored ref. Mirrors the cross-cutting fields the openotters daemon surfaces via its `inspect` / `describe` RPCs.
Created is unix seconds (0 = unknown) so both backends can return a single comparable value: oras-served registries pull it from the on-disk manifest's mtime; the Docker SDK parses the RFC3339 `Created` field on ImageInspect.
Description / Source are surfaced as their own fields (rather than via Labels/Annotations alone) because every consumer the daemon talks to wants those two specifically — extracting them once at the Registry layer means each call site doesn't have to re-implement the OCI standard-key fallback logic.
type Mount ¶
Mount is a host-path → in-agent binding declared by the user via `otters run -v HOST:TARGET[:DESC][:ro|:rw]`. Both the system executor (which realises mounts as symlinks inside the chrooted agent root) and the Docker executor (which realises them as bind-mounts inside the container) use this type.
Host must be an absolute host path. Target is the path the agent sees — system maps it under the chroot root, Docker maps it as the container-side bind target. Description is optional and surfaces to the LLM via the generated MOUNTS.md context layer.
ReadOnly maps to docker's bind ReadOnly flag on the docker executor; the system executor doesn't enforce it (no sandbox), but surfaces it in MOUNTS.md so the model knows the user's intent.
type ObjectPromptRequest ¶
type ObjectPromptRequest struct {
Prompt string
Schema []byte
SchemaName string
SchemaDescription string
}
ObjectPromptRequest carries a one-shot structured-output request: a user prompt plus a JSON Schema describing the response shape. No session id — structured generation is stateless and bypasses the agent's tool loop. SchemaName / SchemaDescription surface in tool-mode providers as the synthetic tool's name / description; they're optional.
type ObjectPrompter ¶
type ObjectPrompter interface {
PromptObject(ctx context.Context, req ObjectPromptRequest) ([]byte, error)
}
ObjectPrompter generates a JSON object that conforms to the supplied schema. The returned bytes are valid JSON suitable for piping into jq or for json.Unmarshal. Rare providers may produce invalid JSON even with repair; callers should treat a returned error as "no usable object" rather than checking bytes shape.
type PromptEvent ¶
PromptEvent is a typed chat event yielded by StreamPrompter. Mirrors the runtime's gRPC ChatStreamEvent so callers can render progress (steps, tool calls, streaming text) without merging everything into one blob.
Known Type values: "step.start", "step.finish", "text.delta", "tool.call", "tool.result", "message.create", "error".
type PromptRequest ¶
type PromptRequest struct {
SessionID string
Prompt string
// Regenerate signals the runtime to attach the produced parts
// as a new branch onto the most recent assistant turn for
// SessionID instead of inserting a fresh row.
Regenerate bool
}
PromptRequest carries a chat request addressed at a specific session. Empty SessionID lets the runtime pick/create a default session.
type Prompter ¶
Prompter returns the full assistant response as a single blob, discarding intermediate events. Use for simple unary request/response callers.
type Provenance ¶
type Provenance struct {
ImageDigest string `yaml:"image_digest,omitempty" json:"image_digest,omitempty"`
RuntimeRef string `yaml:"runtime_ref,omitempty" json:"runtime_ref,omitempty"`
RuntimeDigest string `yaml:"runtime_digest,omitempty" json:"runtime_digest,omitempty"`
}
Provenance records the OCI digests of the artifacts that materialise an agent: the agent image itself, the runtime image it executes against, and the BIN tool images per ResolvedTool. Optional — populated only when the workspace is given a digest resolver (today: the openotters daemon's local registry resolver). Lets a host operator answer "exactly which bytes is this agent running?" without re-querying any registry.
type Provider ¶
type Provider interface {
// Create materializes and prepares an agent with the given ID, returning it ready to Run.
Create(ctx context.Context, id uuid.UUID, ref spec.Reference, opts ...spec.Override) (Agent, error)
// Load recovers previously created agents from the backend.
Load(ctx context.Context) ([]Agent, error)
// Destroy removes all agents and associated artifacts.
Destroy(ctx context.Context) error
// Registry returns the OCI artifact storage backend this
// Provider is bound to. Never nil; backends that don't yet
// support every Registry method return ErrNotImplemented.
Registry() Registry
}
Provider manages agent lifecycle on a specific backend (system, docker, etc.).
Each Provider also exposes the storage backend agents are stored in via Registry(). The system Provider's Registry wraps an embedded oras-go store; the Docker Provider's Registry wraps the Docker daemon's image store. Daemon image-management RPCs (List / Build / Pull / Push / Remove / Inspect) route through Registry() instead of accessing storage directly so the operator's choice of executor determines where artifacts live.
type Registry ¶
type Registry interface {
// List enumerates all refs known to this registry. Order is
// implementation-defined.
List(ctx context.Context) ([]string, error)
// ListEntries returns one ImageInfo per ref in a single backend
// call. Implementations are free to populate every field from
// the bulk-list response (docker's cli.ImageList already
// includes Id, Created, Size, Labels — Inspect-per-ref would
// re-fetch the same data) so the daemon's ListImages doesn't
// have to fan out N additional Inspect calls.
//
// Returns the same ref set as List, but with metadata
// populated. ListEntries SHOULD be the path callers prefer for
// listing surfaces; List is kept for callers that only need the
// ref strings (e.g. cache-warming, cleanup).
ListEntries(ctx context.Context) ([]ImageInfo, error)
// Resolve returns the descriptor for ref, or an error if ref
// is not known. Errors are typed where possible
// (errors.Is(err, ErrRefNotFound) == true on miss).
Resolve(ctx context.Context, ref string) (ocispec.Descriptor, error)
// Inspect returns metadata for ref: digest, size, labels,
// annotations. Implementations populate as much as their
// backend exposes; missing fields are zero-valued.
Inspect(ctx context.Context, ref string) (ImageInfo, error)
// Fetch returns a stream of the content addressed by desc.
// Caller closes.
Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error)
// Tag points dst at the same descriptor as src. dst must be a
// valid registry ref; src must already exist.
Tag(ctx context.Context, src, dst string) error
// Remove deletes ref. Unreferenced content blobs are garbage
// collected by the backend on its own schedule.
Remove(ctx context.Context, ref string) error
// PullRemote fetches remoteRef from a remote registry into
// this Registry. The local ref name matches remoteRef
// verbatim (callers retag via Tag if they want a different
// local name).
PullRemote(ctx context.Context, remoteRef string) error
// PushRemote sends localRef to a remote registry as
// remoteRef. Auth comes from environment / credentials store
// per backend.
PushRemote(ctx context.Context, localRef, remoteRef string) error
// BuildTarget exposes the underlying oras.Target for the
// agentfile/build pipeline (which writes blobs + tags
// directly via oras). Returns nil when the backend doesn't
// support direct OCI writes — Docker today, until we wire
// build-via-ImageLoad. Daemon callers must check for nil and
// emit a clear "build unsupported on this executor" error.
BuildTarget() oras.Target
// ManifestKind returns the manifest's `artifactType` for ref —
// the openotters kind ("application/vnd.openotters.{agent,bin}.v1")
// when the producer stamped it, otherwise the empty string. Used
// by the daemon to populate its image_kinds index at ingestion
// time so subsequent listings don't have to re-derive the kind
// from each backend's idiosyncratic surface (docker config Labels
// for bins, manifest annotations for agents). Cheap on both
// backends: system reads the manifest blob it already has;
// docker reads the same Config.Labels[LabelArtifactType] today's
// Inspect path used to read.
ManifestKind(ctx context.Context, ref string) (string, error)
}
Registry is the storage backend an Executor exposes for agent / runtime / BIN OCI artifacts. Each Executor implements it against its native storage:
- system: an embedded oras-go store + an OCI distribution HTTP server bound to a loopback port.
- docker: the Docker daemon's image store, accessed via the moby SDK (requires the containerd snapshotter for custom OCI mediatypes — agent artifacts use application/vnd.openotters.agent.v1).
The interface is intentionally high-level. For the system executor's build pipeline (which streams blobs) BuildTarget returns the underlying oras.Target so existing build code keeps working without a wrapper. Docker returns nil from BuildTarget while build support is unimplemented; daemon image-build RPCs detect that and return a clear error.
type ResolvedConfig ¶
type ResolvedConfig struct {
Name string `yaml:"name" json:"name"`
Model string `yaml:"model" json:"model"`
Addr string `yaml:"addr,omitempty" json:"addr,omitempty"`
APIBase string `yaml:"-" json:"-"`
APIKey string `yaml:"-" json:"-"`
Exec []string `yaml:"exec,omitempty" json:"exec,omitempty"`
Provenance *Provenance `yaml:"provenance,omitempty" json:"provenance,omitempty"`
Tools []ResolvedTool `yaml:"tools,omitempty" json:"tools,omitempty"`
}
ResolvedConfig holds the merged configuration after applying spec overrides. APIBase and APIKey are intentionally non-serialised (`yaml:"-"` / `json:"-"`): they are credentials resolved from providers.yaml on every (re)start and travel to the runtime subprocess via env (<PROVIDER>_API_KEY / <PROVIDER>_API_BASE), not via disk or argv. Older agent.yaml files that contain these fields are tolerated on load — yaml/json simply skip them.
type ResolvedTool ¶
type ResolvedTool struct {
Name string `yaml:"name" json:"name"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Binary string `yaml:"binary" json:"binary"`
Ref string `yaml:"ref,omitempty" json:"ref,omitempty"`
Digest string `yaml:"digest,omitempty" json:"digest,omitempty"`
}
ResolvedTool describes a tool binary with its resolved filesystem path plus, when known, its source ref and OCI digest.
type Runtime ¶
type Runtime struct {
ID uuid.UUID `yaml:"id" json:"id"`
Source *spec.Agentfile `yaml:"source" json:"source"`
ResolvedConfig `yaml:",inline" json:",inline"`
}
Runtime is the persistable result of creating an agent. It contains the original spec (source of truth) and the resolved configuration.
func LoadRuntime ¶
func LoadRuntime(fs billy.Filesystem) (*Runtime, error)
LoadRuntime reads an Runtime from the given filesystem.
func (*Runtime) WriteTo ¶
func (rt *Runtime) WriteTo(fs billy.Filesystem) error
WriteTo serializes the runtime to the given filesystem as YAML.
Source's nested types (spec.Agentfile, spec.Agent, …) carry json+yaml tags via the spec package; the linter's musttag check only inspects the top-level concrete type and misses the transitive coverage.
type SessionDeleter ¶
SessionDeleter removes a single session from the running agent's memory store. Same opt-in pattern as SessionReader / SessionLister.
type SessionInfo ¶
SessionInfo is one entry in an agent's session log — enough to render a chat-history list (id + activity timestamp + message count) without fetching the full transcript.
func FetchSessions ¶
func FetchSessions(ctx context.Context, conn *grpc.ClientConn) ([]SessionInfo, error)
FetchSessions calls the runtime's ListSessions RPC on conn and adapts the wire response into executor.SessionInfo.
type SessionLister ¶
type SessionLister interface {
ListSessions(ctx context.Context) ([]SessionInfo, error)
}
SessionLister enumerates every session the running agent's memory store knows about. Same opt-in pattern as SessionReader; backends that can't surface this just don't implement it.
type SessionMessage ¶
type SessionMessage struct {
Role string
Content string
BranchesJSON string
ActiveBranch int
CreatedAt time.Time
}
SessionMessage is one stored turn from an agent's memory store. Role is conventionally "user" or "assistant".
Content shape:
- user: prompt text verbatim.
- assistant: JSON-encoded array of "parts" (text chunks + tool blocks). BranchesJSON, when non-empty, is a JSON array of alternative parts arrays from regeneration; ActiveBranch indexes which alternative this row's Content represents within the [Content] ++ [BranchesJSON] union.
func FetchSessionMessages ¶
func FetchSessionMessages( ctx context.Context, conn *grpc.ClientConn, sessionID string, limit int, ) ([]SessionMessage, error)
FetchSessionMessages calls the runtime's ListSessionMessages RPC on conn and adapts the wire response into executor.SessionMessage. The system and docker executors differ only in how they obtain the gRPC connection (loopback port vs. forwarded container port); the RPC shape itself is identical, so the marshalling lives here.
type SessionReader ¶
type SessionReader interface {
ListSessionMessages(ctx context.Context, sessionID string, limit int) ([]SessionMessage, error)
}
SessionReader retrieves historical messages for a session from the running agent's memory store. Optional companion to Prompter / StreamPrompter: backends that don't expose session history simply don't implement it, and callers should type-assert and handle the negative case cleanly.
type Status ¶
type Status uint8
Status represents the lifecycle state of an agent.
func WaitForStatus ¶
WaitForStatus blocks until the observer reaches one of target or ctx is cancelled. Returns the observed status on hit, or ctx.Err() otherwise. The current status is checked first, so callers do not race the transition they are waiting for.
type StatusObserver ¶
type StatusObserver interface {
// Status returns the current state.
Status() Status
// SubscribeStatus returns a channel of status transitions and a cancel
// function that closes the channel. Sends are non-blocking: slow
// subscribers may miss intermediate transitions; call Status() to
// resync. Always call cancel to avoid leaking the subscription.
SubscribeStatus() (<-chan Status, func())
}
StatusObserver provides status tracking.
type StatusTracker ¶
type StatusTracker struct {
// contains filtered or unexported fields
}
StatusTracker manages agent status and broadcasts transitions to subscribers.
func NewStatusTracker ¶
func NewStatusTracker() *StatusTracker
NewStatusTracker creates a new status tracker.
func (*StatusTracker) Set ¶
func (t *StatusTracker) Set(s Status)
Set updates the status and broadcasts to all subscribers. Sends are non-blocking; a slow subscriber's channel may drop values.
func (*StatusTracker) Subscribe ¶
func (t *StatusTracker) Subscribe() (<-chan Status, func())
Subscribe returns a channel of status transitions and a cancel function. Cancel closes the channel and removes the subscription. Callers must call cancel to avoid leaking the subscription; typical use is `defer cancel()`.
type StreamPrompter ¶
type StreamPrompter interface {
PromptStream(ctx context.Context, req PromptRequest, cb func(PromptEvent)) error
}
StreamPrompter delivers typed events as they arrive from the runtime's gRPC server — steps, tool calls, token deltas, and the final message.create with the rendered response. cb is invoked synchronously from the stream goroutine; slow callbacks backpressure the stream.
type WorkspaceView ¶
type WorkspaceView struct {
// Root is what the agent considers the root of its tree —
// the directory under which `etc/`, `home/`, `tmp/`, `var/`
// live (and `workspace/` when WorkspaceDir is empty).
// OTTERS_AGENT_ROOT in the spawned env points here.
Root string
// WorkspaceDir is the agent-visible path to the scratch dir.
// When empty, falls back to `<Root>/workspace`. Docker
// overrides this to `/workspace` so the user-facing CWD is
// not `/agent/workspace` but a clean top-level path; the
// host directory is bind-mounted there in addition to the
// FHS root.
WorkspaceDir string
// BinDirs are the absolute paths the runtime resolves BIN
// tools against, in the agent's view. System: a single
// `<Root>/usr/bin`. Docker: one entry per BIN, e.g.
// `/opt/bins/ping`, `/opt/bins/jq`.
BinDirs []string
// RuntimeBin is the absolute path of the runtime binary in
// the agent's view. System: `<Root>/usr/local/bin/runtime`.
// Docker: `/opt/runtime/runtime`.
RuntimeBin string
// Isolated reports whether a real sandbox boundary separates
// the host from the agent. Docker: true. System: false.
// Affects WORKSPACE.md phrasing — isolated agents are told
// "you're in a container; everything below is the in-container
// path"; non-isolated ones are told "no chroot, real host
// paths in tool calls".
Isolated bool
}
WorkspaceView captures the agent-visible filesystem layout — the paths the runtime feeds into WORKSPACE.md so the LLM has an accurate mental model of where things live in *its* world.
The two backends produce different views:
System executor: host-rooted, no sandbox. Root is the real on-disk path of the agent's materialised tree (`/Users/<me>/.otters/agents/<id>`). Tool calls must use these absolute paths verbatim.
Docker executor: container-rooted at `/workspace`. The host directory is bind-mounted under `/workspace`; runtime + BINs are image-mounted under `/opt/`. The agent never sees the host path of its tree.
A zero-value WorkspaceView is treated as "use the system defaults": Root = the workspace's billy root, BinDirs = a single `<Root>/usr/bin`, RuntimeBin = `<Root>/usr/local/bin/runtime`, Isolated = false.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
api
|
|
|
Package docker implements executor.Provider on top of the Docker Engine via the moby/moby Go SDK.
|
Package docker implements executor.Provider on top of the Docker Engine via the moby/moby Go SDK. |
|
Package system implements executor.Provider using local OS processes and a chrooted billy filesystem per agent.
|
Package system implements executor.Provider using local OS processes and a chrooted billy filesystem per agent. |