Skip to content

Latest commit

 

History

History
416 lines (305 loc) · 21.7 KB

File metadata and controls

416 lines (305 loc) · 21.7 KB

Farms — the control plane

Farms are the connective tissue of CortexObserver. Each is a managed AI service with its own LLM, prompts, skills, and human control UI. Agents consume Farms; humans govern them.

A Farm is not a resource pool. It is a governed boundary through which a whole category of capability — tools, models, memory, knowledge, budgets — is delivered to agents and observed by humans. Every Farm carries three dimensions at once:

Dimension What it means
Service The capability it provides — tool execution, model routing, memory, retrieval, allocation
Enforcement The constraints it imposes — grants, risk budgets, model tiers, quotas, prerequisite checks
Observability The truth it surfaces to humans — spend, usage, risk consumption, freshness, timelines

The key insight: enforcement is structural, not prompt-dependent. An agent cannot talk its way past a budget check or a tool grant. The code says no. That is what closes the circular loop — humans define the rules in a Farm's UI, the Farm enforces them in code, agents work within them, and the results flow back as live truth.

graph TD
    subgraph Agents
        A1["@allen"]
        A2["@amy"]
        A3["@charles"]
        A4["@alice"]
        A5["@chat"]
    end

    subgraph Farms["FARMS · managed AI services"]
        MCP["🔧 MCPFarm<br/>100+ tools · risk scoring"]
        GW["🧠 LLM Gateway<br/>LiteLLM · cost · EOL governance"]
        MEM["💾 Memory Farm<br/>L1–L4 · consolidation"]
        KN["📖 Knowledge Farm<br/>Qdrant hybrid RAG"]
        ALLOC["💰 Allocation<br/>budgets · grants · quotas"]
    end

    A1 --> MCP
    A1 --> GW
    A1 --> MEM
    A2 --> GW
    A2 --> MEM
    A3 --> GW
    A3 --> KN
    A4 --> KN
    A4 --> GW
    A5 --> GW
    A5 --> MEM

    ALLOC -. "enforces limits" .-> MCP
    ALLOC -. "enforces limits" .-> GW
    ALLOC -. "enforces limits" .-> MEM
    ALLOC -. "enforces limits" .-> KN

    style MCP fill:#2d6a4f,stroke:#1b4332,color:#fff
    style GW fill:#1d3557,stroke:#0d1b2a,color:#fff
    style MEM fill:#6a040f,stroke:#370617,color:#fff
    style KN fill:#7b2d8e,stroke:#4a0e5c,color:#fff
    style ALLOC fill:#b5651d,stroke:#7a3b10,color:#fff
Loading

The five Farms at a glance:

Farm What it manages The LLM inside Human control plane
MCPFarm 100+ tools across 17 servers; pre-execution risk scoring + authorization — (deterministic gate) Tool grants, server health, invocation history, playground
LLM Gateway LiteLLM routing, multi-provider model registry, tiers, failover, cost Routes every model call Model lifecycle (active → deprecated → EOL), per-agent budgets, usage analytics
Memory Farm Four-tier agent memory (L1 Redis → L2 Postgres → L3 snapshots → L4 procedural) Haiku-class consolidation Cross-agent Memory Explorer with temporal search; consolidation; quotas
Knowledge Farm Agent-namespaced RAG, Qdrant hybrid BM25 + dense + RRF Embedding + extraction Sources, collections, document lifecycle, retrieval testing
Allocation Per-agent budgets, risk budgets, tool grants, memory/knowledge quotas — (shared state) The enforcement table — checked before every tool call, LLM invocation, memory write

1. MCPFarm — tool execution

The only way agents touch the outside world. 100+ tools across 17 logical servers, each scored by risk and gated by authorization before it can fire.

File root: backend/src/cortex/mcpfarm/

Agents do not implement tools. They call tool_executor.execute(server, tool, params) and MCPFarm handles everything downstream — DB persistence, error handling, WebSocket events, and audit logging. That single chokepoint is what makes governance enforceable: there is no side door.

MCPFarm — 17 servers, 103 tools, live health MCPFarm dashboard — registered servers, tool inventory, and per-server health at a glance.

Risk scoring

Every tool carries a numeric risk score reflecting its blast radius (backend/src/cortex/mcpfarm/risk_scoring.py):

