context package

The story

Every volk call in the pipeline is ultimately one LLM prompt, and that prompt has to carry many competing kinds of context: the volk's system prompt, the project's mission state, the current plan window, memory recall, digests of what upstream stages produced, knowledge-graph code context, AST-derived signals, semantic-search chunks, the task-type pack, and the user's raw instruction. Left uncoordinated, any one of those layers can flood the context window and starve the rest. orchestrata_ultimate.context exists to make that impossible: it is the top-level context assembly package that collapses all of these sources into one budgeted, token-accounted bundle per volk call. It sits deliberately at the orchestration layer so it can combine knowledge-graph code context with memory and session state without violating the project's downward-only dependency rules.

The accounting backbone is budget.py. A ContextBudget is a frozen dataclass naming a hard token reservation for each of eleven layers (system, mission, plan, memory, artifacts, graph, ast, semantic, task, user, output) against a total window; construction fails outright if the reservations over-allocate the total. Its runtime companion ContextUsage records actual spend per layer and refuses over-allocation — spend() raises ValueError on a negative spend, an unknown layer, a layer exceeding its reservation, or the total window being exceeded — and to_dict() emits the nested {"budget", "spent"} per-layer shape that telemetry consumes. The budget performs no retrieval itself; it is pure accounting, traced to SPEC-106 REQ-106-030.

The heart of the package is assembler.py's UnifiedContextAssembler (SPEC-106 REQ-106-030/REQ-106-042). Its single public method, assemble(), takes the CXP payload and subject, the user message, and optional session context, artifact store, task type, and thread id, and returns a ContextAssembly — a parts dict of named layer texts plus a ContextUsage telemetry object. Internally it calls each provider in turn: mission and plan blocks from loaded session ContextData, an artifact digest (preferring the store's own ranked render_artifact_digest when it has one, falling back to the generic duck-typed provider), a memory layer combining a 500-token thread summary with recall under the remaining memory budget (floored at 50 tokens), KG graph/task context through the wired ContextInjector — which is the SPEC-106 selector/packer path — and the AST and semantic blocks. KG injection results are memoized in a per-instance pack cache keyed by graph snapshot (project id + live graph version from get_graph_version()), volk alias, subject, task type, and a fingerprint of the payload's target-file fields, so repeated stages in one pipeline run reuse the pack while a post-sync graph version bump invalidates it; pack_cache_hits/pack_cache_misses counters expose the effect. Before accounting, every trimmable layer is cut to its reservation with trim_to_budget — user and system are protected (the canonical instruction and the volk's own prompt), and output is reserved response headroom that is never an assembled part. Finally the assembler writes a usage record (a failure there only warns; assembly never dies on telemetry) and logs any layer that consumed more than half its reservation. The default budget targets a 128k-window model with conservative per-layer reservations. In production, kernel/pipeline_kg.py constructs one assembler per pipeline run, wired to the shared ContextInjector, and degrades to the legacy raw ContextInjector.inject() path only if assembly itself fails.

The providers/ subpackage holds the stateless renderers — each takes the relevant slice of state and returns a compact markdown block, or "" when it has nothing to say, so the assembler can skip empty layers for free. mission_context.py renders project state, the active goal with its unmet criteria, pending work, and the latest handoff from ContextData (Gap D of docs/context-injection.md, traced to SPEC-006). plan_context.py renders the plan window from the active goal's tasks — current task (starting from the persisted current_step, then scanning forward past done tasks), the last three completed, and up to two upcoming (SPEC-107 REQ-107-003). artifact_digest.py renders a chronological digest of prior-stage artifacts and deliberately duck-types both the kernel and orchestrator ArtifactStore implementations (by_thread/all, stage/stage_alias, type/artifact_type) rather than forcing a canonicalization (Gap E). ast_context.py dynamically imports the real AST toolkit (scripts/serialize_codebase_ast.py, cached per process) and reports public API signatures, complexity hotspots, branch points, missing @traces, and a small call-graph neighborhood for the target files — every signal computed from real source, no LLM involved, degrading to "" when the toolkit or files are unavailable (REQ-107-014). semantic_context.py is a thin renderer over knowledge_graph.semantic_bridge: it extracts keyword terms from the user message and target-file basenames, searches Memgraph chunks, maps them back to Neo4j File roots, and only runs at all for the code-touching subjects in SEMANTIC_SUBJECTS (implement, fix_issues, review_and_test, design_review) (REQ-107-015). trimming.py provides the boundary-aware trim_to_budget used on every trimmable layer: it prefers whole paragraphs, then sentences, then words, and only as a hard-limit last resort binary-searches a byte prefix — so trimmed prompts stay coherent.

