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

Coupling Point
File
Line
Problem

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 ContextService

  • The 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

Pattern
Source
What We Adopt
What We Reject

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

Framework
Key Insight
What We Adopt
What We Reject

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)

Concept
Implication for Interface

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 ContextProvider and SharedContextPort have different consumers, lifecycles, and Phase 3 implementations

  • ABC (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 await

  • Manual 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

Lives at orchestrator/core/ports/context.py. Owned by the domain layer.

3.2 Secondary Port: SharedContextPort

Mission-level shared context between agents. Separate from per-agent context.

3.3 What the Interface Does NOT Expose

Hidden Detail
Why

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

Wraps the existing ContextService (modules/context/service.py:47). Zero behavior change — every existing test continues to pass.

4.2 RedisSharedContext

Wraps the existing SharedContextManager (inter_agent.py:400). Preserves 2h TTL behavior.

4.3 InMemoryContextProvider — Test Fake

For 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 ContextServiceContextService 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:

Priority
File
Caller
Why This Order

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:

Operation
Phase 2 Implementation
Phase 3 Implementation

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
Integration
Direction

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

#
Risk
Impact
Likelihood
Mitigation

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

Source
What It Informed

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