Files
OpenOPC/opc/layer1_perception/context_loader.py
T
LZH-YS1998 6fc5ad6be9 feat(runtime): replace per-round history trimming with threshold-triggered LLM compaction
Align native context management with the Claude Code / Codex model:
entry-capped tool results, history frozen below the threshold, one
high-quality summary at the wall — instead of the old pipeline that
microcompacted old messages from 60% usage and hid everything past 40
messages behind a snip marker with no summary.

- context pipeline: history below the hard threshold is never rewritten
  (model quality and prompt-cache prefixes depend on byte-identical old
  messages); the 60% tool-aware microcompact and the 40-message history
  snip move to an emergency-only fallback used under overflow pressure
  when the summarizer is unavailable or circuit-broken.
- durable compaction (was a stub): at usage >= context_guard.hard_threshold
  (now 0.90, soft_threshold removed) the old span is folded into a
  9-section summary via the new HistoryCompactor.summarize_runtime_history,
  keeping the system head, the seed user request verbatim on every round
  (injected session-memory/artifact messages shift the stale
  base_prefix_len, so the fold start is structure-aware), and a
  pairing-safe recent tail. A previous summary stays foldable, so exactly
  one summary exists at a time and rounds chain.
- token accounting anchors on the provider-reported prompt size of the
  latest request (max with the local estimate).
- reactive_compaction.circuit_breaker_failures (previously unread) now
  stops repeated summarizer failures; provider overflow errors retry
  through the same pipeline, summary-first.
- tool-result budget clip keeps head and tail instead of tail-chopping.
- chat-side transcripts get the same treatment: new
  MemoryManager.maybe_compact_session_history wires the threshold-gated
  maybe_compact_session into secretary, office_ui dispatcher, and
  context_loader before prompt building, closing the unbounded-growth
  path; dead no-op compactor entries (maybe_compact_after_message,
  should_compact_prompt) removed.

Verified by 13 new tests (history sanctity below threshold, multi-round
single-summary/seed-verbatim/chain invariants, breaker, emergency
fallback, provider-overflow end-to-end recovery) plus a live-provider
probe: multi-round compaction with the model completing correctly from
summarized context. Full suite: 1859 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 00:04:42 +08:00

136 lines
5.7 KiB
Python

"""Context loader — assembles all relevant context before routing."""
from __future__ import annotations
from typing import Any
from opc.database.store import OPCStore
from opc.layer5_memory.memory_manager import MemoryManager
from opc.layer5_memory.preference import PreferenceManager
from opc.layer5_memory.secretary_policy import SecretaryPolicyManager
from opc.layer5_memory.capability_manager import CapabilityManager
from opc.layer5_memory.skill_library import SkillLibrary
from opc.layer3_agent.adapters.registry import AdapterRegistry
from opc.layer2_organization.org_engine import OrgEngine
class LoadedContext:
"""Container for all context assembled for a task."""
def __init__(self) -> None:
self.preferences: dict[str, Any] = {}
self.memory: str = ""
self.project_memory: str = ""
self.session_memory: str = ""
self.skills_context: str = ""
self.available_external_agents: list[str] = []
self.external_agent_profiles: list[dict[str, Any]] = []
self.company_profile: str = "corporate"
self.company_profiles: list[str] = ["corporate", "custom"]
self.company_profile_descriptions: dict[str, str] = {}
self.autonomy_preferences: dict[str, Any] = {}
self.autonomy_stats: dict[str, Any] = {}
self.external_sessions: list[dict[str, Any]] = []
self.session_execution_defaults: dict[str, Any] = {}
self.project_id: str | None = None
self.capability_catalog_summary: str = ""
self.secretary_context: str = ""
self.default_channel: str = "cli"
self.origin_chat_id: str = ""
self.origin_thread_id: str = ""
def has_capable_external_agent(self, domains: list[str]) -> bool:
_ = domains
return bool(self.available_external_agents)
class ContextLoader:
"""Loads all relevant context for a task before routing."""
def __init__(
self,
memory: MemoryManager,
preferences: PreferenceManager,
secretary_policies: SecretaryPolicyManager,
skills: SkillLibrary,
capability_manager: CapabilityManager,
adapter_registry: AdapterRegistry,
org_engine: OrgEngine,
store: OPCStore,
) -> None:
self.memory = memory
self.preferences = preferences
self.secretary_policies = secretary_policies
self.skills = skills
self.capability_manager = capability_manager
self.adapters = adapter_registry
self.org_engine = org_engine
self.store = store
async def load(
self,
project_id: str | None = None,
session_id: str | None = None,
domains: list[str] | None = None,
*,
include_project_knowledge: bool = False,
) -> LoadedContext:
ctx = LoadedContext()
ctx.project_id = project_id
ctx.preferences = {}
ctx.autonomy_preferences = {}
ctx.secretary_context = ""
if session_id:
session = await self.store.get_session(session_id)
if session:
defaults = session.metadata.get("execution_defaults", {})
if isinstance(defaults, dict):
ctx.session_execution_defaults = dict(defaults)
ctx.project_memory = await self.memory.build_memory_context(
project_id=project_id,
session_id=None,
include_project_knowledge=include_project_knowledge,
)
if session_id:
maybe_compact = getattr(self.memory, "maybe_compact_session_history", None)
if callable(maybe_compact):
await maybe_compact(session_id, project_id=project_id)
ctx.session_memory = (
await self.memory.build_session_prompt_context(
session_id,
include_latest_user_turn=False,
)
if session_id
else ""
)
ctx.memory = "\n\n".join(part for part in (ctx.project_memory, ctx.session_memory) if part)
# Pre-routing catalog: we don't yet know which execution mode
# the user will land in, so mode-restricted skills (e.g. a
# collaboration playbook scoped to company_mode) are hidden
# from this top-level catalog. They surface later via the
# per-turn prompt harness, which does know the execution mode.
ctx.skills_context = self.skills.build_skills_summary(project_id, execution_mode=None)
ctx.capability_catalog_summary = self.capability_manager.build_catalog_summary()
ctx.available_external_agents = self.adapters.list_available()
ctx.external_agent_profiles = self.adapters.describe_all()
ctx.company_profile = self.org_engine.get_company_profile()
ctx.company_profiles = list(self.org_engine.config.org.company_profiles)
ctx.company_profile_descriptions = self.org_engine.get_company_profile_descriptions()
ctx.autonomy_stats = await self.store.get_autonomy_stats(project_id=project_id)
sessions = []
for agent in ctx.available_external_agents:
session = await self.store.get_external_session(agent_type=agent, project_id=project_id or "default")
if session:
sessions.append({
"agent_type": session.agent_type,
"session_id": session.session_id,
"run_mode": session.run_mode,
"status": session.status,
"updated_at": session.updated_at.isoformat(),
"last_activity_at": session.metadata.get("last_activity_at", ""),
"activity_count": session.metadata.get("activity_count", 0),
"pid": session.metadata.get("pid"),
})
ctx.external_sessions = sessions
return ctx