The package closes its loop with observability. usage_log.py (SPEC-107 FR-005) appends one JSON line per assemble() call — timestamp, volk alias, subject, thread id, and the full per-layer usage dict — to logs/context_usage.jsonl (overridable via the ORCHESTRATA_CONTEXT_USAGE_LOG env var, read at call time). read_usage_records skips malformed lines so one corrupt record never breaks reporting, and aggregate_usage rolls records up into per-layer, per-volk, and per-subject token totals; the companion CLI scripts/context_usage_report.py turns that into the project's context-spend reports. Together the three halves — budget, providers, usage log — mean every token a volk prompt carries is reserved before it is retrieved, trimmed at natural boundaries when it overflows, and recorded after it is spent.

Key responsibilities

Module dependency diagram

Internal module imports within orchestrata_ultimate.context, plus the package's main external collaborators (generated from the real imports):

Diagram projects-orchestrata-ultimate-modules-context-1

context_injector and semantic_bridge live in knowledge_graph, memory_context/token_counter in memory, session_models in session, ast_toolkit is the dynamically imported scripts/serialize_codebase_ast.py, and pipeline_kg (in kernel) is the production consumer that wires the assembler.

Module inventory

Module Purpose
context/__init__.py Package facade — re-exports the assembler, budget types, the six provider functions, and the usage-log functions.
context/assembler.py UnifiedContextAssembler — collapses all context sources into one budgeted, token-accounted ContextAssembly per volk call, with a graph-version-keyed KG pack cache (SPEC-106 REQ-106-030/042).
context/budget.py ContextBudget (frozen per-layer reservations, over-allocation rejected) and ContextUsage (spend tracking that refuses over-budget spends); pure accounting, no retrieval.
context/usage_log.py JSONL context-usage telemetry (SPEC-107 FR-005) — one record per assemble() call, malformed-line-tolerant reads, per-layer/per-volk/per-subject aggregation.
context/providers/__init__.py Provider facade — re-exports the five render_* functions and trim_to_budget.
context/providers/mission_context.py Mission/session block from ContextData: project state, active goal, pending work, latest handoff (Gap D; SPEC-006).
context/providers/plan_context.py Plan window from the active goal's tasks: current, recent done, upcoming (SPEC-107 REQ-107-003).
context/providers/artifact_digest.py Chronological digest of prior-stage artifacts; duck-types both ArtifactStore implementations (Gap E).
context/providers/ast_context.py Deterministic AST signals for target files via the dynamically imported, process-cached AST toolkit (REQ-107-014).
context/providers/semantic_context.py Semantic recall — Memgraph chunk search bridged to Neo4j File roots, gated to code-touching subjects (REQ-107-015).
context/providers/trimming.py Boundary-aware trim_to_budget: paragraphs, then sentences, then words, then a byte-prefix binary search as hard-limit fallback.

assembler.py

UnifiedContextAssembler is constructed with an optional ContextInjector, a ContextBudget (defaulting to a conservative 128k-window allocation), a token counter (defaulting to the project-wide fallback), and a project_id. assemble() fills a fixed parts dict — system, mission, plan, memory, artifacts, graph, ast, semantic, task, user — by calling each provider, injecting KG context through the injector when should_inject(subject) says so, and passing a shallow payload copy because the injector mutates what it receives. The KG pack cache is keyed on a snapshot token (project=<id>:gv=<graph version> by default, so a KG sync invalidates it), the volk alias, subject, task type, and the payload's files_created/project_dir/file_tree fingerprint — deliberately excluding the per-turn user message and session state, which do not change the code-context pack. After rendering, each non-empty trimmable layer is cut to its reservation with trim_to_budget and its token count is spent against ContextUsage; the user and system layers are never trimmed and output is pure response headroom. A usage record is then written (best-effort — a telemetry failure logs a warning, never fails the call), and any layer that used more than half its reservation is logged at info level. The result is a frozen ContextAssembly(parts, usage).

budget.py

ContextBudget is the declaration side: a frozen dataclass whose __post_init__ sums every layer reservation and raises ValueError if the sum exceeds total, with reserved/available properties and a layer_budget(name) accessor that rejects unknown layer names. ContextUsage is the enforcement side: spend(layer, tokens) raises on negative amounts, unknown layers, a layer exceeding its own budget, or the running total exceeding the window; total_spent/remaining derive from the per-layer counters, and to_dict() returns the telemetry shape — top-level total/spent/remaining ints plus one {"budget", "spent"} dict per layer. Nothing in this module retrieves or renders anything; it exists so the assembler physically cannot over-fill a prompt.

usage_log.py

