Skip to main content

Gateway ↔ Agent Loop API Contract

How we define this contract

Every system with more than one component has to answer a basic question: how do they talk to each other? In this architecture, the two external boundaries — the Gateway API (how clients connect) and the Model API (how the Agent Loop connects to models) — were defined from the start. But there was one internal handoff left undefined: how does the Gateway pass a request to the Agent Loop, and how does the Agent Loop send results back?

A protocol evaluation (D136) looked at every communication gap in the architecture — 29+ potential interfaces — and found that all of them resolve through things that already exist (contracts, tools, configuration). All except this one. The Gateway needs to hand off a conversation to the Agent Loop. The Agent Loop needs to stream results back. Auth needs to sit on the path between them. That's the entire scope.

This is not a third API. The Gateway API and Model API are the swappability boundaries — external things plug into them. This is an internal contract between two components we control, inside the same deployment. It just needs to be well-defined so the components stay decoupled — you should be able to swap the Agent Loop without touching the Gateway, and vice versa (FS-5, FS-7).

The design principle: the simplest contract that fills the one gap. One endpoint. One stream format. Auth on the path. No protocol envelope, no routing, no node registration, no versioning scheme. Two components in the same deployment don't need the ceremony of an external API — they need a clear agreement about the shape of the data and what to expect back.

Related documents: foundation-spec.md (architecture overview, links to all component specs)


The Contract

One HTTP endpoint. Gateway POSTs a request, Agent Loop returns an SSE stream. Auth middleware sits on the path.

Client → Gateway API → Gateway → [Auth] → POST /engine/chat → Agent Loop → Model API → Model

Tools

Request: POST /engine/chat

{
"messages": [
{ "role": "system", "content": "Read /AGENT.md for your instructions." },
{ "role": "user", "content": "Help me plan my finances" },
{ "role": "assistant", "content": "I'll read your finance page..." },
{ "role": "user", "content": "Focus on budgeting" }
],
"metadata": {
"conversation_id": "conv_abc123",
"correlation_id": "req_xyz789",
"trigger": "message",
"client_context": { "path": "/finances" }
}
}

Messages follow the industry-standard role + content format (consistent with D16: use the prevailing standard, not a custom protocol). The Gateway packages this array from three sources:

  1. System message — from configuration (the bootstrap prompt that tells the model to read Your Memory)
  2. Conversation history — from Your Memory, via the Gateway's conversation store tool (D152)
  3. Current message — from the client request

Metadata is pass-through context. The Agent Loop can use it or ignore it.