Risk level Score Examples
Zero 0 echo.echo, calc.add (dev tools)
Low 1–5 governance.search, identity.lookup, ssm.get_parameter
Medium 10–15 identity.create, skills.approve, ssm.put_parameter
Medium-high 20 comms.send_email, comms.send_whatsapp
High 30–50 bootstrap.deploy_stack (50), bootstrap.rollback (50)

Unknown tools default to 5. A deploy costs 50 risk points; a parameter read costs 1. The score is the currency of the daily risk budget.

Pre-execution authorization

Before any tool fires, the authorization gate runs four checks in order (backend/src/cortex/mcpfarm/authorization.py):

sequenceDiagram
    participant Agent
    participant Gate as Authorization Gate
    participant Exec as Tool Executor
    participant Alloc as Allocation

    Agent->>Gate: execute(server, tool, params)
    Note over Gate: 1. GRANT — tool in mcp_allowed_tools?
    Note over Gate: 2. RISK BUDGET — daily points remaining?
    Note over Gate: 3. PREREQUISITES — deploy params resolvable?
    Note over Gate: 4. CIRCUIT BREAKER — if exhausted, read-only (score ≤ 2)
    alt any gate fails
        Gate-->>Agent: DENY (with reason)
    end
    Gate->>Exec: PERMIT — run tool
    Exec->>Alloc: Commit risk spend
    Exec-->>Agent: Result + audit log
Loading

This is a Reserve-Commit-Rollback pattern. The gate reserves by checking all four conditions; the tool executes only on permit; the spend is committed to the daily risk budget after execution; and on failure, risk points are not refunded — the attempt still cost something.

Deploy guardrails — the structural fix

For bootstrap.deploy_stack, the gate loads the target template's CloudFormation Parameters section, identifies required parameters without defaults, and checks whether they exist in SSM or were supplied. If VpcId is required but unavailable, the gate blocks before any AWS call:

Cannot deploy 'Hello World': missing required parameters.
  - VpcId: Deploy Foundation VPC first
  - SubnetIds: Deploy Foundation VPC first (provides subnets)

This is structural — it catches the problem even if the LLM hallucinates that the VPC exists.

The human control plane

  • Tool grants — which agents can call which tools, edited per agent
  • Server health — live status across all 17 servers
  • Risk score editor — adjust per-tool blast-radius scores
  • Usage heatmap — tools × time, colored by invocation volume
  • Playground — invoke any tool by hand, inspect the full request/response and audit trail

2. LLM Gateway — model routing

Every model call in CortexObserver routes through here. LiteLLM provider abstraction, a DB-backed multi-provider model registry, three tiers, failover chains, cost tracking, and full model end-of-life governance.

File: backend/src/cortex/gateway/llm.py

The Gateway exposes one invoke_llm() function that every agent and subsystem calls. Internally it routes through LiteLLM for provider abstraction, failover, and cost calculation — but not before it checks governance.

LLM Gateway — model registry with end-of-life governance The model registry — providers, tiers, pricing, and lifecycle status (active → deprecated → EOL) managed without a code change.

Governance checks — before every call

_check_llm_governance() runs two gates ahead of any invocation:

  1. Budget check — if the agent's llm_current_spend_usd >= llm_monthly_budget_usd, the call is refused. No more LLM calls until the budget resets or a human raises it.
  2. Model tier enforcement — if the agent's llm_allowed_models list is non-empty and the requested model is not on it, the call is refused. A fast-tier agent cannot reach for a reasoning model.

After the call, _record_agent_spend() deducts the calculated cost from the agent's allocation.

