executor

package
v1.0.0-alpha.12 Latest Latest
Warning

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

Go to latest
Published: May 9, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package agent defines the lifecycle contract for a running agent and the shared status tracker used by concrete backends.

Index

Constants

This section is empty.

Variables

View Source
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).

View Source
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

func AppendUserEnv(base []string, userEnvs []*spec.Env) ([]string, []string)

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>

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

	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 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

type Mount struct {
	Host        string
	Target      string
	Description string
}

Mount is a host-path → in-agent binding declared by the user via `otters run -v HOST:TARGET[:DESC]`. 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.

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

type PromptEvent struct {
	Type    string
	Step    int32
	Tool    string
	Content string
}

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
}

PromptRequest carries a chat request addressed at a specific session. Empty SessionID lets the runtime pick/create a default session.

type Prompter

type Prompter interface {
	Prompt(ctx context.Context, req PromptRequest, w io.Writer) error
}

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)

	// 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
}

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 SessionMessage

type SessionMessage struct {
	Role      string
	Content   string
	CreatedAt time.Time
}

SessionMessage is one stored turn from an agent's memory store. Role is conventionally "user" or "assistant".

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.

const (
	StatusCreated Status = iota
	StatusRunning
	StatusStopped
	StatusRemoving
	StatusRemoved
	StatusInitError
	StatusPullError
	StatusModelError
)

func WaitForStatus

func WaitForStatus(ctx context.Context, o StatusObserver, target ...Status) (Status, error)

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.

func (Status) String

func (s Status) String() string

String returns the lowercase name of the Status — used for logs and the `otters ps` STATUS column. Unknown numeric values render as "unknown" rather than panicking.

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) Get

func (t *StatusTracker) Get() Status

Get returns the current status.

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/`,
	// `workspace/` live. OTTERS_AGENT_ROOT in the spawned env
	// points here.
	Root 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.

Directories

Path Synopsis
api
v1
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.

Jump to

Keyboard shortcuts

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