The framework allows developers to compose and execute directed cyclic graphs of work items ("AgentGraphs") for agentic workflows. Each node is a Task that emits an immutable state object; edges transport that state to the next node, enabling transparent plan → act → observe → act cycles that repeat until all graph edges are resolved, all while retaining full provenance.
Why?
- Multitenant Agent Runner – runs agent graphs at scale with per-tenant Kafka topic isolation and PostgreSQL persistence.
- Clean separation of concerns – "Tasks" handles doing, "Plans" handles deciding, letting you mix deterministic rules with LLM‑powered reasoning without entangling the two.
- Simple Graph Abstraction - Logically, Task nodes output to Plan nodes, and Plan nodes output to Task nodes in alternating tick-tock execution.
- Auditable – every Task Execution and Plan Execution is an append‑only event persisted to PostgreSQL.
- Protocol Buffers – all inter-service messages are protobuf-serialized over Kafka; Python task/plan code runs in isolated subprocesses.
graph TD
Start((Start)) --> TaskHITL[Task: Human In The Loop]
TaskHITL -- human input--> PlanRAG{{Plan: Call RAG}}
PlanRAG -- RAG query--> TaskCallRAG[Task: RAG Lookup]
TaskCallRAG -- RAG Output--> TaskLLMSubmit[Task: Submit <br>to LLM]
TaskLLMSubmit -- LLM Results --> PlanRefine{{Plan: Elucidate/<br> Do Execute}}
PlanRefine -- clarification--> TaskHITL
PlanRefine -- LLM Results --> CodeLLM1[Task: Tool <br> Call 1...]
PlanRefine -- LLM Results --> CodeLLM2[Task: Tool <br> Call ...n]
CodeLLM1 -- Tool 1 Results--> PostGenPlan1{{Plan: LLM}}
CodeLLM2 -- Tool n Results--> PostGenPlan2{{Plan: LLM}}
PostGenPlan1 -- plan results--> CheckCodeLLM1[Task: LLM <br>Validate]
PostGenPlan2 -- plan results --> CheckCodeLLM2[Task: LLM <br>Validate]
CheckCodeLLM1 -- Validated Results --> PlanAccept1{{Plan: Pass/Fail}}
CheckCodeLLM2 -- Validated Results --> PlanAccept2{{Plan: Pass/Fail}}
PlanAccept1 -- pass --> HumanAcceptReject1[Task: Human In The Loop]
PlanAccept1 -- observe/refine --> CodeLLM1
PlanAccept2 -- pass --> HumanAcceptReject1
HumanAcceptReject1 -- accept --> End((End))
HumanAcceptReject1 -- reject --> PlanRefine
This Coding Agent example Agent Graph illustrates plan → act → observe loops:
- A human describes an app, leading to iterative LLM summarisation and clarification cycles.
- The planner can spawn parallel code‑generation branches (
CodeLLM1 … CodeLLM N), each with its own validation and human acceptance loop. - Conditional edges (dashed in text) show how planners route either back for refinement or forward toward acceptance, embodying both depth‑first and breadth‑first flows within a single AgentGraph.
| Entity | Description |
|---|---|
| AgentGraph | Immutable graph template (nodes + edges). |
| AgentLifetime | One runtime instance of an AgentGraph executing to completion. |
| Task | Node type that performs work – API call, DB query, computation. |
| Plan | Node type that selects the next Task(s) based on rules or prior state. |
| Edge | Directed link between a Task and a Plan (or vice versa). Edges enforce alternation: PLAN→TASK or TASK→PLAN only. |
| TaskResult | Structured output from a Task. Protobuf schema supports inline data or external URI reference (currently inline only). |
| TaskExecution | Append-only record of a single Task run; contains the TaskResult. |
| PlanResult | Structured output from a Plan; contains next_task_names[] routing decision. |
| PlanExecution | Append-only record of a single Plan run; contains the PlanResult. |
A record emitted on every TaskExecution. Two sections:
- Headers – identity (
exec_id,graph_id,lifetime_id,tenant_id), timing (created_at), status, iteration, attempt count. - TaskResult –
{ oneof: { inline_data: Any | external_data: StoredData } }. Currently all results are inlined; external blob storage is defined in the protobuf schema but not yet implemented at runtime.
- Task executes and produces some output as a TaskResult.
- TaskResult is used as input to the downstream Plan.
- Plan evaluates and produces a PlanResult listing
next_task_names[]. - Downstream Task executes using the PlanResult as input and produces a TaskResult.
- Task executes and produces a TaskExecution (containing a TaskResult).
- The TaskExecution is published to the
task-executions-{tenantId}Kafka topic.
- The TaskExecution is published to the
- Data Plane persists the TaskExecution in PostgreSQL (append‑only) and republishes the message to the
persisted-task-executions-{tenantId}topic. - Control Plane reads the persisted execution, evaluates guardrails, and looks up the downstream Plan in the graph.
- Pass → publishes a PlanInput to the
plan-inputs-{tenantId}topic.
- Pass → publishes a PlanInput to the
- Executor consumes the PlanInput, executes the Plan Python code, produces a PlanExecution listing
next_task_names[], and publishes to theplan-executions-{tenantId}topic. - Data Plane persists the PlanExecution and republishes to the
persisted-plan-executions-{tenantId}topic. - Control Plane reads the persisted PlanExecution, resolves next task names against the graph, and publishes TaskInput messages to the
task-inputs-{tenantId}topic. - Steps 1-6 repeat following the graph path until all graph edges are resolved, completing the AgentLifetime.
For the complete Kafka topic contracts, protobuf schemas, lifecycle states, and internal API reference, see ARCHITECTURE.md.
The system consists of 3 core microservices plus supporting services, all Java 21 / Spring Boot, communicating via Kafka with Protocol Buffers:
| Service | Port | Role |
|---|---|---|
| Data Plane | 8081 | Persists execution records, forwards to control plane topics |
| Control Plane | 8082 | Evaluates guardrails, routes messages between executors based on graph topology |
| Executor | 8083 | Executes Python task/plan code via subprocess, publishes results |
| Graph Composer | 8088 | Web UI + REST API for graph management |
| Graph Builder | 8087 | Parses graph specifications (DOT format) |
| Admin | 8086 | Administrative functions |
| Frontend | 5173 | React/Vite UI for graph visualization |
sequenceDiagram
participant E as Executor
participant DP as Data Plane
participant CP as Control Plane
E->>DP: TaskExecution (protobuf/Kafka)
DP->>DP: Persist to PostgreSQL
DP->>CP: persisted-task-execution
CP->>CP: Evaluate guardrails + resolve graph
CP->>E: PlanInput
E->>DP: PlanExecution (protobuf/Kafka)
DP->>DP: Persist to PostgreSQL
DP->>CP: persisted-plan-execution
CP->>CP: Resolve next task(s) from graph
CP->>E: TaskInput
Note over E,CP: Loop repeats until all graph edges are resolved
All topics follow pattern: {message-type}-{tenantId}
task-executions-{tenantId}– Executor publishes TaskExecution protobuf messagesplan-executions-{tenantId}– Executor publishes PlanExecution protobuf messagespersisted-task-executions-{tenantId}– Data Plane forwards to Control Planepersisted-plan-executions-{tenantId}– Data Plane forwards to Control Planeplan-inputs-{tenantId}– Control Plane publishes PlanInput for Executortask-inputs-{tenantId}– Control Plane publishes TaskInput for Executor
The control plane includes a GuardrailEngine that evaluates every execution before routing to the next node. The engine is currently a pass-through stub with the interface in place for future policy enforcement.
Planned capabilities:
- Declarative YAML policies (e.g.,
sum(tokens_used) > 10,000 → abort_lifetime) - Per‑execution or cumulative per‑lifetime/tenant enforcement
- Actions on breach:
REJECT_EXECUTION,PAUSE_LIFETIME,ABORT_LIFETIME
- Task/Plan Python Code – implement
plan.pywithplan(plan_input) -> PlanResultortask.pywithtask(task_input) -> TaskResult - Graph Specification – define graphs in GraphViz DOT format with
type="plan"/type="task"attributes - Guardrail Engine – pluggable policy evaluation (planned: YAML v1, Rego)
- Language Ports – interfaces are protobuf; additional language runtimes can be added alongside Python
- Per-tenant isolation – Kafka topics are scoped by tenant ID; routing and persistence are tenant-aware.
- Append-only audit trail – all executions are immutable records in PostgreSQL.
- Manual Kafka acknowledgment – messages are only acknowledged after successful persistence, providing at-least-once delivery.
- Implement guardrail engine with YAML policies + cost/token tracking.
- Add policy telemetry fields (
tokens_used,cost_usd,latency_ms) to execution records. - Implement blob store adapter (S3/GCS) for large TaskResult payloads.
- Add edge type semantics (
NORMAL,PARALLEL,JOIN,FINAL) for richer graph control flow. - Add deterministic replay capability.
- Build Python SDK with typed Task/Plan base classes.
- Pluggable adapter SPI for Event Bus, State DB, Blob Store, KV Store.
- Local development profile (in-memory queue + SQLite).
- Dead letter queue (DLQ) per tenant for failed messages.
- gRPC interfaces for inter-service communication alongside REST.
- Helm chart for Kubernetes deployment.