Pain Points → Cordis Remedies (Phase 0, Step 6)
Source: Audit P0–P10 (AppState god-struct through header usage tracking) + docs/cordis-mapping.md primitives.
Rule: One-line remedy per pain point — the Cordis primitive that eliminates it.
| ID | Pain Point (at e4f3bcc) | Cordis Remedy |
|---|---|---|
| P0 | AppState god-struct (17–22 fields, src/lib.rs:230–274, clone cost, base_router wiring) | Decomposed Context with typed provide/inject; handlers declare ctx.get::<T>() coeffects instead of receiving the whole state. type AppState = Arc<Context> shim for 1 commit, then State<Arc<Context>>. |
| P1 | cfg(feature) soup (6 #[cfg(postgres)] fields in ConfigurableAgent, #[cfg(mcp)], #[cfg(search-tools)] in handlers/main.rs) | Feature-gated Service implementations registered via inventory/linkme (#[cfg(feature = "postgres")] only on the impl Service for PostgresService block); handlers use if ctx.get::<PostgresService>().is_some() / ctx.get::<LlmService>().check() not #[cfg] in bodies. Cargo.toml keeps feature flags for dep selection, business logic does not. |
| P2 | Duplicated agent execution (5 call-sites: chat.rs:execute_agent, v1.rs:v1_chat, scheduler.rs:execute_scheduled_agent, pipeline_engine.rs:execute_target_agent, trigger_engine.rs:execute_triggered_agent) | Single AgentExecutionService::execute(req, ctx) that owns: history loading, memory/context_provider injection, ToolCoordinator loop, fallback LLM chain, observability sink (run_history + agent_runs), usage/cost/budget, loop detection, checkpointing. All 5 sites become ctx.get::<AgentExecutionService>().execute(...). Eliminates 5× drift. |
| P3 | 3-tier agent resolution triplication (resolve_agent_for_tenant / resolver.rs / registry.rs with tenant DB → community → system) | AgentResolverService::resolve(name, tenant) -> Result<Arc<dyn Agent>> with ordered CoEffect providers; callers inject it once. Agents themselves become Services created by resolver, not by scattered AgentRegistry::create_agent. Supports ctx.isolate("agent", tenant_label) scoping. |
| P4 | 3 registries: ToolRegistry (static HashMap) / RuntimeToolRegistry (ArcSwap<HashMap> from DB) / McpRegistry (external MCP clients) | Unified ToolService trait behind one ToolRegistry iface: resolve(name, tenant), list(tenant), reload(). Composes sources with precedence tenant runtime → fleet runtime → MCP bridge → static. Agents inject it; ctx.isolate("tool_service", tenant_label) enforces per-tenant visibility. Delete execute_for_tenant branching. |
| P5 | 110 KB toml_config.rs (monolith, crates/ares-config/src/toml_config.rs + toon_config.rs) | Split by domain (server, auth, providers, tools, agents, workflows, rag, billing) each as its own Service with schema; re-aggregated via Loader EntryTree. No silent API break — public API re-exported with #[deprecated] for one release if crate merged. |
| P6 | Sequential orchestrator (src/agents/orchestrator.rs runs subtasks serially) | Parallel subtask execution via tokio::JoinSet behind same OrchestratorAgent interface. The interface is unchanged; internals swap for subtask in tasks { subtask.run() } for JoinSet::spawn + join_all with timeout/bail semantics from Dispatch::Parallel vs Serial. |
| P7 | Manual wiring (run_server 17 sequential steps in src/main.rs:296–889) | Context::plugin(plugin, config) registration; run_server becomes root_ctx.plugin(...).await.plugin(...).await (5–8 lines). RegistryService tracks FiberId → Fiber and enforces single-source discipline ("duplicate provider for <TypeId>"). Static plugins via inventory/linkme, dev HMR behind #[cfg(feature = "hmr")]. |
| P8 | MCP server HTTP loopback (MCP server calls reqwest to localhost:3000/api/chat to execute agents) | Direct ctx.get::<AgentExecutionService>().execute(req, &ctx) — eliminate reqwest hop. MCP server becomes a Service that injects AgentExecutionService; latency improvement, no behavioral regression; proves Context is process-scoped, not HTTP-scoped. |
| P9 | No circuit breaker (ClientPool in crates/ares-llm/src/pool.rs has no Closed/Open/HalfOpen) | Wrap ClientPool with breaker state as Service field (threshold, cooldown); Service::check() returns false when breaker Open, causing dependent fibers to deactivate gracefully (Thm 63 guarded withdrawal — provider does not withdraw until dependents Inactive). Expose health_metrics_job through breaker state. |
| P10 | Header-based usage tracking (X-Usage-* headers parsed ad-hoc in src/middleware/usage.rs + track_usage) | Request-scoped UsageContext Service injected into handlers; middleware provides it per request (ctx.extend().provide(UsageContext::from_headers(req.headers()))), handlers inject it via ctx.get::<UsageContext>() and pass to AgentExecutionService for record_usage. No global mutation, testable in isolation. |
Notes
- P0–P10 numbering follows the audit that produced the plan; if audit re-runs show different IDs, keep this table's content and update IDs.
- Each remedy is a
Servicedecomposition (Phase 2–5) — no#[cfg]in handler bodies by Phase 6. - The Loader (
EntryTree) is the mechanism that makes P5 and P7 composable declarative config rather than imperative wiring. - P8 and P9 are the smallest vertical slices to prove DI before tackling
AppStatedecomposition (P0) — P8 validatesAgentExecutionServicedirectly, P9 validatesService::check()health.