C-01 Agent Runtime
Brick C: Agents, autonomy, execution. French version: C-01-agent-runtime
1. Role and responsibilities
Runs the agents declared by an Agent Manifest following the Navin architecture: version resolution, message intake, execution turn (model call then tool calls until the final answer), model calls through the Model Gateway, tool calls through the Tool Gateway, steps emitted in the execution contract.
- Load the agent and its immutable version (
eai.agent_versions) - Start a run (state
pendingthenrunning) and enforce the budgets AgentLoop(enterprise_ai.loop): message intake (bus, pending runs, watched sessions), context, sessions, crash resume, stop, cron, heartbeat,serve()AgentRunner(enterprise_ai.runtime): one full turn with the continuation rules (work remaining, user already said go, verify before finishing)- Handle suspend, resume and kill at agent level
2. Position in the architecture and interactions
- Callers: Portal, SDK, CLI, Channel Gateway (D-13), Scheduler (C-09), Event Triggers (C-10).
- Execution path: synchronous run creation (
POST /v1/agents/{id}/runs), then the clientAgentLoop.serve()process picks up the pending run and executes it; SSE events onGET /v1/runs/{id}/events. - The
MessageBus(in memory, orNatsMessageBusover NATS) links the channels to the engine: inbound queue read byserve(), outbound queue read by the channels. - Server side,
python -m eai.workerhandles the scheduler tick, outbox drain, quota expiry, stale heartbeats and retention. - Every interaction goes through the Control Plane API v1 (
/v1) behind the API gateway (D-14).
3. Open source component and developed part
- Engine: Navin architecture of the Python SDK, with no declared graph.
enterprise_ai.loop.AgentLooptakes in messages, prepares the context, manages sessions, resumes a run after a crash, handles stop, cron and heartbeat, and exposesserve();enterprise_ai.runtime.AgentRunnerruns one turn;enterprise_ai.subagents.SubagentManagerruns sub-agents in parallel;enterprise_ai.loop.plan.RunPlancarries the plan;enterprise_ai.runtime.bus.MessageBuslinks channels and engine. - Optional adapters: LangGraph (
enterprise_ai.runtime.langgraph.build_graph) and Temporal (enterprise_ai.runtime.temporal.build_workflow) reuse the same steps; neither is required. - Server side,
python -m eai.workerruns the periodic jobs: scheduler tick, event outbox drain, quota reservation expiry, stale heartbeat detection, retention. - Developed:
AgentLoop,AgentRunnerand its continuation rules (enterprise_ai.runtime.continuation.default_rules),SubagentManager,RunPlan,MessageBus, theservices/control-plane/src/eai/domains/agentsdomain (registry, runs, checkpoints, heartbeats, scheduler, triggers).
4. Data model
Tables (schema eai, tenant_id RLS):
eai.agentseai.agent_versionseai.runseai.run_steps
Common columns: id uuid, tenant_id uuid NOT NULL, created_at, updated_at; RLS policy current_setting('app.tenant_id', true)::uuid.
5. Contract
- Applied contract: agent-manifest + execution-contract
- JSON Schema:
contracts/v1/agent-manifest.schema.json,contracts/v1/execution-contract.schema.json - Key fields:
spec.runtime, spec.models, spec.tools, spec.permissions, spec.autonomy, spec.budgets; run: state, steps, budgets, retry_policy, trace - Validation:
POST /v1/contracts/{name}/validate; manifest apply:POST /v1/manifests/apply.
6. API endpoints
GET /v1/agentsPOST /v1/agentsGET /v1/agents/{id}PATCH /v1/agents/{id}DELETE /v1/agents/{id}GET /v1/agents/{id}/versionsPOST /v1/agents/{id}/runsGET /v1/runsGET /v1/runs/{id}GET /v1/runs/{id}/events
Request POST /v1/agents/{id}/runs:
{
"task": {
"input": "Summarize ticket INC-42"
},
"session_id": "ses_7f2",
"budgets": {
"max_steps": 20,
"max_cost": {
"amount": "0.50",
"currency": "EUR"
}
}
}
Response:
{
"run_id": "run_01J9",
"state": "queued",
"agent": {
"id": "agt_support",
"version": 3
},
"trace": {
"trace_id": "4bf92f35..."
}
}
7. SDK examples
Python:
from enterprise_ai import EnterpriseAI
from enterprise_ai.loop import AgentLoop
from enterprise_ai.runtime.bus import BusMessage, MessageBus
client = EnterpriseAI(base_url="<control-plane-url>", api_key="eai_xxx_secret")
bus = MessageBus()
loop = AgentLoop(client=client, model="claude-sonnet-eu", agent_id="agt_support", bus=bus,
watch_sessions=["ses_7f2"])
result = loop.run_once("Summarize INC-42", session_id="ses_7f2") # one AgentRunner turn
print(result.stop_reason, result.final_text)
loop.resume("run_01J9") # restart a crashed run from its last checkpoint
# asyncio.run(loop.serve()) # intake: bus, pending runs, watched sessions
TypeScript:
import { EnterpriseAI } from "@enterprise-ai/sdk";
const client = new EnterpriseAI({ baseUrl: "<control-plane-url>", apiKey: process.env.EAI_API_KEY });
const agent = await client.agents.create({ manifest });
const run = await client.agents.run(agent.id, { task: { objective: "Summarize INC-42" } });
Java:
EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.build();
var agent = client.agents().create(manifest);
var run = client.runs().start("agt_support", Map.of("task", "Summarize INC-42"));
Go:
c, _ := eai.NewClient(eai.WithBaseURL("<control-plane-url>"), eai.WithAPIKey(os.Getenv("EAI_API_KEY")))
_, _ = c.Agents.Create(ctx, manifest)
run, _ := c.Runs.Start(ctx, "agt_support", map[string]any{"task": "Summarize INC-42"})
8. Configuration
- Environment variables:
EAI_DATABASE_URL,EAI_HEARTBEAT_STALE_SECONDS,EAI_NATS_URL(optional NATS bus),EAI_TEMPORAL_URL(optional Temporal adapter) - Helm keys:
controlPlane.replicas,controlPlane.config,worker.enabled,worker.replicas - Client side:
AgentLoopoptions (model,agent_id,bus,watch_sessions,poll_interval,history_limit,context_budget,budget)
9. Security controls
- Scope agents:run required; OpenFGA relation can_run on the agent
- OPA policy agent.run evaluated before start
- Secrets never injected into the prompt; only leases (D-11)
- Every request carries the tenant resolved from the credential; PostgreSQL RLS
- Every mutation and policy decision writes a hash-chained audit event
10. Observability
- Prometheus metrics:
eai_runs_total{state},eai_run_duration_seconds,eai_run_steps_total - Traces: OpenTelemetry spans with GenAI semantic conventions (
gen_ai.operation.name,gen_ai.agent.id,gen_ai.tool.name), attributeseai.tenant_id,eai.run_id,traceparentpropagation. - Audit actions:
agent.apply,run.create,run.transition
11. FinOps units
- Model tokens per run, step count, worker duration; agent_id/run_id attribution in the Usage Event.
- Emitted as Usage Event (
POST /v1/finops/usage) with tenant, project, agent and run.
12. Failure modes and resilience
- Client process crash:
AgentLoop.resume(run_id)reads the last checkpoint (GET /v1/runs/{id}/checkpoints) and resumes the turn - Stop requested: the heartbeat answers
action=stopandAgentRunnerstops before the next model or tool call;AgentLoop.shutdown()finishes the current turn before exiting - Idempotency-Key on POST runs: no duplicate run
- Budget exceeded: the turn stops with the reason of the limit reached
- Normalized errors (error envelope, message_i18n en/fr)
13. Standalone adoption (client of a client)
- Python SDK
enterprise_ai.loop+enterprise_ai.runtime+ Control Plane + PostgreSQL; NATS, LangGraph and Temporal are optional. - SDK:
enterprise_ai.agent(AgentsClient,RunsClient,SessionsClient),enterprise_ai.loop.AgentLoop,enterprise_ai.runtime.AgentRunner - Minimal deployment: Helm chart with
controlPlane.enabled=true,worker.enabled=trueand PostgreSQL; external components that are not configured use their in-process fallback.
14. Acceptance tests
- Create an agent from a valid manifest then start a run
- Run refused without agents:run scope (403)
- Same Idempotency-Key -> same run_id
- Cross-tenant access refused
- Error returned in fr with Accept-Language: fr