Skip to main content

Architecture overview

managed-agent-go is one codebase with explicit API and worker process roles. PostgreSQL owns public resources and events, Temporal owns in-flight orchestration, and NATS Core carries ephemeral wakeups and previews. The local Compose stack runs those roles separately; they can also be packaged in one deployment for development.

The selected stack is Temporal orchestration, PostgreSQL, NATS Core, and replaceable sandbox providers. Provider sandbox bindings are persisted in PostgreSQL; object storage is not implemented.

Design principles

The server owns history

The event log is the source of truth for a session, but two different orderings are read from it:

  • Public event history (GET .../events, list, and the live SSE stream) is the immutable receipt/commit sequence. It never reorders or hides events.
  • Model-facing conversation order is reconstructed per turn from causality, not from raw commit order. PostgreSQL tags committed output with the trigger event ID; prior processed triggers are replayed with their exact output before the current trigger. A turn never sees a later message queued while it was still running.

Before each model turn the application reconstructs that causal history and projects it into a Messages API conversation. The model endpoint performs inference; it does not own session state.

Wire and domain models are separate

internal/httpapi owns request decoding and response encoding. internal/domain models persisted resources and execution facts. Mapping is explicit in both directions so internal sequence numbers, run states, and storage details cannot leak into the compatibility wire.

Public history and execution bookkeeping are different things

Session events are the public append-only history. Temporal Workflow history, turn attempts, and tool steps are private execution facts. Keeping those models separate lets orchestration recover without leaking Temporal or retry details onto the public API.

Interfaces sit at expensive boundaries

The model client, agent runtime, and sandbox provider are interfaces because they cross process, trust, or infrastructure boundaries. Domain entities stay concrete. This keeps the code easy to follow without locking the project to one model vendor, sandbox backend, or worker topology.

Package boundaries

PackageResponsibility
cmd/managed-agentComposition root, configuration, process lifecycle
internal/httpapiHTTP routes, strict validation, DTO mapping, SSE
internal/appShared resource validation and transport-neutral use-case types
internal/controlplanePostgreSQL-backed public Session/Event use cases
internal/domainResource, event, message, tool, and run semantics
internal/pgPostgreSQL repositories, ledger, outbox, and tool journal
internal/temporalSession Workflow, Activities, worker, and relay
internal/liveNATS wakeups/previews plus PostgreSQL cursor reconciliation
internal/agentruntimeReusable model, message, and tool execution primitives
internal/modelOffline and Messages API model clients
internal/sandboxProvider registry, lifecycle contract, and local/remote adapters

The dependency direction points inward: transport and infrastructure depend on application/domain semantics, while the domain has no HTTP, SQL, model-client, or sandbox dependencies.

Durable write path

Submitting input is not “write an event, then call Temporal.” PostgreSQL commits the client events, session.status_running, Session projection, and one coalescible outbox wakeup in one transaction. A crash therefore cannot leave accepted input without a durable path to orchestration. A direct Signal-With-Start is only a latency optimization; the relay is the correctness path.

Model and tool calls happen as Temporal Activities outside SQL transactions. At turn completion, authoritative output, trigger processed_at, the final Session status, and optional attempt finalization commit together. PostgreSQL then emits a best-effort NATS wakeup; SSE subscribers read the committed rows by sequence.

Physical session deletion is a small saga: PostgreSQL first marks the row as deleting under the admission lock, the API terminates its Session Workflow, a short Temporal Workflow durably releases the provider sandbox and binding, and only then does PostgreSQL remove the projection. The binding foreign key blocks deletion from discarding the last reference to a live sandbox.

Live text deltas are the exception: they are explicitly ephemeral previews, delivered only to opted-in SSE subscribers. They are never returned by event history.

Scaling boundaries

API replicas are stateless around PostgreSQL and NATS. Temporal assigns Workflow and Activity tasks to workers; the PostgreSQL tool journal records the side-effect ambiguity boundary. Core NATS is at-most-once, so streams periodically reconcile their durable cursor and never treat a wakeup as data. Worker Versioning, remote sandbox adapters, and orphan reconciliation are still required before production rolling deployments.

Workflow changes use Temporal version markers where replay compatibility requires them. Production rolling deployments still need Worker Versioning and explicit replay coverage.

Current implementation boundaries

The strongest current risks are semantic rather than structural:

  1. Context growth is not yet bounded by a server-owned compaction policy.
  2. Sandboxes are session-scoped and durably bound to opaque provider IDs. Restart reattachment and deletion cleanup are implemented for local and Docker on the same host/daemon. Remote providers, orphan reconciliation, quotas, and eviction are not implemented.
  3. Worker Versioning, observability, authentication, large-payload offload, and production manifests remain open.

Current API support is tracked in the compatibility matrix; planned capability work is kept in the roadmap.