flowchart LR
    REQ["Agent request"] --> BUDGET{"Budget<br/>check"}
    BUDGET -- over limit --> DENY["DENY · $$$ exhausted"]
    BUDGET -- ok --> TIER{"Model tier<br/>check"}
    TIER -- not allowed --> DENY2["DENY · model restricted"]
    TIER -- ok --> LITE["LiteLLM<br/>router"]
    LITE --> ANTH["Anthropic"]
    LITE --> OAI["OpenAI · others"]
    ANTH --> RESP["Response"]
    OAI --> RESP
    RESP --> SPEND["Record spend<br/>update allocation"]

    style REQ fill:#264653,stroke:#1d3440,color:#fff
    style BUDGET fill:#2a9d8f,stroke:#1e7268,color:#fff
    style TIER fill:#2a9d8f,stroke:#1e7268,color:#fff
    style LITE fill:#1d3557,stroke:#0d1b2a,color:#fff
    style ANTH fill:#e76f51,stroke:#a84b37,color:#fff
    style OAI fill:#457b9d,stroke:#2e5468,color:#fff
    style RESP fill:#264653,stroke:#1d3440,color:#fff
    style SPEND fill:#b5651d,stroke:#7a3b10,color:#fff
    style DENY fill:#9b2226,stroke:#641416,color:#fff
    style DENY2 fill:#9b2226,stroke:#641416,color:#fff
Loading

Tiers, not model names

Agents request capability by tier, never by hard-coded model string. models("fast") resolves to the current default fast-tier model; the registry decides what that is right now.

Tier Intent Typical use
reasoning Deepest, most capable Deploy plans, multi-agent debate, complex synthesis
balanced Capable + cost-aware Most day-to-day agent work
fast Cheap, low-latency Classification, consolidation, routine turns

The model registry — and end-of-life governance

All model configuration lives in the llm_models table — nothing is hardcoded. The runtime keeps an in-memory cache (refresh_model_cache()) rebuilt at startup and after every mutation.

Field Purpose
model_id Unique identifier (e.g. claude-sonnet-4-20250514)
provider Vendor — anthropic, openai, bedrock, vertex
tier reasoning · balanced · fast
is_default_for_tier One default per tier + provider
pricing_input/output USD per 1M tokens — drives cost tracking
litellm_model_string LiteLLM routing format
status Lifecycle: activedeprecateddisabled
failover_priority Ordering within the provider failover chain
deprecated_by Successor model_id for the deprecation chain

When a vendor announces a model's end of life, a human walks it through the lifecycle in the UI — mark it deprecated, point deprecated_by at its successor, then disable it on the cutoff date. Failover chains and tier defaults re-resolve automatically. No code change, no redeployment — the registry is the source of truth, and the governance is data.

The human control plane

  • Usage tab — invocation metrics, provider breakdown, per-agent budget bars
  • Model Registry tab — register, edit pricing, deprecate, disable models
  • Tier management — assign models to reasoning / balanced / fast
  • Threshold alerts — green < 70%, amber 70–90%, red > 90%

3. Memory Farm — time-indexed cognition

Agents remember — in tiers, indexed by UTC time, consolidated by an LLM, scoped per agent. Humans watch it all through the cross-agent Memory Explorer.

File root: backend/src/cortex/memory/

Four tiers, plus global

Tier Scope Storage Lifecycle
L1 SESSION Redis TTL-based, cleared per session
L2a EPISODIC PostgreSQL Timestamped event logs, append-only
L2b SEMANTIC PostgreSQL Distilled facts/knowledge, mergeable
L3 SNAPSHOT PostgreSQL Aggregated summaries for longitudinal reasoning
L4 PROCEDURAL PostgreSQL Learned tool-use patterns
GLOBAL PostgreSQL Read-only shared context, admin-written

Every entry (backend/src/cortex/memory/schemas.py) is anchored on observed_atwhen the event occurred (UTC) — the universal time index. It also carries recall_window_hours (0 = permanent), source_agent_id (enabling cross-agent sharing), a structured category, tags, and scope.

Consolidation — episodic into semantic

The consolidation engine (backend/src/cortex/memory/consolidation.py) distills raw episodic events into structured semantic knowledge, using a haiku-class model for cost efficiency.

