Skip to content

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.

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.

Every DeepAgents subagent becomes a Subagent custom resource in the eventide.eventide.ai/v1alpha1 group. Each CR carries:

  • type — how it runs: sync, async, or dynamic.
  • parentRef — its parent in the tree. The controller turns this into a Kubernetes ownerReference, 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, and cost.

This CR is the single contract between Go and Python. Python creates it; Go reconciles it.

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 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.

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.

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.

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.