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
- func WithExecStreamSinks(ctx context.Context, sinks ExecStreamSinks) context.Context
- type Agent
- type Capability
- type ContextEntry
- type EnvOptions
- type ExecResult
- type ExecStreamSinks
- type FailureReason
- type ImageInfo
- type Mount
- type OCIRef
- type ObjectPromptRequest
- type ObjectPrompter
- type PromptEvent
- type PromptRequest
- type Prompter
- type Provider
- type Registry
- type ResolvedConfig
- type ResolvedTool
- type Runtime
- type RuntimeRef
- 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>
- OTTERSD_URL — <DaemonURL> (omitted when empty)
- OTTERS_AGENT_TOKEN — <AgentToken> (omitted when empty)
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.
func WithExecStreamSinks ¶
func WithExecStreamSinks(ctx context.Context, sinks ExecStreamSinks) context.Context
WithExecStreamSinks attaches stream sinks to ctx so executor implementations forward live BIN output as bytes arrive. Mainly used by the openotters async-jobs pool to push partial output into SQLite while the BIN is still running — UI / CLI observers then see growing logs instead of a blank pane until terminal status.
Sinks travel through context (rather than a new Exec method argument) so the existing Agent.Exec signature stays the same and backends that don't implement live-streaming keep compiling. Backends that DO implement it call ExecStreamSinksFrom inside their Exec method.
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
// StatusTracker exposes the underlying tracker so the daemon
// supervisor can drive transitions it owns (Ready after the
// readiness probe answers, Working ↔ Ready around in-flight RPCs,
// Failed+FailureReadinessTimeout on probe timeout, Failed+
// FailureCrashed on unexpected exit). Executors mutate their own
// status through this same tracker for the Pulling / Starting /
// Stopped / Failed transitions they own.
StatusTracker() *StatusTracker
// Probe issues a single readiness check against the running
// runtime. Returns nil when the runtime answered Ready=true.
// Returns a non-nil error when the dial fails, the call returns
// Unavailable, or ctx expires.
//
// Used by the daemon supervisor to gate the Starting → Ready
// transition. Implementations should not retry internally — the
// caller owns the backoff and the overall timeout.
Probe(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 (StatusFailed with a FailureReason of FailurePull / FailureInit / FailureModel).
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 Capability ¶
type Capability = spec.Capability
Capability aliases spec.Capability so the executor layer can use it directly without an extra conversion at API boundaries.
type ContextEntry ¶
type ContextEntry struct {
Name string `yaml:"name" json:"name"`
File string `yaml:"file" json:"file"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
}
ContextEntry is one declared context file. `Name` is the short handle the model uses with the runtime's context_show tool (e.g. "SOUL"); `File` is the absolute (agent-root) path to the materialised markdown; `Description` is a one-line summary so the model knows what the file is for without reading it.
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
// DaemonURL is the openotters daemon's TCP endpoint
// (e.g. http://host.docker.internal:5050) the runtime should
// dial back to for daemon-side capabilities (async-jobs RPCs in
// the next iteration). Optional — empty means the spawned
// runtime gets no OTTERSD_URL and any daemon client lazy-init
// no-ops gracefully.
DaemonURL string
// AgentToken is the JWT minted by the daemon at CreateAgent. The
// runtime presents it as `Authorization: Bearer …` on every
// outbound RPC to DaemonURL. Issued and revoked by the daemon
// (see internal/auth on the openotters side). Optional — empty
// means no token is exposed and outbound RPCs would fail
// Unauthenticated, but the agent process still spawns fine.
AgentToken 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 ExecStreamSinks ¶
ExecStreamSinks carries optional io.Writer destinations for live stdout / stderr forwarding while a BIN is executing. The fields are independent: either or both may be nil and the executor must tolerate that without panicking. Writers SHOULD be safe for concurrent calls from the executor's stdout / stderr demux paths.
The executor still returns the full ExecResult.Stdout / ExecResult.Stderr on completion — the sinks are an additional progress channel, not a replacement. This way callers that don't care about live output (tests, the chat path) keep working unchanged.
func ExecStreamSinksFrom ¶
func ExecStreamSinksFrom(ctx context.Context) ExecStreamSinks
ExecStreamSinksFrom returns the sinks attached via WithExecStreamSinks, or a zero value (both Writers nil) when none were set. Executors should treat nil Writers as "no live forwarding" — they're still expected to fill ExecResult.Stdout / Stderr in the returned ExecResult either way.
type FailureReason ¶
type FailureReason uint8
FailureReason narrows StatusFailed to a specific cause. The daemon surfaces it on the wire alongside the status string so dashboards / the CLI can explain why the agent is in Failed.
const ( // FailureNone is the zero value carried by non-Failed agents. FailureNone FailureReason = iota // FailurePull — the image (agent / runtime / a BIN) could not be // pulled into the local store. Network, registry-auth, or // missing-tag. FailurePull // FailureInit — workspace materialise or container create failed. // Filesystem write, mount target collision, exec format errors. FailureInit // FailureModel — provider / model resolution failed. Missing // provider config, bad API base, key rotation gone wrong. FailureModel // FailureReadinessTimeout — the runtime subprocess started but // did not answer the daemon's Ready() probe within the timeout. FailureReadinessTimeout // FailureCrashed — the subprocess / container exited unexpectedly // after having reached Ready. The daemon marks Failed+Crashed // rather than retrying, since this is usually a code or config // bug, not a transient blip. FailureCrashed )
func (FailureReason) String ¶
func (r FailureReason) String() string
String returns the lowercase name of the FailureReason. Used in the wire field (`AgentInfo.failure_reason`) and in CLI columns. FailureNone returns the empty string so callers can render it as "no failure" without a special-case branch.
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 OCIRef ¶
type OCIRef struct {
Ref string `yaml:"ref" json:"ref"`
Digest string `yaml:"digest,omitempty" json:"digest,omitempty"`
}
OCIRef pairs an image reference with its content-addressed digest. Used for the agent image and per-tool refs.
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 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 {
ID uuid.UUID `yaml:"id" json:"id"`
Name string `yaml:"name" json:"name"`
Model string `yaml:"model" json:"model"`
Workspace string `yaml:"workspace,omitempty" json:"workspace,omitempty"`
Image *OCIRef `yaml:"image,omitempty" json:"image,omitempty"`
Runtime *RuntimeRef `yaml:"runtime,omitempty" json:"runtime,omitempty"`
Configs map[string]string `yaml:"configs,omitempty" json:"configs,omitempty"`
// Capabilities enumerates the LLM-facing tool functions the
// runtime image registers (NOT per-BIN tools — those live in
// Tools). Each carries its description so the model can read
// what every tool does straight out of agent.yaml. Populated
// by the daemon at materialise time. A future Agentfile
// CAPABILITY directive will gate individual entries.
Capabilities []Capability `yaml:"capabilities,omitempty" json:"capabilities,omitempty"`
Envs []*spec.Env `yaml:"envs,omitempty" json:"envs,omitempty"`
Mounts []*spec.Mount `yaml:"mounts,omitempty" json:"mounts,omitempty"`
Context []ContextEntry `yaml:"context,omitempty" json:"context,omitempty"`
Tools []ResolvedTool `yaml:"tools,omitempty" json:"tools,omitempty"`
// Exec is the operator-supplied entrypoint override (e.g. a
// custom runtime invocation). Lives in daemon.db; never on
// disk in agent.yaml.
Exec []string `yaml:"-" json:"-"`
Addr string `yaml:"-" json:"-"`
APIBase string `yaml:"-" json:"-"`
APIKey string `yaml:"-" json:"-"`
}
ResolvedConfig is the runtime-side configuration that agent.yaml serialises: identity, OCI provenance (image + runtime as ref+digest pairs), spec-declared envs/mounts (operator values live in daemon.db and hydrate in-memory), declared context filenames, runtime tunables, and resolved tool surface. APIBase / APIKey are intentionally non-serialised — credentials travel via spawn env (<PROVIDER>_API_KEY / <PROVIDER>_API_BASE), not disk.
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"`
Usage string `yaml:"usage,omitempty" json:"usage,omitempty"`
}
ResolvedTool describes a tool binary with its resolved filesystem path plus, when known, its source ref + OCI digest and the in-workspace path to its USAGE.md.
type Runtime ¶
type Runtime struct {
Source *spec.Agentfile `yaml:"-" json:"-"`
ResolvedConfig `yaml:",inline" json:",inline"`
}
Runtime is the persistable result of creating an agent. ResolvedConfig is the slim, single-job view that agent.yaml carries on disk. Source is in-memory only — the original Agentfile lives next door at etc/Agentfile (see spec.AgentfileMediaType) and the daemon re-parses it on Restore / Start when it needs the full spec.
func LoadRuntime ¶
func LoadRuntime(fs billy.Filesystem) (*Runtime, error)
LoadRuntime reads an Runtime from the given filesystem.
type RuntimeRef ¶
type RuntimeRef struct {
Ref string `yaml:"ref" json:"ref"`
Digest string `yaml:"digest,omitempty" json:"digest,omitempty"`
Binary string `yaml:"binary,omitempty" json:"binary,omitempty"`
}
RuntimeRef is OCIRef plus the in-container path of the runtime binary. The path is what the executor uses as the container's entrypoint (e.g. `/opt/runtime/runtime` on docker) — surfacing it here lets `otters inspect` answer "where does the agent's runtime live?" without inferring from the executor backend.
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.
The state machine:
Pulling ── (cache hit) ──┐
│ │
▼ ▼
Starting ────────────► Failed (carries a FailureReason)
│ ▲
▼ │
Ready ◄──────────────────┤
│ ▲ │
▼ │ │
Working ─────────────────┘
│
▼
Stopped ── (Start) ──► Pulling
│
▼
Removing ──► Removed
Pulling and Starting are emitted by the executor; Ready and Working are owned by the daemon supervisor (probes Ready() / tracks in-flight RPCs) and pushed into the tracker via Set.
const ( // StatusPulling — image / layers being downloaded into the local // store. The slow phase when the cache is cold; a hit flips to // StatusStarting almost immediately. StatusPulling Status = iota // StatusStarting — pull is done; workspace materialise, model // resolution, and subprocess / container spawn happen here. The // runtime has not yet acknowledged a readiness probe. StatusStarting // StatusReady — runtime answered the readiness probe. Idle, // accepting RPCs. Set by the daemon supervisor, not the executor. StatusReady // StatusWorking — at least one chat turn or async-job RPC is in // flight to this agent. Tracked daemon-side over the in-flight // count and flipped back to Ready when the count drops to 0. StatusWorking // StatusStopped — user-initiated stop, or the subprocess / // container exited (cleanly or otherwise). The daemon decides // whether to retry (re-Run) or to flip to Failed+Crashed based on // the exit context. StatusStopped // StatusFailed — terminal error. The companion FailureReason on // the tracker carries the specific cause; surface it on the wire // so operators see why. StatusFailed // StatusRemoving — cleanup in progress (workspace delete, container // teardown). StatusRemoving // StatusRemoved — agent is fully gone; the row may still exist // briefly before its persisted record is deleted. StatusRemoved )
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
// FailureReason returns the cause when Status() == StatusFailed.
// Returns FailureNone for non-failed states; callers can render
// the zero value as the empty string via FailureReason.String().
FailureReason() FailureReason
// 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 its companion FailureReason) and broadcasts transitions to subscribers.
func NewStatusTracker ¶
func NewStatusTracker() *StatusTracker
NewStatusTracker creates a new status tracker. The zero value is StatusPulling (the first transition any Run() goes through), so freshly-created Agents read pulling before they emit anything.
func (*StatusTracker) Failure ¶
func (t *StatusTracker) Failure() FailureReason
Failure returns the current FailureReason. Meaningful only when Get() == StatusFailed; otherwise the zero value (FailureNone).
func (*StatusTracker) Set ¶
func (t *StatusTracker) Set(s Status)
Set updates the status and broadcasts to all subscribers. Clears any prior FailureReason — moving out of Failed is always intentional. Sends are non-blocking; a slow subscriber's channel may drop values.
func (*StatusTracker) SetFailure ¶
func (t *StatusTracker) SetFailure(reason FailureReason)
SetFailure marks the agent failed with a specific reason. Equivalent to Set(StatusFailed) but carries the cause for downstream surfaces (the daemon's wire field, the dashboard's badge tooltip, the CLI's FAILURE column).
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. |