flowchart TB
    L1["L1 Session<br/>Redis · TTL"] --> L2A["L2a Episodic<br/>Postgres · timestamped events"]
    L2A -->|"LLM (fast tier)<br/>structured extraction"| L2B["L2b Semantic<br/>Postgres · distilled facts"]
    L2B --> L3["L3 Snapshot<br/>Postgres · aggregated summaries"]
    L3 --> L4["L4 Procedural<br/>Postgres · learned patterns"]

    L2A -.-> EP["Recent episodic"]
    EP --> LLM["LLM extraction"]
    LLM --> CAT["6 categories:<br/>profile · preferences · entities<br/>events · cases · patterns"]
    CAT --> DEDUP["Dedup &<br/>merge/append"]
    DEDUP --> L2B

    style L1 fill:#e63946,stroke:#a4262c,color:#fff
    style L2A fill:#457b9d,stroke:#2e5468,color:#fff
    style L2B fill:#1d3557,stroke:#0d1b2a,color:#fff
    style L3 fill:#2d6a4f,stroke:#1b4332,color:#fff
    style L4 fill:#6a040f,stroke:#370617,color:#fff
    style LLM fill:#e9c46a,stroke:#b8972e,color:#000
    style CAT fill:#2a9d8f,stroke:#1e7268,color:#fff
    style DEDUP fill:#264653,stroke:#1d3440,color:#fff
Loading

Extraction sorts memories into six categories. Four are mergeable — updated in place when the same key recurs: profile, preferences, entities, patterns. Two are non-mergeable — append-only, every entry distinct: events and cases. Consolidation runs per agent or across all agents:

from cortex.memory.consolidation import consolidate_agent_memory
summary = await consolidate_agent_memory("allen", window_hours=24)

Recall windows match the pace of each agent's domain — @allen at 72h (infrastructure is slow), @becky at 168h (identity changes are rare), @brian at 24h (code generation is session-focused).

The human control plane — Memory Explorer

The Memory Explorer is a cross-agent, temporal view of cognition. Pick any agent, scrub a time range, and watch memories render on a timeline grouped by hour or day.

Memory Explorer — cross-agent temporal timeline Memory Explorer — temporal search across agents, with scope filters and a per-entry inspector.

  • Agent selector — inspect any agent's memory
  • Temporal timeline — memories indexed and grouped by time
  • Scope filter — SESSION / EPISODIC / SEMANTIC / PROCEDURAL / SNAPSHOT
  • Consolidation trigger — run a snapshot on demand
  • Memory inspector — expand any entry for its full value, tags, and provenance

4. Knowledge Farm — governed RAG

Agent-namespaced retrieval-augmented generation. Each agent gets its own Qdrant collection; retrieval is hybrid — BM25 + dense vectors fused by RRF; every document moves through a governed lifecycle.

File root: backend/src/cortex/knowledge/

Each agent owns a collection (knowledge_{agent_id}); a __global__ collection is readable by all agents and writable only by admin. KnowledgeStore (backend/src/cortex/knowledge/store.py) provides ingest, retrieve, and delete.

Knowledge Farm — governed RAG, 42 docs / 1281 chunks Knowledge Farm — sources, collections, document lifecycle, and corpus stats per agent.

Ingestion — quota-gated

flowchart LR
    DOC["Document<br/>submitted"] --> QUOTA{"Quota check<br/>AllocationEnforcer"}
    QUOTA -- over quota --> DENY["DENY · corpus full"]
    QUOTA -- ok --> CHUNK["Chunk<br/>fixed · semantic · paragraph"]
    CHUNK --> EMBED["Embed<br/>text-embedding-3-small · 1536d"]
    EMBED --> QDRANT["Qdrant upsert<br/>dense + BM25"]
    QDRANT --> TRACK["Track metadata<br/>knowledge_documents"]

    style DOC fill:#7b2d8e,stroke:#4a0e5c,color:#fff
    style QUOTA fill:#2a9d8f,stroke:#1e7268,color:#fff
    style CHUNK fill:#264653,stroke:#1d3440,color:#fff
    style EMBED fill:#e9c46a,stroke:#b8972e,color:#000
    style QDRANT fill:#1d3557,stroke:#0d1b2a,color:#fff
    style TRACK fill:#457b9d,stroke:#2e5468,color:#fff
    style DENY fill:#9b2226,stroke:#641416,color:#fff
Loading

The AllocationEnforcer.check_knowledge_quota() gate runs first — checking knowledge_max_documents and knowledge_max_corpus_mb — before a single chunk is embedded.

Hybrid retrieval — BM25 + dense + RRF

Retrieval does not rely on vectors alone. Qdrant runs a dense semantic search and a BM25 lexical search in parallel, then fuses the two ranked lists with Reciprocal Rank Fusion (RRF) — combining semantic recall with exact-term precision. Results return to the agent with scores and provenance attached.

Source registry and lifecycle