FieldDescriptionRequired
messagesSystem prompt + conversation history + current messageYes
metadata.conversation_idWhich conversation this belongs toNo (Agent Loop doesn't need it — Gateway tracks this)
metadata.correlation_idRequest trace ID, generated by GatewayYes
metadata.trigger"message" | "webhook" | "schedule"No (default: "message")
metadata.client_contextClient-provided context (path, folder)No

What is NOT in the request — the Agent Loop is pre-configured with:

  • Tool definitions — which tool sources (MCP servers, CLI tools, native functions), which tools are always-send (D109)
  • Provider configuration — model, API key, base URL
  • Optional implementation bounds — implementations MAY pass a maxIterations safety bound. This is an optional, implementation-provided parameter with no default. The Agent Loop itself does not impose an iteration cap — it loops until the model signals completion. If an implementation provides maxIterations, that is the implementation's deployment choice, not an architectural constraint.

These are Agent Loop startup configuration, not per-request data. They don't change between requests.

Trigger Types

The Agent Loop gets the same messages array regardless of trigger source. It genuinely doesn't know the difference.

TriggerHow it entersExample message
Client messageClient → Gateway API → Gateway"Help me plan my finances"
WebhookExternal service → Gateway API → Gateway"New email received: [subject]"
ScheduleCron tool → Gateway"Run the daily digest"

All triggers flow through the Gateway. Even scheduled triggers create conversations (the daily digest produces output the owner can review).


Response: SSE Stream

The response is a Server-Sent Events stream. Always streaming — no non-streaming mode. Callers who want the complete response collect the stream.

event: text-delta
data: {"content":"Hello, I'll help with budgeting."}

event: tool-call
data: {"id":"tc_1","name":"read_file","arguments":{"path":"/finances/AGENT.md"}}

event: tool-result
data: {"id":"tc_1","output":"# Finances\nBudget goal: ..."}

event: text-delta
data: {"content":"Based on your finance page..."}

event: done
data: {"finish_reason":"stop","usage":{"prompt_tokens":150,"completion_tokens":75}}

Events

EventWhenData
text-deltaModel generates text{ content: string }
tool-callModel initiates a tool call{ id: string, name: string, arguments: object }
tool-resultTool execution completes{ id: string, output: string } or { id: string, error: string }
doneAgent loop complete{ finish_reason: string, usage: { prompt_tokens, completion_tokens } }
errorUnrecoverable failure mid-stream{ code: string, message: string }

Tool calls are visible in the stream because:

  • Clients use them for status display ("Reading file...", "Searching library...")
  • Approval gates need the client to see and approve tool calls (D104-D106)
  • Observability — the full interaction is traceable

The Agent Loop may call the model multiple times in one stream (model calls tools, Agent Loop executes them, sends results back to model, model continues). This multi-turn loop is internal to the Agent Loop — the stream captures everything that happens.


Error Handling

Pre-Stream Errors (HTTP Status Codes)

The request never started processing. No SSE stream opened.

CodeWhen
400Invalid request — missing messages, bad format
401Auth middleware rejected the request
503Agent Loop unavailable

Response body: { "code": "...", "message": "..." }

Mid-Stream Errors (SSE Error Events)

The stream started, then something failed.

CodeWhen
provider_errorModel unreachable or timeout
tool_errorTool failed and model cannot recover
context_overflowMessages exceed model context window

The Agent Loop must classify errors correctly — no catch-all error code. Recoverable tool failures (a tool returns an error result) go back to the model as tool-result events for the model to decide what to do. Only unrecoverable tool infrastructure failures emit tool_error and close the stream.

Error message safety: The message field in error events must be safe for client display. Never forward raw Error.message, stack traces, or file paths. Map each error code to a fixed safe message. Log raw diagnostic detail to structured stdout only. See Security Requirements below.

After an error event, the stream closes. The Gateway decides how to communicate the failure to the client.


Auth Middleware

Auth sits between the Gateway and the Agent Loop as HTTP middleware. It is not part of either component (D60: Auth is independent and cross-cutting).

  1. Validates the request (token, session, API key)
  2. Attaches identity to the request via headers: X-Actor-ID, X-Actor-Permissions
  3. Rejects unauthorized requests with HTTP 401

The Agent Loop receives pre-authenticated requests. It never validates identity — Auth already did. The Agent Loop can read the identity headers if tool permissions depend on the actor, but it doesn't enforce authentication.


Conventions

Agreed formats that are not contractually required — adoptable incrementally. Correlation ID is not listed here because it is a required contract field (metadata.correlation_id, see request table above) and a security requirement (end-to-end traceability).

ConventionImplementation
Error format{ code: string, message: string } — consistent across HTTP errors and SSE error events.
LoggingStructured JSON to stdout: { timestamp, correlation_id, component, level, message }. Not a protocol — a convention components follow.

What This Contract Explicitly Excludes

Per the protocol evaluation — these capabilities were considered and determined unnecessary for this interface:

ExcludedWhy
Routing / capability matchingOne Agent Loop. No discovery needed.
Node registrationComponents are fixed, not dynamic.
Intent-based routingThe Agent Loop (with the model) decides what to do.
Operating modesThe Agent Loop handles complexity internally.
Workflow orchestrationThe Agent Loop orchestrates via tools.
Protocol versioningInternal contract between two components we control. Versioning adds overhead with no benefit — we update both sides together.
Envelope format (CloudEvents, BDP)Marginal value for a single internal endpoint. The request body IS the contract.

Implementation Notes (Implementation Reference)

These are implementation notes, not architecture requirements. The contract above is the foundation. These notes show one way to implement it.

Same-Process Option

Gateway and Agent Loop may run in the same process. The endpoint can be implemented as a direct function call that follows the contract shape — the Gateway calls an Agent Loop function with the same inputs, receiving a stream back. Defining it as HTTP means the components can be separated later without changing the contract.

SSE Event Mapping

Most AI SDKs produce streaming events that map directly to this contract's SSE event types (text-delta, tool-call, tool-result, done, error). The Agent Loop is a thin wrapper — it receives the request, calls the SDK's streaming function with the messages + its configured tools + provider, and forwards the stream events.


Open Questions

  • OQ-1: Approval gates and mid-stream interaction. When the model calls the approval tool and needs owner confirmation, the stream must pause or complete. Simplest approach: the stream completes with the approval request in the response, the client sends a new message with the decision through the normal Gateway API, and the Agent Loop continues in a new stream. This handles approval at the conversation level, not the stream level. Needs validation when Dave J implements approval gates.

Success Criteria

  • The Gateway can send a request to the Agent Loop and receive a streamed response
  • Swapping the Agent Loop does not require Gateway changes — the contract shape is the only dependency
  • Swapping the Gateway does not require Agent Loop changes — the Agent Loop accepts any caller that sends the right shape
  • Auth middleware can authenticate requests without either component knowing how authentication works
  • Tool calls are visible in the stream — clients can display status and present approval gates
  • All trigger types (message, webhook, schedule) produce the same request shape — the Agent Loop genuinely cannot tell the difference
  • Pre-stream errors return HTTP status codes; mid-stream errors return SSE error events — no ambiguity about which failure mode applies
  • The contract works as both an HTTP endpoint and a direct function call (V1 same-process) without changing the shape

Security Requirements

Per-component requirements from security-spec.md. Security-spec owns the "why" (D131); this section owns the "what" for the Gateway ↔ Agent Loop interface.

  • Every request must pass through Auth middleware before reaching the Agent Loop — no bypass path
  • Credentials (tokens, API keys, session IDs) must never appear in the request body — Auth validates and attaches identity via headers (X-Actor-ID, X-Actor-Permissions)
  • The Agent Loop must treat identity headers as read-only — it can use them for tool-level permission checks but must not modify, forge, or strip them
  • Correlation IDs must flow end-to-end (Gateway → Auth → Agent Loop → tools → logs) for full request traceability
  • The SSE stream must not leak internal system paths, stack traces, or credentials in error events — error messages must be safe for client display

Decisions Made

#DecisionRationale
D136Protocol evaluation — 29+ communication gaps collapsed to one interfaceSystematic review identified that all inter-component communication concerns resolve through existing architecture (contracts, tools, configuration). Only one gap remained: how the Gateway passes requests to the Engine.
D137Gateway ↔ Engine is a plain HTTP API contractNot a third connector — an internal interface between two components we control. One endpoint, SSE streaming, auth middleware on path. No protocol envelope, no routing, no node registration. Fills the one gap identified by D136.

Changelog

DateChangeSource
2026-03-01"No users, only owners" language pass: user → ownerOwnership model alignment (Dave W + Claude)
2026-03-01Cross-doc consistency: Implementation Notes labeled "Level 2 Reference", genericized SDK references (Hono/Vercel AI SDK → generic same-process option and SSE event mapping).Cross-doc review (Dave W + Claude)
2026-03-01D152: Conversation history source clarified — "from Your Memory, via the Gateway's conversation store tool." Structural alignment with component specs: narrative opener, Success Criteria, Security Requirements.Doc consistency pass + architecture review (Dave W + Claude)
2026-02-26Initial contract created — one endpoint, SSE stream, auth middleware, conventionsDesign session (Dave W + Claude)
2026-03-17maxIterations clarified as optional implementation-provided parameter with no default. Error taxonomy: classification guidance added (no catch-all codes, recoverable vs unrecoverable tool failures). Error message safety: explicit requirement added inline (safe for client display, no raw Error.message).Drift remediation — MVP Build Review (Dave W + Dave J + Claude)

One endpoint. One stream. Auth on the path. This is the simplest contract that fills the one gap between the Gateway and the Agent Loop that the protocol evaluation identified.