Agents service
Agents service
Section titled “Agents service”The agents service owns the core runtime loop for every AI agent in paperboard
(the one-word marketing product name). It is the only service that touches the
agents Postgres schema. Gateway, runtime, and identity all call agents — none
of them own any part of the agent lifecycle.
Why a separate service
Section titled “Why a separate service”We put agent CRUD, session state, LLM streaming, and tool dispatch into one service because they share a tight data dependency: the session event log is the source of truth for replay, billing, and debugging, and splitting those concerns across services would require cross-schema joins or an event-bus round-trip on every token delivered to the client.
The scaling profile of SSE streaming (long-lived connections, backpressure to the LLM provider) is also different from the request/reply profile of identity or billing. A single deployment unit lets us tune concurrency limits and timeouts independently.
What the service does
Section titled “What the service does”- Stores agent definitions — name, slug, model, and system prompt — in
agents.agents. - Creates sessions scoped to an org and an agent in
agents.sessions. - Streams LLM completions from Anthropic over Server-Sent Events (SSE) through
POST /v1/sessions/{id}/prompt. - Dispatches
tool_useblocks from the LLM to the compute service via gRPC and feedstool_resultmessages back into the conversation loop. - Persists every turn as a
session_eventrow for replay and auditing. - Emits metering events into
agents.usage_events(Phase 3, PB-62) so the platform service can aggregate cost and quota data in Phase 4.
Who calls agents
Section titled “Who calls agents”| Caller | Protocol | What it asks for |
|---|---|---|
| gateway | REST/HTTP | Routes public traffic; Phase 7 only — direct in 1–6 |
| runtime | gRPC | Lifecycle management; Phase 3+ |
| identity | gRPC | JWT/API-key validation via AuthServiceClient |
| compute | gRPC | Executes tool calls inside gVisor sandboxes |
| CLI / tests | REST/HTTP | Integration testing and developer tooling |
Schema: agents
Section titled “Schema: agents”Four tables ship as of Phase 3 (migration 000003):
| Table | Purpose |
|---|---|
agents.agents | Agent definitions (name, slug, model, system prompt) |
agents.sessions | Session lifecycle (active / completed / failed) |
agents.session_events | Append-only event log per session |
agents.usage_events | Wide metering table; Phase 4 outbox consumer target |
Cross-schema foreign keys are forbidden (ADR-0003). Identity, billing, and platform reference agents rows by UUID only.
Agent lifecycle
Section titled “Agent lifecycle”flowchart LR client["Client\n(HTTP/SSE)"] api["agents\nHTTP API"] core["SessionService\n(core)"] llm{{"Anthropic API"}} compute["compute\ngRPC"] pg[("agents\nPostgres")] usage[("usage_events")]
client -->|"POST /v1/sessions/{id}/prompt"| api api --> core core -->|"Stream()"| llm llm -->|"tool_use block"| core core -->|"ExecCommand gRPC"| compute compute -->|"tool_result"| core core -->|"SSE chunks"| api api -->|"text event-stream"| client core --> pg core --> usage
classDef controlPlane fill:#10b981,stroke:#047857,color:#fff classDef dataPlane fill:#3b82f6,stroke:#1d4ed8,color:#fff classDef sandbox fill:#f97316,stroke:#c2410c,color:#fff classDef external fill:#ef4444,stroke:#b91c1c,color:#fff classDef persistence fill:#6b7280,stroke:#374151,color:#fff
class core controlPlane class api dataPlane class compute sandbox class llm external class pg,usage persistenceKey design choices
Section titled “Key design choices”Consumer-owned interfaces. The AgentReader, AgentWriter, SessionReader,
SessionWriter, LLMClient, and ComputeClientFactory interfaces are defined
in internal/core — not in the adapters that satisfy them. This prevents
circular imports and lets the store and LLM packages evolve independently.
Advisory lock id 3. The migrator holds Postgres advisory lock 3 during schema migrations. Lock ids are cluster-wide constants: identity=1, billing=2, agents=3, platform=4.
PgBouncer split. DATABASE_URL (port 6432, PgBouncer transaction pool)
goes to the server. MIGRATION_DB_URL (port 5432, direct Postgres) goes to the
migrator. Mixing the two breaks advisory locks at migration time and loses
prepared statements at server scale.
SSE timeout separate from request timeout. SSE_TIMEOUT defaults to 300 s;
REQUEST_TIMEOUT defaults to 30 s. Long LLM responses would time out under the
regular request timeout on streaming routes.
Metering Day 1. usage_events was introduced in Phase 3 so that billing
(Phase 5) has a complete event history from the first session. The outbox
consumer columns (consumed_at, consumer_id, consumer_attempts) are
schema-ready but unpopulated until the platform outbox worker ships in Phase 4.