PRD-107 — Context Interface Abstraction
Version: 1.0 Type: Research + Design Status: Complete — Ready for Peer Review Priority: P1 Dependencies: PRD-102 (Coordinator Architecture), PRD-100 (Master Research) Author: Gerard Kavanagh + Claude Date: 2026-03-15
1. Problem Statement
1.1 The Gap
Phase 3 swaps message-passing for shared semantic fields, but the coordinator (PRD-102) shouldn't know or care which context implementation runs behind the interface. Today, every consumer directly imports ContextService and calls build_context() with a specific ContextMode enum value. There is no abstraction boundary — replacing the context layer means rewriting every caller.
1.2 Direct Coupling Points
Direct ContextService(db) instantiation
smart_orchestrator.py
194
Creates ContextService(self._db_session) directly — no injection, no interface
Direct ContextService(db) instantiation
heartbeat_service.py
439
Same pattern — ContextService(db).build_context(...)
Direct ContextService(db) instantiation
recipe_executor.py
143
Same pattern
Direct ContextService(db) instantiation
routing/engine.py
525
Same pattern — ContextService(self._db).build_context(...)
Direct ContextService(db) instantiation
agent_factory.py
748
Same pattern
Direct ContextService() instantiation
task_decomposer.py
181
Even uses ContextService() with no DB session
Direct ContextService() instantiation
agent_negotiation.py
125
Same pattern
Direct ContextService() instantiation
quality_assessor.py
264
Same pattern
Direct ContextService() instantiation
complexity_analyzer.py
427
Same pattern
Direct ContextService() instantiation
nl2sql/service.py
294
Same pattern
ContextMode enum dependency
modules/context/modes.py:13
13
Callers import and pass specific enum values — adding a mode requires touching callers
ContextResult structure assumption
modules/context/result.py:10
10
Callers destructure .system_prompt, .tools, .messages
SharedContextManager disconnected
inter_agent.py:400
400
Exists but is NOT integrated into ContextService pipeline
10 production callers directly instantiate ContextService. Swapping the backend means changing all 10.
1.3 Why This Matters Now
Without this abstraction:
Phase 3 requires rewriting all 10 callers of
ContextServiceThe coordinator (PRD-102) would be built against a concrete implementation, making Phase 3 a breaking change
No way to A/B test message-passing vs. field-based context (PRD-108 experiment requires running both)
No clean path to inject mission-level shared context into agent prompts
1.4 What This PRD Delivers
A port/adapter boundary between context consumers (coordinator, chatbot, heartbeat, recipe, router, agent factory, orchestrator stages) and context providers (current ContextService for Phase 2, neural field engine for Phase 3). Consumers call the port. The adapter maps to whichever backend is active. Zero consumer changes when Phase 3 ships.
2. Prior Art Analysis
2.1 Architecture Patterns
Hexagonal Architecture (Cockburn 2005)
Port ownership by domain layer, adapter as infrastructure, driving vs. driven ports
Port defined in core/ports/ — coordinator owns the contract. Adapter lives in modules/context/adapters/ — infrastructure layer
Symmetric port model — our ports are asymmetric: ContextProvider is a driven port (coordinator drives it), not a driving port
Clean Architecture (Martin 2017)
Dependency Rule: source code dependencies point inward only. The domain layer never imports from infrastructure
Coordinator imports ContextProvider from core/ports/. Never imports ContextService from modules/context/
Full 4-ring layering — our codebase doesn't have formal ring boundaries. We adopt the dependency direction rule, not the full structure
Strategy Pattern (GoF 1994)
Runtime backend swap via config-driven factory
Factory function create_context_provider(config) selects adapter at startup based on CONTEXT_BACKEND env var
Formal Strategy interface hierarchy — our ABC ports serve the same role without GoF boilerplate
Repository Pattern (Evans 2003)
Collection-like interface over diverse backends
SharedContextPort.query() hides whether backend is Redis dict lookup or Qdrant vector search
Repository-per-aggregate — context isn't a DDD aggregate; the pattern is borrowed, not applied literally
Cosmic Python (Percival & Gregory)
Python-specific ports/adapters with ABC, manual composition root, in-memory test fakes
ABC-based ports, InMemoryContextProvider for testing, factory function as composition root — no DI framework
dependency-injector library — manual factory is sufficient at our scale
2.2 Multi-Agent Framework Context Abstractions
LangGraph
Two-tier separation: BaseCheckpointSaver (thread-scoped state) + BaseStore (cross-thread memory). Both injected at compile() time via constructor
Two ports: ContextProvider (per-agent, per-call) + SharedContextPort (mission-scoped, cross-agent). Separate concerns, separate lifecycles
Sync/async dual API — our codebase is async-first; sync wrapper is unnecessary complexity
AutoGen
Memory ABC with 5 methods: add(), query(), update_context(), clear(), close(). update_context(model_context) is a context preprocessor — it mutates the model's context by injecting memories
The preprocessor concept: both Phase 2 (inject retrieved messages) and Phase 3 (inject field query results) are context preprocessing. But we use immutable returns, not mutation
MemoryContent with MIME types — over-engineered for our use case. Our context is always text/JSON
CrewAI
Hierarchical scopes: memory.scope("workspace/agent/task") restricts operations to a subtree. memory.slice() creates read-only views across scopes
Scope concept for future: workspace → mission → agent context hierarchy maps cleanly to our data model
Composite scoring (semantic + recency + importance) in the port — that's adapter-internal, not port-level
2.3 Context Engineering Theory (from repo chapters 08-14)
Field operations (inject, decay, resonate, attractor, boundary)
SharedContextPort must support inject() and query() — the minimal operations that both message-passing AND field-based backends can implement
Resonance as query
Phase 3 queries via resonance ("what resonates with X?"), not retrieval ("get context for agent X"). The query() method must accept a natural language query, not just a key lookup — so both backends can implement it
Non-commutativity
inject(A); inject(B) ≠ inject(B); inject(A) because attractors formed by A change how B resonates. The interface preserves operation ordering — no batched unordered writes
Decay
Phase 2 uses Redis TTL (2h). Phase 3 uses exponential decay (S(t) = S₀ × e^(-λt)). The interface doesn't expose decay — it's adapter-internal
Boundary permeability
Phase 3 can configure which agents see which field patterns. Phase 2 uses team membership lists. The create_context() method accepts team_agent_ids — both backends enforce access control their own way
2.4 Key Design Decision
Two ports, ABC-based, async-only, constructor-injected via manual factory.
Rationale:
Two ports (not one) because
ContextProviderandSharedContextPorthave different consumers, lifecycles, and Phase 3 implementationsABC (not Protocol) because port contracts are foundational — explicit inheritance catches missing methods at class definition time, not at runtime
Async-only because all backends are I/O-bound and all callers already use
awaitManual factory (not DI framework) because Cosmic Python's pattern works and we have <15 callers to wire
3. Port Interfaces
3.1 Core Port: ContextProvider
ContextProviderLives at orchestrator/core/ports/context.py. Owned by the domain layer.
3.2 Secondary Port: SharedContextPort
SharedContextPortMission-level shared context between agents. Separate from per-agent context.
3.3 What the Interface Does NOT Expose
Section-level control
Callers don't pick sections. The adapter decides based on mode config
Token budget internals
Callers don't set per-section budgets. The adapter's TokenBudgetManager handles it
Resonance/decay parameters
Phase 3 internals — adapter tunes them via its own config
Storage backend (Redis, Postgres, Qdrant)
Invisible to callers — the whole point of ports/adapters
ContextMode enum values
Callers use ContextModeType strings; adapter maps internally
ContextResult dataclass
Adapter-internal — callers see AgentContext only
3.4 Interface Segregation
Consumer
Uses ContextProvider
Uses SharedContextPort
Coordinator (PRD-102)
Yes — builds context for task agents
Yes — manages mission-level shared context
Chatbot (smart_orchestrator.py)
Yes — chatbot context
No — no mission shared context
Heartbeat (heartbeat_service.py)
Yes — heartbeat context
No
Recipe (recipe_executor.py)
Yes — task execution context
No
Router (routing/engine.py)
Yes — router context
No
AgentFactory (agent_factory.py)
Yes — task execution context
No (receives mission context via kwargs)
Orchestrator stages
Yes — orchestrator_stage context
No
Most consumers only need ContextProvider. Only the coordinator needs both. Interface Segregation Principle: don't force agents to depend on shared context methods they don't use.
4. Phase 2 Adapters
4.1 DefaultContextProvider
DefaultContextProviderWraps the existing ContextService (modules/context/service.py:47). Zero behavior change — every existing test continues to pass.
4.2 RedisSharedContext
RedisSharedContextWraps the existing SharedContextManager (inter_agent.py:400). Preserves 2h TTL behavior.
4.3 InMemoryContextProvider — Test Fake
InMemoryContextProvider — Test FakeFor unit testing coordinators without DB/Redis:
5. Factory Function & Composition Root
5.1 Factory
5.2 Config Extension
5.3 Per-Mission Override (for PRD-108 A/B Testing)
The factory accepts an explicit backend parameter. The coordinator can pass a mission-level override:
This enables PRD-108's controlled experiment: specific missions run through the neural field adapter while everything else uses the default.
6. MissionContextSection
New section that injects shared mission context into an agent's system prompt via SharedContextPort.
Register in SECTION_REGISTRY:
Add to mode configs that need it:
7. Migration Path
7.1 Strategy: Incremental, Non-Breaking
The adapter wraps ContextService — ContextService itself is unchanged. Callers migrate one-by-one. Both old and new patterns work simultaneously.
7.2 Migration Steps Per Caller
Each caller follows the same 3-step pattern:
Step 1: Accept ContextProvider via constructor (with default fallback).
Step 2: Replace ContextMode.X enum with string "x".
Step 3: Replace ContextResult type hints with AgentContext.
7.3 Migration Order
Migration order follows dependency criticality — highest-impact callers first:
1
agent_factory.py:748
AgentFactory
All agent execution flows through here — highest blast radius
2
smart_orchestrator.py:194
Chatbot
User-facing — validates the pattern works end-to-end
3
heartbeat_service.py:439
Heartbeat
High-frequency caller — validates performance
4
recipe_executor.py:143
Recipe
Task execution path
5
routing/engine.py:525
Router
Routing path
6-10
Orchestrator stages
Various
Less critical — only active during orchestrated execution
7.4 Backward Compatibility
During migration, ContextResult is NOT removed — it becomes adapter-internal. Callers that haven't migrated continue to work. AgentContext has the same fields as ContextResult plus metadata:
ContextResult field
AgentContext field
Change
system_prompt
system_prompt
Same
messages
messages
Same
tools
tools
Same
tool_choice
tool_choice
Same
mode
mode
Same
sections_included
sections_included
Same
sections_trimmed
sections_trimmed
Same
token_estimate
token_estimate
Same
token_budget
token_budget
Same
memory_context
memory_context
Same
user_name
user_name
Same
preparation_time_ms
preparation_time_ms
Same
—
metadata
New — extensible dict for adapter-specific data
8. Phase 3 Adapter Skeleton
Stub implementation for PRD-108+ to fill in:
Why the interface holds for Phase 3:
build_context(mode, agent, ...)
12 sections + budget manager → system prompt
Field query by agent pattern → resonant context assembly
inject(context_id, key, value, ...)
Redis HSET with 2h TTL
Neural field injection with strength, triggers attractor formation
query(context_id, query, ...)
Dict key-value lookup, return all
Resonance measurement, return ranked results
create_context(team_ids, ...)
Allocate Redis key + in-memory dict
Create field with boundary config for team
destroy_context(id)
Delete Redis key + dict entry
Trigger field decay + explicit cleanup
Same 5 methods. Different backends. Zero coordinator changes.
9. Module Hierarchy
9.1 New Files
9.2 Dependency Direction
Source code dependencies point inward only: adapters/ → ports/ (never ports/ → adapters/). The coordinator imports from core/ports/, never from modules/context/.
10. Cross-PRD Integration
PRD-102 (Coordinator)
Coordinator receives ContextProvider + SharedContextPort via constructor. Uses mode="coordinator". Creates mission shared context via create_context()
PRD-102 consumes → PRD-107 provides
PRD-103 (Verification)
Verifier uses ContextProvider with mode="verifier". New mode added to ContextModeType and MODE_CONFIGS
PRD-103 consumes → PRD-107 provides
PRD-104 (Ephemeral Agents)
Contractors receive context through same ContextProvider — no special path. Shared context entries survive contractor destruction
PRD-104 consumes normally
PRD-105 (Budget)
Budget enforcement can wrap ContextProvider as a decorator — check budget before build_context(), not inside it
PRD-105 decorates → PRD-107 core
PRD-106 (Telemetry)
AgentContext.token_estimate and sections_trimmed are first-class telemetry fields. metadata dict carries additional observability data
PRD-106 reads → PRD-107 produces
PRD-108 (Memory Field)
Neural field adapter implements both ports. PRD-108 experiment tests whether field-based context outperforms message-passing — same interface, different backend
PRD-108 implements → PRD-107 defines
11. Risk Register
1
Over-abstraction — interface too generic to be useful
High
Medium
Start with the narrowest interface that satisfies the coordinator. 5 methods total across both ports. Expand only when Phase 3 demands it
2
Leaky abstraction — backend-specific types leak through metadata
Medium
High
Strict rule: metadata values are string/int/float/bool only. No ContextResult, no SharedContext, no VectorSearchResult
3
Phase 3 requirements break the interface
High
Medium
PRD-108 prototype IS the validation gate. If it can't implement the ports, the interface is updated before Phase 3 PRDs
4
Performance regression from adapter indirection
Low
Low
Adapter is one function call + one dataclass construction. Sub-microsecond overhead. Context assembly (100ms+) dominates
5
Migration disruption — changing all 10 callers at once
Medium
Medium
Incremental migration: adapter wraps ContextService (unchanged), callers migrate one-by-one, old pattern continues to work
6
MissionContextSection token budget competes with critical sections
Medium
Medium
Priority 3 (after identity/task, before skills). Max 2000 tokens. Budget manager drops it before identity or conversation
7
Two ports increase wiring complexity
Medium
Low
Single factory function creates both. Coordinator receives both from same factory call. Test fakes implement both in one import
12. Acceptance Criteria
Must Have
Should Have
Nice to Have
Appendix A: Research Sources
Alistair Cockburn, "Hexagonal Architecture" (2005)
Port ownership by domain, adapter as infrastructure, driving vs. driven ports
Robert C. Martin, Clean Architecture (2017)
Dependency Rule (always inward), boundary anatomy
Gamma et al., Design Patterns (1994)
Strategy Pattern for runtime backend swap
Eric Evans, Domain-Driven Design (2003)
Repository Pattern adapted for context access
Harry Percival & Bob Gregory, Cosmic Python (cosmicpython.com)
Python ABC-based ports, in-memory test fakes, manual composition root
LangGraph (langchain-ai/langgraph)
Two-tier separation: BaseCheckpointSaver + BaseStore, constructor injection
CrewAI (crewAIInc/crewAI)
Hierarchical scopes, StorageBackend protocol
AutoGen (microsoft/autogen)
Memory ABC (5 methods), update_context() preprocessor pattern
Context Engineering repo, chapters 08-10, 14
Field operations catalog, resonance as query primitive, non-commutativity constraint
Automatos ContextService (modules/context/service.py:47)
8 modes, 12 sections, ContextResult dataclass, TokenBudgetManager
Automatos SharedContextManager (inter_agent.py:400)
In-memory + Redis, 2h TTL, merge strategies, team access control
PEP 544 — Python Protocols
Structural typing alternative; decided against for critical ports
Appendix B: ContextMode Enum Values (Current)
From modules/context/modes.py:13:
New modes added by PRD-107 ContextModeType: COORDINATOR, VERIFIER.
Last updated

