Skip to main content

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 pending then running) 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 client AgentLoop.serve() process picks up the pending run and executes it; SSE events on GET /v1/runs/{id}/events.
  • The MessageBus (in memory, or NatsMessageBus over NATS) links the channels to the engine: inbound queue read by serve(), outbound queue read by the channels.
  • Server side, python -m eai.worker handles 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.AgentLoop takes in messages, prepares the context, manages sessions, resumes a run after a crash, handles stop, cron and heartbeat, and exposes serve(); enterprise_ai.runtime.AgentRunner runs one turn; enterprise_ai.subagents.SubagentManager runs sub-agents in parallel; enterprise_ai.loop.plan.RunPlan carries the plan; enterprise_ai.runtime.bus.MessageBus links 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.worker runs the periodic jobs: scheduler tick, event outbox drain, quota reservation expiry, stale heartbeat detection, retention.
  • Developed: AgentLoop, AgentRunner and its continuation rules (enterprise_ai.runtime.continuation.default_rules), SubagentManager, RunPlan, MessageBus, the services/control-plane/src/eai/domains/agents domain (registry, runs, checkpoints, heartbeats, scheduler, triggers).

4. Data model​

Tables (schema eai, tenant_id RLS):

  • eai.agents
  • eai.agent_versions
  • eai.runs
  • eai.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/agents
  • POST /v1/agents
  • GET /v1/agents/{id}
  • PATCH /v1/agents/{id}
  • DELETE /v1/agents/{id}
  • GET /v1/agents/{id}/versions
  • POST /v1/agents/{id}/runs
  • GET /v1/runs
  • GET /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: AgentLoop options (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), attributes eai.tenant_id, eai.run_id, traceparent propagation.
  • 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=stop and AgentRunner stops 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=true and PostgreSQL; external components that are not configured use their in-process fallback.

14. Acceptance tests​

  1. Create an agent from a valid manifest then start a run
  2. Run refused without agents:run scope (403)
  3. Same Idempotency-Key -> same run_id
  4. Cross-tenant access refused
  5. Error returned in fr with Accept-Language: fr