Last Updated: January 9, 2026 Purpose: Document key architectural decisions and their rationale
Status: Decided (Shared Runtime with future per-team option)
When serving multiple teams, we need to decide how to isolate agent execution:
- All teams share one agent deployment, or
- Each team gets dedicated pods
Start with shared runtime, add per-team pods as premium enterprise feature.
- Simplicity: Shared runtime is easier to operate and monitor
- Cost: Per-team pods multiply infrastructure costs linearly
- Latency: No cold start with shared runtime
- Soft isolation sufficient: Per-request config loading with resource quotas covers 95% of use cases
- Team isolation is configuration-based, not infrastructure-based
- Need rate limiting and quotas per team to prevent noisy neighbors
- Enterprise customers needing hard isolation will need dedicated pods (future)
Status: Decided (consolidate routing in Config Service)
When webhooks arrive (Slack, Incident.io, PagerDuty, GitHub), we need to identify which team should handle them. Currently routing is split:
orchestrator_team_slack_channelstable (Orchestrator)routingJSON in team config (Config Service)/api/v1/internal/routing/lookupendpoint (Config Service)
Consolidate all routing in Config Service. Remove orchestrator_team_slack_channels table.
- Single source of truth: All team config in one place
- Already implemented: Config Service has
/routing/lookupendpoint - Extensible: Routing config supports Slack, Incident.io, PagerDuty, GitHub, services
- Validation: Config Service can enforce uniqueness per-org
- Orchestrator no longer stores Slack mappings directly
- During provisioning, Orchestrator updates routing config via Config Service
- Agent service calls Config Service for routing lookup
- Simpler mental model
Status: Decided (Orchestrator handles all external webhooks)
Webhooks are currently duplicated across services:
- Web UI:
/api/slack/events,/api/github/webhook,/api/pagerduty/webhook - Agent:
/webhooks/slack/events,/webhooks/github,/webhooks/pagerduty,/webhooks/incidentio - Orchestrator:
/api/v1/internal/slack/trigger(internal)
This is a mess with three different patterns.
Orchestrator handles all external webhooks. Web UI and Agent webhook handlers are removed.
Webhook → Orchestrator → Routing (Config Service) → Agent → Audit (Config Service)
| Reason | Explanation |
|---|---|
| Single entry point | One place for all external events |
| Security | All webhook secrets in one service, easy rotation |
| Audit/Compliance | Log every event before execution (SOC2, GDPR) |
| Rate limiting | Prevent abuse, queue if overloaded |
| Separation | "Receive event" ≠ "Execute agent" |
| Routing | Centralized team lookup via Config Service |
┌─────────────────────────────────────────────────────────────────┐
│ Slack │ GitHub │ PagerDuty │ Incident.io │ Custom │
└────────────────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ ORCHESTRATOR │
│ 1. Verify signature (per-source) │
│ 2. Rate limit check │
│ 3. Routing lookup → Config Service │
│ 4. Audit: log incoming event │
│ 5. Trigger Agent run with team context │
│ 6. Agent posts results (Slack/GitHub/etc) │
└─────────────────────────────────────────────────────────────────┘
To Implement:
- Move webhook handlers from Agent to Orchestrator
- Remove Web UI webhook handlers
- Orchestrator needs: Slack, GitHub, PagerDuty, Incident.io signature verification
- Agent exposes simple
/api/v1/runendpoint (no webhooks)
Latency:
- Extra hop adds ~10-50ms
- Acceptable for enterprise requirements (audit, security)
Status: Decided
For an enterprise product, clear separation between control plane and data plane is critical:
- Config Service should be the single source of truth for all team data
- Clients should be able to call Config Service directly for CRUD operations
- Orchestrator should only be needed for infrastructure/coordination
Config Service handles all data operations directly. Orchestrator handles infrastructure and multi-service coordination.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Clients │
│ (Web UI, Admin CLI, External Systems) │
└─────────────────────────────────────────────────────────────────────────────┘
│ │
│ Data Operations │ Infra Operations
│ (direct) │ (workflows)
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Config Service │ │ Orchestrator │
│ (Data Plane) │ │ (Control Plane) │
└─────────────────────────────┘ └─────────────────────────────┘
Clients call Config Service directly for:
- Create/update team nodes
- Set/get team configuration
- Issue/revoke tokens
- Routing lookup
- Audit logs
Orchestrator is called when:
- K8s resources needed (CronJobs, Deployments)
- Multi-service coordination (Config + Pipeline + KB)
- Complex workflows with rollback
- Full provisioning (convenience wrapper)
| Operation | Call | Why |
|---|---|---|
| Create team + config + token | Config Service | Pure data operation |
| Full provisioning with CronJob | Orchestrator | Needs K8s API |
| Update team config | Config Service | Pure data operation |
| Deprovisioning with cleanup | Orchestrator | Multi-service + K8s |
| Routing lookup | Config Service | Runtime data lookup |
- Config Service shouldn't have K8s access - security principle
- Clients shouldn't need Orchestrator for CRUD - simplicity
- Orchestrator adds value only for infrastructure - clear purpose
- Both are stateless - Config Service stores data in Postgres
Status: Decided (Shared Postgres with service-prefixed tables)
Should each service have its own database, or share one?
Shared Postgres database with service-prefixed tables.
| Service | Tables |
|---|---|
| Config Service | org_nodes, node_configurations, team_tokens, org_admin_tokens, agent_runs |
| Orchestrator | orchestrator_team_slack_channels, orchestrator_provisioning_runs |
| AI Pipeline | ai_pipeline_* (future) |
- Simplicity: One RDS instance to manage
- Cost: Fewer database instances
- Transactions: Cross-service queries possible if needed
- Isolation: Table prefixes provide logical separation
- Schema migrations need coordination
- Connection pool shared across services
- Future: May need to split if scale requires it
Status: Decided (Dynamic from Config Service)
How should agents get their configuration (prompts, tools, sub-agents)?
Options:
- Hardcoded in Python classes
- YAML/JSON files in repo
- Dynamic from Config Service per request
Dynamic loading from Config Service via get_planner_for_team().
- Per-team customization: Each team can have different prompts
- Hot reload: Config changes don't require redeploy
- Governance: Config Service handles approvals
- Audit trail: Config changes tracked
from ai_agent.core.config_loader import get_planner_for_team
# Load team-specific agent configuration
planner = get_planner_for_team(org_id="acme", team_node_id="platform-sre")
result = await Runner.run(planner, "Investigate high latency")Status: Proposed
Each team needs periodic AI Pipeline jobs:
- Ingestion (pull from Slack, tickets, etc.)
- Gap analysis (identify missing tools/knowledge)
- Evaluation (test agent performance)
Orchestrator creates K8s CronJobs per team during provisioning.
# Created by Orchestrator on team provision
apiVersion: batch/v1
kind: CronJob
metadata:
name: incidentfox-pipeline-${team_id}
spec:
schedule: "0 2 * * *" # Daily at 2am
jobTemplate:
spec:
containers:
- name: pipeline
image: ${pipeline_image}
env:
- name: TEAM_ID
value: ${team_id}
command: ["python", "-m", "ai_learning_pipeline.scripts.run_orchestrator"]- EventBridge (AWS): Good for serverless, but K8s-native is simpler in EKS
- In-process scheduler: Less observable, harder to manage per-team
- Single shared CronJob: Doesn't scale with many teams
Not yet implemented. Track in orchestrator/docs/MULTI_TENANT_DESIGN.md.
| ADR | Decision | Status |
|---|---|---|
| 001 | Shared agent runtime (per-team pods as premium) | ✅ Decided |
| 002 | All routing via Config Service | ✅ Decided |
| 003 | Agent handles all webhooks | ✅ Decided |
| 004 | Orchestrator = control plane for lifecycle | ✅ Decided |
| 005 | Shared Postgres with service-prefixed tables | ✅ Decided |
| 006 | Dynamic agent config from Config Service | ✅ Decided |
| 007 | K8s CronJobs per team for AI Pipeline | 📋 Proposed |