The Farm tracks where data comes from (backend/src/cortex/models/knowledge_source.py). A KnowledgeSource records source_type (s3, directory, git_repo, nosql, api), uri, owner_agent_id, status, retention_policy, and scan_interval_hours (0 = manual, >0 = auto-scan). A VectorizationConfig records how to process it — path_filter, embedding_model, chunk_strategy, chunk_size (512 default), chunk_overlap (50 default).

Documents move through a governed lifecycle: active (vectors current) → stale (source changed, vectors outdated) → re-embed back to activearchived (vectors removed, metadata kept) → decommed (fully removed).

The human control plane

  • Source registry — list / add / edit / decommission data sources
  • Collections — per-agent corpora and the shared __global__ collection
  • Document browser — docs by agent, source, and status, with lifecycle badges
  • Vectorization status — pending / embedding / synced / stale per document
  • Retrieval playground — query and see hybrid results with scores and provenance
  • Stats — docs per agent, chunks per collection, freshness distribution

5. Allocation — cross-cutting resource governance

The shared state every other Farm reads before it acts. One record per agent; the single source of truth for budgets, risk, grants, and quotas.

File: backend/src/cortex/models/allocation.py

The AgentAllocation model is the hub of governance state. The AllocationEnforcer is consulted before every tool call, every LLM invocation, and every memory or knowledge write — it is the table the other four Farms check.

Allocation — per-agent budget and grant table Allocation — per-agent spend vs budget, risk consumption, tool grants, and quotas in one enforcement view.

What it governs

Dimension Fields Enforced by
LLM budget llm_monthly_budget_usd, llm_current_spend_usd, llm_allowed_models LLM Gateway (_check_llm_governance)
Risk budget risk_budget_daily, risk_current_daily MCPFarm authorization
MCP grants mcp_allowed_tools ([], ["*"], or a specific list) MCPFarm authorization
Memory quotas mem_max_session_entries, mem_max_agent_entries, current counts Memory Farm
Knowledge quotas knowledge_max_documents, knowledge_max_corpus_mb, current count Knowledge Farm ingest

Grant semantics

  • mcp_allowed_tools: []no tools (agent cannot use MCPFarm)
  • mcp_allowed_tools: ["*"]all tools (unrestricted)
  • mcp_allowed_tools: ["governance.search", "identity.lookup"]specific grants

Fail-open by design

When no allocation record exists for an agent, all checks pass — fail open. This preserves compatibility with agents that predate the Allocation system. The moment a record is created, enforcement goes live.

The human control plane

  • Budget overview — per-agent bar chart, spent vs budget
  • Threshold alerts — green < 70%, amber 70–90%, red > 90%
  • Grant manager — per-agent tool grant CRUD
  • Model tier editor — allowed models per agent
  • Reset controls — monthly spend reset, quota adjustment
  • History chart — spend over time per agent

How the Farms interoperate

A single agent action can touch every Farm in sequence — each enforcing its own slice, all reading and writing the same Allocation state:

@allen receives: "Deploy Hello World to dev"
   │
   ├─▶ Memory Farm   recall(window_hours=72) — "what have I deployed recently?"
   ├─▶ MCPFarm       iac.search_templates    — grant check + risk −1
   ├─▶ MCPFarm       ssm.list_parameters     — grant check + risk −1
   ├─▶ LLM Gateway   invoke_llm("analyze")   — budget + tier check · records $0.003
   ├─▶ LLM Gateway   invoke_llm("plan")      — same governance checks
   ├─▶ MCPFarm       bootstrap.deploy_stack  — grant + risk −50 + prerequisite check
   │                                            VpcId in SSM? YES → proceed
   ├─▶ Memory Farm   write episodic(...)      — quota check · session < max
   └─▶ Allocation    all spend/quotas updated across every dimension

Allocation is the bridge. Each Farm reads from it to enforce, and writes back to it after consuming resources — and the human sees every one of those numbers move in the dashboards. That is the circular loop, made of code.


Related

  • ARCHITECTURE.md — the circular loop, Humans/Agents/Farms, A.T.O.M, time as the universal index
  • GOVERNANCE.md — Policies/Procedures/Standards, the Skills Store, Identity Store, risk & approval
  • AGENTS.md — the repeatable agent pattern and the roster that consumes these Farms