Skip to content

Architecture

ai-agent-subsystem is built as a single dub monorepo: reusable code lives in a shared library and the executables are thin layers on top, all under packages/. It produces three binaries and one shared library.

flowchart TB
    subgraph MONO["D monorepo (dub)"]
        direction TB
        CORE[["agentcore<br/>shared library"]]
        CTRL["controller<br/>binary"]
        INIT["initializer<br/>binary"]
        SUP["supervisor<br/>binary"]
        CORE --> CTRL
        CORE --> INIT
        CORE --> SUP
    end

    CTRL <-->|watch / create / patch| K8S[("Kubernetes API")]
    CTRL <-->|"batch: submit / poll / cancel / delete"| VENDOR[("Vendor APIs<br/>Anthropic, Gemini, OpenAI")]
    CTRL -->|"model catalog: list models"| VENDOR
    CALLER(["caller"]) -->|"realtime prompts: HTTP :8082"| CTRL
    CTRL -->|"forwards to the supervisor: :8090"| POD
    INIT -->|init container, prepares| POD["Agent Pod"]
    SUP -->|runs as PID 1 inside| POD
    STATIC["statically linked with LDC, no runtime deps"] -.-> CTRL
    STATIC -.-> INIT
    STATIC -.-> SUP

The pure core, with no process of its own:

  • CRD type definitions (AgentDefinition, Station, Agent, ModelCatalog, AgentSpawner).
  • AgentSpawner scheduling (agentcore/spawner): five-field cron parsing and next/previous fire times, when a spawner is due, and the Agent it creates; for GitHub triggers, webhook signature checks, event matching and the Agent a delivery creates.
  • A Kubernetes REST + watch client.
  • The pure reconcile state machine (I/O injected, so it is unit-testable).
  • Prompt templating.
  • The Job builder.
  • Batch providers (agentcore/batch): the Anthropic, Gemini and OpenAI batch adapters and the translation of a batch result into the event lines of the vendor’s CLI, with HTTP injected so tests run on fixtures.
  • The model catalog refresh (agentcore/models): the vendors’ list-models calls and the build of the ModelCatalog status from them.
  • The shared vendor routing (core/vendor.d) that both CLI selection and batch selection use.
  • Realtime session support: the per-vendor session argv (vendors/realtime.d, with Claude’s stdin session in vendors/claude/agent.d), the session-id parsing (output/session.d) and the per-run prompt token (core/token.d).

The operator. It watches Agent resources, resolves each one’s Station and AgentDefinition, builds and creates a Job, polls the Job’s outcome, and patches the Agent’s status. It also prunes old runs beyond the Station’s history limits, and exposes /healthz (liveness), /readyz (readiness), plus a Prometheus /metrics endpoint (reconcile counts and latency, agents by phase, watch reconnects, Kubernetes API latency, and leader status).

For an AgentDefinition with execution: batch (opt-in, enabled by the components/batch kustomize component) it creates no Job. It submits the prompt itself to the batch API of the model’s vendor (Anthropic, Gemini or OpenAI), polls the batch at most every BATCH_POLL_SECONDS (default 60), records the result in status.output, and on batch.timeoutMinutes either fails the Agent or falls back to an ordinary Job. An agents.re-cinq.com/batch-cleanup finalizer makes sure a deleted Agent’s batch is cancelled and deleted at the provider. See batch runs and the controller lifecycle.

With the opt-in components/model-catalog component the leader replica also keeps the default ModelCatalog current (catalogloop.d): right after it wins the Lease, then every MODEL_CATALOG_REFRESH_SECONDS (default 3600), it lists the models each vendor’s API reports for the keys the AgentDefinitions map and writes them to the catalog’s status.

The leader also runs AgentSpawners (spawnerloop.d): every 15 seconds it lists them, creates an Agent for each one whose schedule came due, and records the slot in the spawner’s status, so a new leader carries on from there.

With the opt-in components/realtime component every replica, leader or not, also serves the prompt API on port 8082 (promptapi.d). It forwards each prompt or close for a running realtime session to the run pod’s supervisor on port 8090, signed with that run’s own token, and records a newly revealed status.sessionId.

With the opt-in components/webhooks component every replica also serves the GitHub webhook receiver on port 8083 (webhook.d). It verifies each delivery against the HMAC key its AgentSpawner names and creates an Agent named after the delivery, so a redelivery, or the same delivery reaching two replicas, starts one run. See Trigger runs from GitHub.

It combines a low-latency watch with a ~15s poll over one shared in-memory cache, so reconcile work is O(changed) and a dropped watch event is still caught by the next poll. Two replicas run with Lease-based leader election, so only the leader reconciles. See Watch + poll + cache and leader election for the mechanics.

Runs as the Pod’s init container, before the supervisor. It provisions the agent’s environment from what the recipe declares: cloning the resources.repos into the workspace and installing the agent CLI (e.g. Claude via the official installer), self-bootstrapping any missing prerequisites (git, bash, curl, sha256sum, base64) through the distro’s package manager first, and reporting its lifecycle to the same output sinks as the agent. New provisioning tools and distros are added behind the Tool and PackageManager interfaces. See Agent runtime for the full model.

Runs inside the Job Pod as the entrypoint. It launches the agent process, streams its stream-json output line by line to the configured sinks, forwards termination signals for graceful shutdown, and exits with the agent’s exit code. For a realtime session it instead serves prompts on port 8090, runs them one turn at a time (on one Claude process’s stdin, or one process per turn for the other vendors) and ends the session when it is closed, idles out or is signalled. See Agent runtime.

All three binaries are compiled with LDC with the D runtime linked statically, so they ship as self-contained executables with no language runtime to install. The initializer and supervisor can be injected into any glibc-based Station image, and run unchanged across the common Kubernetes base distros (Debian, Ubuntu, RHEL-family, Amazon Linux, Alpine). See Building for the dub configuration and link flags.

There is no external database. The controller’s entire state is the set of Agent resources and their status. This keeps the system observable with plain kubectl and recoverable after a restart: on startup the controller simply lists Agents and reconciles whatever it finds.