Orchestration Engine
The Orchestration Engine is the core agent execution runtime for AEGIS. It uses LangGraph to build a stateful graph pipeline that processes user messages through system prompts, memory retrieval, LLM calls, tool execution, skill injection, HITL approval checkpoints, and output formatting.
Overview
This service is the domain-neutral agent runtime at the center of AEGIS. Compliance domains are installable packs discovered at startup (packs/<name>/manifest.yaml, or out-of-tree via PACKS_DIR), and agent behavior — skills, routers, rules, prompts, settings — is tenant-configurable data served by the agent-config-service, not code in this repo. The five bundled packs (rule_37, rule_32, form_pr, flaring_monitor, epa_oooob) are all part of the first installed vertical (Texas RRC oil & gas / EPA); adding a domain is a new pack directory, not a core edit.
When a user sends a message to an AI agent, the orchestration engine:
- Assembles the agent’s system prompt (persona + identity; for general mode, a router manifest sealed at conversation birth) and conversation history
- Retrieves working memory and episodic memories
- Calls the LLM (via LiteLLM) with budget enforcement
- Executes any tool calls the LLM requests — including the R36 selection tools
pull_routers/select_skill, which load skills mid-turn - Checks if HITL approval is required
- Formats the final output
(The legacy marker-scanning skill selection hop was retired in R36, and the vestigial synthesis LLM phase was removed entirely in R41 B0 — the graph runs a single LLM node and approval_node flows straight to output_format.)
Beyond the core agent pipeline, the orchestration engine also hosts the compliance dashboard API, checklist management, rule lifecycle, and workspace SSE endpoints.
Port & Language
| Property | Value |
|---|---|
| Port | 8001 |
| Language | Python 3.12 |
| Framework | FastAPI |
| Entry point | src/orchestration/main.py |
Key Endpoints
Authorization (R42d)
All user-facing routers require perimeter-stamped identity via
aegis_shared.auth (get_current_user — including /execute and the SSE
stream), mode-gated by AEGIS_AUTH_MODE. Fine-grained gates on top:
filings.submit on checklist submit, filings.review on checklist
reject, eval.read / eval.run on the eval routes,
power_user/admin on rule-version writes and /compliance/assess, and
skills.author on the sandbox code-block dry-run. See
Auth Flow — Service-Side Enforcement.
Agent Execution
| Method | Path | Description |
|---|---|---|
POST | /execute | Synchronous full pipeline execution. Runs the entire LangGraph graph and returns the final state. |
GET | /conversations/{conversation_id}/stream | SSE streaming endpoint. Streams node_end events as each LangGraph node completes, then a done event. |
GET | /health | Health check. Returns service version. |
Conversations
| Method | Path | Description |
|---|---|---|
GET | /conversations | List the requesting user’s conversations (scoped to the gateway-injected X-User-Id; 401 without identity). Excludes soft-deleted rows and harness runs (conversation_type replay/test). Rows include a preview snippet. |
POST | /conversations | Create a conversation. Accepts an optional caller-supplied id (harness pre-registration, idempotent) and derives ownership from X-User-Id. |
GET | /conversations/{id}/messages | Get conversation messages. |
PATCH | /conversations/{id} | Update conversation title or status (owner-guarded; 404 on mismatch). |
DELETE | /conversations/{id} | Soft-delete a conversation (owner-guarded). |
Compliance Dashboard (R1.2)
| Method | Path | Description |
|---|---|---|
GET | /compliance/summary | Summary cards (total entities, domain counts, status distribution). |
GET | /compliance/matrix | Entity x compliance domain matrix with color-coded status cells. |
GET | /compliance/charts/* | Chart data (deadline bar, compliance trend, filing status, flaring volume). |
POST | /compliance/assess | Trigger compliance assessment for an entity. |
Checklists (R1.3)
| Method | Path | Description |
|---|---|---|
GET | /checklists | List filing checklists. |
POST | /checklists | Create a new checklist from a template. |
GET | /checklists/{id} | Get checklist with items. |
PUT | /checklists/{id}/items/{idx} | Update a checklist item status. |
POST | /checklists/{id}/submit | Submit a completed checklist for review. |
Artifacts
| Method | Path | Description |
|---|---|---|
GET | /checklists/{id}/items/{idx}/artifacts | List artifacts for a checklist item. |
POST | /checklists/{id}/items/{idx}/artifacts | Upload or create an artifact. |
Rules (R1.5)
| Method | Path | Description |
|---|---|---|
GET | /rules | List rule versions. |
POST | /rules | Create a new rule version (immutable). |
GET | /rules/{identifier}/versions | Get all versions of a specific rule. |
GET | /rules/changes | Detect rules that changed since a reference date. |
POST | /rules/monitor/scan | Trigger a rule change monitoring scan. |
Workspaces (R1.4)
| Method | Path | Description |
|---|---|---|
POST | /workspaces/{id}/assess | Start an automated compliance assessment. |
GET | /workspaces/{id}/stream | SSE stream for workspace events. |
POST | /workspaces/{id}/items/{idx}/agent | Send a message to the scoped agent for a checklist item. |
Eval Capture (R40a)
| Method | Path | Description |
|---|---|---|
POST | /eval/captures | Record an expert verdict (thumbs up/down + optional enrichment) on a completed turn. Upserts on (execution_id, captured_by); identity comes from X-User-Id/X-Tenant-Id headers, never the body. |
GET | /eval/captures?conversation_id= | The requesting tester’s captures for one conversation (renders captured state on reload). |
GET | /eval/metrics | Internal missing-rate counts (snapshot_status='missing' is a writer-bug detector, not an operating mode). |
At turn end (both the SSE turn runner and the sync /execute path) the full
assembled context publishes fire-and-forget to the snapshots:pending Redis
stream; a background consumer-group writer drains it into turn_snapshots
(ack-after-persist, so entries survive restarts). See the database schema
page for the tables.
Architecture
LangGraph Pipeline
The core execution follows a StateGraph (R36 — the skill_select_node / skill_inject_node hop is deleted; skill selection happens inside the tool loop):
START -> system_prompt_node -> memory_node -> resume_guard -> initial_llm_call
-> [tool_node -> initial_llm_call]* (tool loop; pull_routers /
select_skill execute here as ordinary tool calls)
-> approval_node -> output_format -> ENDRouting logic between nodes:
- After
initial_llm_call: If there are pending tool calls, route totool_node. If there is an error, route tooutput_format. Otherwise, route toapproval_node. - After
tool_node: If a HITL gate fired (awaiting_hitl), route tooutput_format. Check iteration limit. Otherwise route back toinitial_llm_call. approval_nodeflows straight tooutput_format(R41 B0 removed the vestigial synthesis phase). It either pausesawaiting_hitlor passes through — either way the next stop isoutput_format.
Module Breakdown
src/orchestration/
├── main.py # FastAPI app, /execute and /stream endpoints
├── engine.py # LangGraph StateGraph construction and compilation
├── nodes.py # Node implementations (1 function per node)
├── tools.py # Tool definitions and execution for LLM function calling
│ # (hosts SKILL_SELECT_MARKER_TOMBSTONE — the retired legacy selection marker)
├── selection_tools.py # R36: pull_routers / select_skill tool handlers
├── conversation_runtime.py # R36: sealed-prefix runtime state (conversation_runtime_state)
├── context_maintenance.py # R36: per-turn router deltas, lazy upgrades, tombstones
├── state.py # GraphState TypedDict (flows through the graph)
├── schemas.py # Request/response Pydantic models
├── services.py # HTTP clients for memory, KG, approval services
├── sandbox/ # Unix-socket client + pipeline for the sandbox-runner
│ ├── python_client.py # (R33): ships skill code blocks to the jailed runner
│ ├── pipeline.py # AST pre-validation → socket → structured result
│ └── harness.py # fail-closed governance gate around execution
├── budget.py # Token/cost budget enforcement
├── config.py # Settings from environment variables
├── conversation_routes.py # Conversation CRUD (list, create, get, update, delete)
├── packs/ # Pack discovery (R41 B2): manifest per compliance domain
│ ├── __init__.py # discover_packs() / load_packs()
│ ├── schema.py # PackManifest schema (v1)
│ ├── rule_37/manifest.yaml # Skills + checklist template for Rule 37
│ ├── rule_32/manifest.yaml # … Rule 32
│ ├── form_pr/manifest.yaml # … Form PR
│ ├── flaring_monitor/manifest.yaml
│ └── epa_oooob/manifest.yaml # … EPA OOOOb (fifth domain, platform-expansion)
├── skills/ # Checklist-driven skill implementations
│ ├── __init__.py # Slug-keyed skill registry (register_skill, list_registered_skills)
│ ├── base.py # Base skill class (SKILL_KEY identity)
│ ├── declarative.py # Descriptor-driven skills (R41 B4): manifest descriptors, no Python
│ ├── rule37/ # Rule 37 spacing exception skills (11 skills)
│ ├── rule32/ # Rule 32 flaring exception skills (10 skills)
│ ├── form_pr/ # Form PR production report skills (8 skills)
│ ├── flaring_monitor/ # Flaring monitor skills (6 skills)
│ └── field_event/ # Field event conversation skills
├── compliance/ # Compliance dashboard and workspace
│ ├── routes.py # /compliance/summary, /matrix, /charts
│ ├── engine.py # Compliance status computation engine
│ ├── queries.py # Cypher query templates for compliance
│ ├── checklist_routes.py # Checklist CRUD routes
│ ├── artifact_routes.py # Artifact management routes
│ ├── rule_routes.py # Rule version CRUD routes
│ ├── rule_lifecycle.py # Immutable rule versioning logic
│ ├── rule_monitor/ # Rule change monitoring
│ └── workspace/ # Entity workspace (assessment, SSE, agent)
│ ├── routes.py # Workspace API routes
│ ├── assessment.py # Automated assessment logic
│ └── events.py # WorkspaceEventType enum, SSE event formatting
├── seed_checklists.py # Checklist template seeder (content from pack manifests)
├── seed_rules.py # Compliance rule data seeding script
├── seed_conversations.py # Demo conversation seeding
└── seed_demo_data.py # Combined demo data seederGraphState
The GraphState TypedDict carries all execution state through the graph:
class GraphState(TypedDict, total=False):
execution_id: str
conversation_id: str
agent_id: str
agent_type: str # "rule_37", "rule_32", etc.
tenant_id: str
status: str # "pending", "running", "completed", "awaiting_hitl", "failed"
messages: list[dict] # Conversation messages (role/content/tool_calls)
system_prompt: str
model: str # LLM model to use (default: gpt-4o)
loaded_skills: list # skill_keys currently loaded (R36: grows mid-turn via select_skill)
injected_skill_ids: list # Skills already injected
active_skills: dict # skill_id -> tier level
tokens_used: int
cost_usd: float
max_tokens: int
max_cost_usd: float
pending_tool_calls: list # Tool calls awaiting execution
iteration: int # Guards against infinite loops
memory_context: dict # Working + episodic memory data
hitl_required: bool
hitl_checkpoint_type: str
hitl_approval_id: str | NoneBudget Enforcement
The budget.py module enforces two independent tiers (R41 A8):
- Per-conversation (cumulative across every turn):
BudgetExceededErrorwhentokens_used >= max_tokensorcost_usd >= max_cost_usd. - Per-turn (spend since the turn began):
BudgetExceededErrorwhentokens_used - turn_tokens_start >= max_tokens_per_turn(or the cost equivalent) — bounds a single runaway turn while the conversation still has headroom.
tokens_remaining returns the tighter of the two tiers, so the per-call output
ceiling never blows past either. Budget is checked before every LLM call. When exceeded,
/execute returns HTTP 429 and the SSE stream emits an error event. Per-turn env
defaults (DEFAULT_MAX_TOKENS_PER_TURN / DEFAULT_MAX_COST_PER_TURN) equal the
per-conversation ceiling, so the per-turn tier is dormant until an operator lowers it.
Platform Settings Resolution (Tenant LLM Config)
At every turn start — both the synchronous /execute path and the SSE stream — the engine fetches GET /settings from the agent-config-service (with X-Tenant-Id/X-User-Id headers) and resolves the model and per-execution budgets as:
request override → tenant platform setting → env defaultThe env defaults (DEFAULT_LLM_MODEL, DEFAULT_MAX_TOKENS_PER_EXECUTION, DEFAULT_MAX_COST_PER_EXECUTION, DEFAULT_MAX_OUTPUT_TOKENS_PER_CALL, DEFAULT_THINKING_MODE) remain the fallback floor; a settings fetch failure degrades to them.
The last two are per-LLM-call knobs (no request override): max_output_tokens_per_call is the max_tokens sent on each call — thinking + visible output share it, so on Claude Sonnet 5 (adaptive thinking by default) a low ceiling can be consumed entirely by a thinking block and yield an empty assistant message; llm_call now fails the turn loudly when that happens (finish_reason=length with no content and no tool calls) instead of appending an empty message. thinking_mode (adaptive | disabled) is sent explicitly to Anthropic models (omitted for Haiku-tier/non-Anthropic, and re-derived when a call retries on the fallback_model). Admins manage the tenant values from the Platform Settings page. If the tenant sets a fallback_model, every LLM call site retries once on that model when the primary model errors. Seeded agent_definitions no longer pin model_config.model_preference (now null), so the tenant default_model governs — an explicitly set per-agent model_preference still overrides. Workspace assessment skills and the scoped agent use the same settings.
Daily caps (tenant-aggregate plus optional per-user overrides) are enforced with Redis counters — usage:daily:{tenant}:{yyyymmdd}:tokens|cost and usage:daily:{tenant}:{user}:{yyyymmdd}:* (48h TTL). The counters are checked before a turn starts (429 on /execute; a terminal SSE error event on the stream) and incremented with actual usage after each turn — so an in-flight turn can overshoot a cap, and it is the next turn that gets blocked.
Skill Selection (R36 — message-driven)
The legacy marker-scanning selection flow is gone. General-mode runs (an agent_definitions row with root_skill_key = NULL) receive a router manifest (router_key + recruitment_blurb per active router) in the index-0 system prompt block, plus two selection tools (orchestration/selection_tools.py):
pull_routers(router_keys[])— returns the pulled routers’ selectable skills with full descriptions (derived read over approved mappings × active skills × latest-approved description version)select_skill(skill_key)— fires the existingload_skill()machinery: context templates land as system messages, the skill’sllm_visiblecode-block tools join the next tool assembly
Validation failures return typed errors and fail toward deny — domain tools do not exist in the run until select_skill succeeds. Agent-mode runs (rule_37, rule_32) load their root skill at conversation start and do not pull routers.
The general-mode index-0 block is sealed at conversation birth (conversation_runtime_state, migration 016) and reused byte-for-byte on reopen; router changes, lazy skill version upgrades, and skill-deletion tombstones reach live conversations as tail appends only (conversation_context_appends), applied per-turn by context_maintenance.py.
HITL Approval Checkpoints
HITL fires from the seeded require_hitl SkillRules on the RRC skills (R35 P5): a rule matches the executed tool name on after_tool_call (e.g. rule37_filing_assembly → pre_filing, good_cause_narrative → good_cause_review, rule32_filing_assembly → pre_filing). A before_tool_call platform rule gates mutating capabilities, and request metadata can still override hitl_checkpoint_type.
When HITL is required, the engine creates an approval request via the approval service and pauses execution with status: "awaiting_hitl".
Tool Definitions
Since R35 P4 the visible tool list derives from provenance, not agent type: core capabilities (entity_resolve, context_assemble, and since R43 render_chart) are always present; R36 adds the selection tools (pull_routers, select_skill) for general mode; everything domain-specific arrives from the loaded skills’ llm_visible code blocks. Those code blocks are not run in-process — they are shipped over a unix socket to the sandbox-runner (nsjail-jailed Python), which is what keeps tenant-authored skill code from touching the host:
| Tool | Source | Description |
|---|---|---|
entity_resolve | core | Resolve an entity by name/API/alias |
context_assemble | core | Assemble an entity’s dual-view context |
render_chart | core (R43) | Validate an AEGIS chart spec and emit it as a chart artifact (presentational, read-only — never HITL-gated) |
pull_routers | selection (R36, general mode) | Pull routers to see full skill descriptions |
select_skill | selection (R36, general mode) | Load a skill from a pulled router |
spacing_assessment, offset_well_analysis, rule37_filing_assembly, good_cause_narrative | skill rrc_rule37 | Rule 37 domain tools (sandboxed code blocks) |
flaring_volume_calc, gas_analysis, rule32_filing_assembly, emissions_estimate | skill rrc_rule32 | Rule 32 domain tools (sandboxed code blocks) |
create_skill_draft, add_code_block, add_rule, run_dry_run, submit_for_review, create_skill_revision | skill skill_creator (R38, native handlers) | Skill Creator write tools (authoring) |
list_action_capabilities, list_my_drafts, list_categories, get_effective_rules | skill skill_creator (R38 + R39 P0, native handlers) | Skill Creator read tools |
R38 — the Skill Creator’s authoring tools. The platform creator agent (root skill skill_creator, seeded by agent-config-service) gets ten tools (R39 P0 added the list_categories read) from orchestration/authoring_tools.py — native handlers, not sandboxed code blocks — but visibility stays data-driven: their schemas are unioned into the LLM payload only when the skill_creator skill is loaded (no agent_type branch). All six write tools are in MUTATING_TOOLS (fail-closed when governance is unevaluable), and they act as the conversing user via fail-closed X-User-Id headers, so owner_id lands as the real user. The Creator authors only — it has no approve/map/retire/invoke tools; admin approval (publish-on-approve, R38) is what makes an authored skill loadable and reachable.
The deferred flaring_monitor / compliance_monitor agents keep a hardcoded fence set (R35-FENCE) until their skills land. See LangGraph Pipeline for the full breakdown.
Sandbox Runner (Jailed Code Execution)
Skill code blocks are tenant-authored, so they never execute in the orchestration
process. The sandbox-runner (services/sandbox-runner/) is the platform’s only
privileged component: a stdlib-only root server, reachable solely over a local
unix socket (root:aegis, 0660 — never a TCP port), that spawns nsjail to run
a single Python code block under namespaces + seccomp + cgroups and returns a
structured JSON result. It has no service page of its own because it has no HTTP API.
The defense is two-layer: an unprivileged AST validator
(aegis_shared.sandbox.validation) is the cheap first pass, and the deny-by-default
seccomp policy (infrastructure/deploy/sandbox/python.seccomp.kafel) is the
load-bearing containment. The orchestration side lives in
src/orchestration/sandbox/ (python_client.py → socket, pipeline.py,
harness.py); the fail-closed governance gate blocks classification-bearing code
when governance is unevaluable (R41 Phase A). Socket path and limits are configured
via AEGIS_SANDBOX_SOCKET, AEGIS_SANDBOX_WALL_S, and AEGIS_SANDBOX_MAX_OUTPUT_BYTES.
Dependencies
Python Packages
| Package | Version | Purpose |
|---|---|---|
fastapi | ^0.115 | Web framework |
uvicorn | ^0.34 | ASGI server |
langgraph | ^0.4 | StateGraph agent execution engine |
litellm | ^1.60 | LLM routing (OpenAI, Anthropic, etc.) |
sse-starlette | ^2.0 | Server-Sent Events for streaming |
aegis-shared | local | Shared models and DB helpers |
langfuse | ^4.0 | LLM observability / tracing (OTEL-based, optional) |
opentelemetry-{api,sdk,exporter-otlp} | ^1.27 | OTEL plumbing for the litellm langfuse_otel callback |
Service Dependencies
| Service | Protocol | Purpose |
|---|---|---|
| Memory Service (8002) | HTTP | Working memory, episodic memory, injection ledger |
| Knowledge Graph Service (8003) | HTTP | Entity context assembly (Tier 3.5) |
| Approval Service (8004) | HTTP | HITL approval request creation |
| Sandbox Runner | Unix socket | Jailed Python execution for skill code blocks (R33; AEGIS_SANDBOX_SOCKET, default /run/aegis-sandbox/sandbox.sock) — never a TCP port |
| Agent Config Service (8010) | HTTP | Skill definitions/personas/rules (R35); router manifest + disambiguation reads for skill selection (R36); tenant platform settings (GET /settings at every turn start) |
| PostgreSQL | TCP | Skill registry, conversation persistence, conversation runtime state (R36 sealed prefix), compliance data |
Configuration
| Environment Variable | Default | Description |
|---|---|---|
ORCHESTRATION_HOST | 0.0.0.0 | Bind address |
ORCHESTRATION_PORT | 8001 | Bind port |
DATABASE_URL | postgresql://aegis:aegis_local@localhost:5432/aegis | PostgreSQL connection |
REDIS_URL | redis://localhost:6379 | Redis connection |
KAFKA_BOOTSTRAP_SERVERS | localhost:9092 | Kafka brokers |
DEFAULT_LLM_MODEL | gpt-4o (code fallback) | Default LLM model for agent calls. Fallback floor under tenant Platform Settings — used when the tenant has no default_model set or the settings fetch fails. The dev box sets this to anthropic/claude-sonnet-5 (routes via LiteLLM to Anthropic; the box OpenAI key is dead) |
DEFAULT_MAX_TOKENS_PER_EXECUTION | 100000 | Default token budget per execution (fallback floor under Platform Settings) |
DEFAULT_MAX_COST_PER_EXECUTION | 5.0 | Default cost budget (USD) per execution (fallback floor under Platform Settings) |
DEFAULT_MAX_OUTPUT_TOKENS_PER_CALL | 16384 | Per-LLM-call max_tokens ceiling — thinking + visible output (fallback floor under Platform Settings) |
DEFAULT_THINKING_MODE | adaptive | Thinking mode sent explicitly to Anthropic models: adaptive | disabled (fallback floor under Platform Settings) |
MAX_GRAPH_ITERATIONS | 20 | Max tool call loop iterations |
MEMORY_SERVICE_URL | http://localhost:8002 | Memory service URL |
KNOWLEDGE_GRAPH_SERVICE_URL | http://localhost:8003 | Knowledge graph service URL |
APPROVAL_SERVICE_URL | http://localhost:8004 | Approval service URL |
AGENT_CONFIG_SERVICE_URL | http://localhost:8010 | Agent config service URL (skills, personas, rules, R36 skill routers) |
EPISODIC_TOP_K | 3 | Number of episodic memories to retrieve |
LANGFUSE_PUBLIC_KEY | (empty) | Langfuse public key — tracing is enabled only when set |
LANGFUSE_SECRET_KEY | (empty) | Langfuse secret key — tracing is enabled only when set |
LANGFUSE_HOST | https://us.cloud.langfuse.com | Langfuse host/region (US default; EU/JP/HIPAA/self-hosted supported) |
ORCHESTRATION_REPLAY_PORT | 8101 | R40b loopback replay bind port (replay_app). Host is pinned to 127.0.0.1 in code — never 0.0.0.0 |
EVAL_JUDGE_MODEL | anthropic/claude-sonnet-4-6 | R40b expert-grader judge model, passed explicitly to litellm so it never self-grades with the model under test |
EVAL_JUDGE_THRESHOLD | 0.7 | R40b judge score ≥ threshold → pass (recorded per run) |
AEGIS_GIT_SHA | (unset) | R40b: optional override for the build SHA on /health; falls back to git rev-parse HEAD at the repo root |
Replay bind (R40b eval)
The eval runner drives cases through a separate FastAPI app
(orchestration.replay_app:replay_app) bound loopback-only on
127.0.0.1:8101, run as its own systemd unit (aegis-replay.service). It is
not a second bind of the main app — that would expose /replay publicly via
Caddy’s forward_auth. It mounts only POST /replay (plus /health) and shares
the engine internals by import; the replay_origin flag exists only on its
request model, so there is no public surface to forge it on. Nothing in the
Caddyfile references the port → it is structurally unreachable from the edge.
Observability (Langfuse Tracing)
The engine ships optional Langfuse tracing, wired in
orchestration/observability.py. It is off by default and activates only
when both LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are set — otherwise
every tracing helper is a no-op and the service behaves identically.
How it works:
- At startup (
init_tracing()in the FastAPI lifespan) the engine enables the LiteLLMlangfuse_otelcallback for alllitellm.acompletioncalls and initialises the Langfuse SDK (v4, OpenTelemetry-based). - Each agent run is wrapped in a parent span via
trace_agent_run(...). LiteLLM generation spans nest under it automatically: both layers ride the global OTELTracerProviderand share OTEL context, and Langfuse’s span processor exportsgen_ai.*spans by default — yielding one trace per agent run with each LLM call as a nested generation (model, tokens and cost captured automatically).
Trace attributes set:
| Attribute | Source | Why |
|---|---|---|
session_id | conversation_id / checklist_id | Groups multi-turn runs in the Sessions view |
user_id | metadata.user_id | User filtering and cost attribution |
tags | agent type + phase (execute / stream / assessment / workspace-agent) | Per-agent / per-feature analytics |
| input / output | user message + final assistant message | Readable traces without leaking full state |
Instrumented entry points: POST /execute, GET /conversations/{id}/stream,
workspace assessment (GET /workspaces/{id}/stream), and the scoped agent
(POST /workspaces/{id}/items/{idx}/agent).
R34 entity-layer child spans (orchestration/entity_spans.py): the entity
resolution / context-assembly / HITL pipelines emit child_span observations that
nest under the agent-run span, so an auto-select decision and the full HITL flow
are reconstructable from the trace alone. They carry the internal, precise
values (the audit audience, not the LLM) and are no-ops when tracing is off. All
fire orchestration-side — the knowledge-graph service is untraced and OTEL
context doesn’t cross the HTTP boundary, so KG returns the facts (the
resolution_trace block on the resolve response) and the orchestration tool
wrappers + gate/guard emit the spans.
| Span | When | Key attributes |
|---|---|---|
entity_resolve.exact_match | every resolve | match_found, query, entity_types_scope |
entity_resolve.trigram_match | every resolve | top_name_similarity (the calibration signal), candidates_returned |
entity_resolve.proximity_score | resolve w/ candidates | top_proximity_score, context_entity_ids_count |
entity_resolve.composite_score | resolve w/ candidates | name_component / proximity_component / type_hint_component (weighted) |
entity_resolve.outcome | every resolve | outcome (auto_selected / asked_user / no_match), confidence |
context_assemble.walk | context_assemble | entity_id, depth, entities_traversed |
context_assemble.render | context_assemble | token_estimate, truncation_occurred, relationships_truncated_count |
hitl.gate_fired | mutating-capability gate fires | capability_key, tool_call_id, assistant_message_stripped |
hitl.resume_reconcile | resume_guard reconciles a verdict | verdict (approved / rejected / pending / unreadable), reviewer_note_present |
hitl.resume_replay | approved mutation replayed | replay_succeeded, duplicate_emission_suppressed |
The entity_resolve.outcome span is a separate layer from the
platform:log_entity_resolution rules-audit entry — the spans only wrap the
resolve callsite and never re-fire the on_entity_resolve trigger (one audit
entry + one outcome span per auto-select). An approved-but-unexecuted mutation is
the absence of a hitl.resume_replay span next to an approved approval
record. See docs/specs/phases/R34-phase-6-calibration-and-demo-runbook.md.
Running Locally
cd services/orchestration-engine
poetry install
poetry run uvicorn orchestration.main:app --reload --port 8001Seeding Data
The service includes several seeding scripts:
# Seed checklist templates
poetry run python -m orchestration.seed_checklists
# Seed compliance rules
poetry run python -m orchestration.seed_rules
# Seed demo conversations
poetry run python -m orchestration.seed_conversationsThe orchestration engine requires PostgreSQL to be running for conversation persistence and skill registry access. The memory service and knowledge graph service should be running for full pipeline functionality, but the engine degrades gracefully if they are unavailable.