Architecture
Eventide takes a LangChain DeepAgents agent — normally an in-process Python library — and runs its subagent hierarchy as distributed Kubernetes workloads. Sync subagents become Pods that run in real parallel across nodes; async subagents become managed Agent Protocol servers; dynamic fan-out becomes Job batches. The whole tree is observable as a custom resource and survives pod crashes via a Postgres checkpoint store.
The big picture
Section titled “The big picture”Two halves talk to each other only through the Kubernetes API — there is no Go↔Python IPC. Python writes Subagent custom resources; a Go controller reads them and reconciles them into workloads.
flowchart TB
A["agent.py\n(DeepAgents Python)"] --> B["eventide deploy CLI\n(AST introspection)"]
B -->|"kubectl apply"| C["Subagent CRs"]
C --> E["Go Controller\n(controller-runtime)"]
E -->|"sync: Pod"| F["Pod: sync subagent"]
E -->|"async: Deployment"| G["Agent Protocol Server"]
E -->|"dynamic: Job"| H["Job: dynamic fan-out"]
F --> K[("Postgres\ncheckpoint store")]
G --> K
H --> K
| Piece | Lives in | Role |
|---|---|---|
| Go controller | operator/ |
Reads Subagent CRs, creates Pods / Deployments / Jobs, mirrors their state back. |
| Python SDK | sdk/python/ |
The eventide deploy CLI + the middleware that intercepts subagent calls. |
| Postgres | Helm chart | Checkpoint store for crash recovery. |
The Subagent CRD
Section titled “The Subagent CRD”Every DeepAgents subagent becomes a Subagent custom resource in the
eventide.eventide.ai/v1alpha1 group. Each CR carries:
type— how it runs:sync,async, ordynamic.parentRef— its parent in the tree. The controller turns this into a KubernetesownerReference, so deleting a parent garbage-collects its children automatically.budget— token and wall-time limits the controller enforces.status— what the controller and runtime write back:phase,podName,serverUrl,result, andcost.
This CR is the single contract between Go and Python. Python creates it; Go reconciles it.
Three ways to run a subagent
Section titled “Three ways to run a subagent”A subagent’s type decides which Kubernetes workload the controller creates.
flowchart LR
subgraph Sync["sync"]
S1["task tool call"] --> S2["separate Pod"]
end
subgraph Async["async"]
A1["graph_id"] --> A2["Deployment + Service\n(Agent Protocol server)"]
end
subgraph Dynamic["dynamic"]
D1["count=N"] --> D2["N Jobs\n(fan-out batch)"]
end
Sync — a Pod per call. The middleware intercepts DeepAgents’ task tool via
wrap_tool_call, creates a Subagent CR, and blocks until the controller has run
a fresh Pod and written status.result. Each sync subagent is a separate Pod,
so multiple subagents run in genuine parallel across nodes — no Python GIL
contention.
Async — a managed server. The controller deploys a Deployment + Service
running an Agent Protocol server for the subagent’s graph, then writes the URL to
status.serverUrl. The middleware injects that URL into the async client, and
the subagent keeps running independently of the parent Pod’s lifecycle.
Dynamic — a Job batch. A count=N fan-out creates N Subagent CRs of type
dynamic, each materialized as a Job (capped at 32). The middleware polls them
to completion and aggregates results.
The reconcile loop
Section titled “The reconcile loop”The Go controller (SubagentReconciler, built on controller-runtime) runs one
idempotent loop per Subagent CR. It never assumes a workload exists — it
observes current state and nudges toward the desired state.
flowchart TB
Start(["Subagent CR changes"]) --> Final{"finalizer /\nowner set?"}
Final -->|"no"| SetMeta["set finalizer +\nparent ownerReference"] --> Requeue
Final -->|"yes"| Disp{"spec.type?"}
Disp -->|sync| Sync["ensure Pod"]
Disp -->|async| Async["ensure Deployment + Service"]
Disp -->|dynamic| Dyn["ensure Job"]
Sync --> Status["mirror phase → status\npreserve result + cost"]
Async --> Status
Dyn --> Status
Status --> Done{"terminal?"}
Done -->|"Pending / Running"| Requeue(["requeue"])
Done -->|"Succeeded / Failed"| Stop(["done"])
Workload state flows back through status: the runtime Pod patches its own
result and token usage; the controller adds phase, podName, serverUrl,
wall-time, and canonical conditions. While a workload is still Pending or
Running, the controller requeues and checks again.
Durability: crash and resume
Section titled “Durability: crash and resume”Agent state is externalized to Postgres, not held in a Pod’s memory. A
runtime component implements LangGraph’s Checkpointer interface against
Postgres.
flowchart LR
Pod1["Pod (running)"] -->|"checkpoint\nperiodically"| PG[("Postgres")]
Crash["Pod killed"] --> Pod2["new Pod\n(rescheduled)"]
PG -->|"rehydrate from\nlast checkpoint"| Pod2
Kill a Pod mid-task and the controller reschedules it. On restart the agent rehydrates from its last checkpoint and continues — completed subagent results are preserved, so no progress is lost.
Deploy: AST, not dry-run
Section titled “Deploy: AST, not dry-run”eventide deploy agent.py reads the agent file by parsing its syntax tree.
It never executes the agent, so there’s no need for a dry-run and no side
effects at deploy time. The CLI extracts each subagent’s name, prompt, model,
and tools into Subagent CRs. Raw Python Callable tools can’t be serialized to a
CRD, so it records tool names and warns instead.
What you observe
Section titled “What you observe”The result is a live, addressable subagent tree:
flowchart TD
Root["agent (root)"] --> R["researcher · sync · Pod"]
Root --> A["analyst · sync · Pod"]
Root --> L["long-runner · async · Agent Protocol server"]
L --> D1["dynamic job #1"]
L --> D2["dynamic job #2"]
kubectl get subagents shows every node with its type, phase, backing Pod, and
cost — the in-memory subagent graph, materialized as Kubernetes objects you can
inspect, debug, and trust to survive failure.