Three functions implement SPEC-107 FR-005. write_usage_record appends a compact JSON line — UTC timestamp, volk alias, subject, thread id, and the full ContextUsage.to_dict() value — to a path resolved from an explicit argument, the ORCHESTRATA_CONTEXT_USAGE_LOG env var (read at call time through an injectable env mapping, per the project's env-seam rule), or the logs/context_usage.jsonl default, creating parent directories as needed. read_usage_records returns every parseable record and silently skips blank or malformed lines so one corrupt write never breaks reporting. aggregate_usage folds a record list into total_calls, total_spent, and per-layer, per-volk, and per-subject spend totals — the data behind scripts/context_usage_report.py.

providers/mission_context.py

render_mission_context(ctx, max_lines=40) turns a loaded ContextData into the # MISSION CONTEXT block: a one-line project status (branch, version, status line, test state), the active session number, the active goal with its status, statement, and up to five unmet criteria, up to five pending work items (priority-prefixed), and the latest session handoff's decisions, blockers, active work, key files, and alerts (five items each). A hard line cap drops from the end — handoff first, as the lowest-priority section — and a None context yields "". It deliberately never dumps the full context.md; the block is sized for the assembler's mission reservation.

providers/plan_context.py

render_plan_context(ctx, max_lines=25) renders the # PLAN WINDOW block from the active goal's tasks: the goal statement, a done/total progress line, the current task, up to three recently completed tasks, and up to two upcoming ones. The current task is found by starting at the goal's persisted current_step and scanning forward for the first task not marked done — honoring explicit step tracking while coping with hand-edited statuses. No context, no goal, or no tasks all yield "", as does a cap so tight that only the header survives. This is the read-side of the SPEC-023 goal loop: planning.plan_parser fills goal.tasks from the execution plan, and this provider renders the window back into every volk prompt.

providers/artifact_digest.py

render_artifact_digest(store, thread_id="", max_items=10) gives downstream volks a chronological # PRIOR ARTIFACTS digest of what upstream stages produced, without each volk hand-copying payload keys. It is intentionally store-agnostic: an internal _ArtifactView normalizes either PipelineArtifact shape by trying stage/stage_alias, type/artifact_type, and content attributes, artifacts are fetched by by_thread(thread_id) when available (falling back to all()), sorted by timestamp when present, and rendered as one line each with a 200-character content preview. A None store or an empty result yields "". Full canonicalization of the two artifact stores is tracked as a later cleanup; until then duck-typing is the contract.

providers/ast_context.py

render_ast_context(target_files, ...) wraps the real AST toolkit rather than duplicating its analysis: scripts/serialize_codebase_ast.py is loaded by file path with importlib (so scripts/ never becomes a static dependency) and cached per process, since the toolkit is ~10k LOC. For the resolved target sources it emits an # AST CONTEXT block with up to five public API signatures, five complexity hotspots (threshold 5), five branch-point functions, five symbols missing @traces, and a defined/used call-graph summary for the top three public symbols — every section individually exception-guarded, so one failing analysis never kills the block. A project_id line scopes the signals in multi-project workspaces, a hard max_lines cap keeps the block inside its reservation, and no target files, an unloadable toolkit, unresolvable sources, or zero extracted signals all yield "".

providers/semantic_context.py

render_semantic_context(user_message, target_files, ...) is now a thin renderer over knowledge_graph.semantic_bridge, where the reusable search logic lives. It gates on subject first — semantic recall only runs for SEMANTIC_SUBJECTS (implement, fix_issues, review_and_test, design_review), a deliberately narrower set than the injector's — then extracts keyword terms from the user message plus target-file basenames, calls search_semantic_roots (Memgraph chunk search by content, chunk file paths mapped back to Neo4j File root entities, project-scoped), and renders the roots with render_semantic_roots under a line cap. All backend access is synchronous by design — the assembler and kernel pipeline are synchronous, so the provider uses the sync Bolt driver and the operational graph's SyncGraphRepository seam — and an unreachable Memgraph or Neo4j degrades to "" rather than failing assembly.

providers/trimming.py

trim_to_budget(text, budget_tokens, counter, hard_limit=True) keeps trimmed prompts coherent by cutting at natural boundaries: text already under budget passes through untouched; otherwise it greedily includes whole paragraphs (blank-line splits), then whole sentences (terminator-plus-whitespace splits), then whole words, returning the largest prefix that fits. Only when even a single word cannot fit — and hard_limit is on — does it binary-search a UTF-8 byte prefix, guaranteeing the result is at or below budget. A zero or negative budget yields ""; with hard_limit=False an untrimmable text is returned as-is. The assembler applies this to every trimmable layer before spending tokens, so one oversized retrieval can never blow up the whole assembly.

Class map (generated)

Generated from the live source AST by scripts/gen-module-class-diagrams.py — do not hand-edit. Abstract classes have a green border; members listed are those defined on the class itself (inherited members excluded). Solid empty-triangle arrows = in-package inheritance; dashed arrows into dashed boxes = external/stdlib/cross-package bases; filled diamond = composition (constructed in __init__); open diamond = aggregation (annotated/injected reference); diamond edge labels name the owning attribute.

context class map