refactor(acms): unify ContextFragment model hierarchies by extending CRP base types
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 17s
CI / security (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 35s
CI / unit_tests (pull_request) Successful in 2m22s
CI / integration_tests (pull_request) Successful in 3m0s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 4m21s
CI / lint (push) Successful in 14s
CI / quality (push) Successful in 16s
CI / build (push) Successful in 16s
CI / security (push) Successful in 30s
CI / typecheck (push) Successful in 36s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 3m38s
CI / docker (push) Successful in 39s
CI / integration_tests (push) Successful in 4m25s
CI / coverage (push) Successful in 4m19s
CI / benchmark-publish (push) Successful in 15m57s
CI / benchmark-regression (pull_request) Successful in 28m50s

Core domain types (FragmentProvenance, ContextFragment, ContextBudget,
ContextPayload) now extend their CRP counterparts via Pydantic v2
inheritance, ensuring isinstance compatibility across the model
hierarchy.

Key changes:
- CRP base types made frozen=True (no consumer mutates them)
- CRP AssembledContext fields changed from list to tuple (frozen consistency)
- Core types extend CRP bases: FragmentProvenance(CRPFragmentProvenance),
  ContextFragment(CRPContextFragment), ContextBudget(CRPContextBudget),
  ContextPayload(CRPAssembledContext)
- Removed duplicate ContextFragment dataclass from skeleton_compressor
- Updated project_context.py to pass tuples to frozen AssembledContext
- Added Behave tests (10 scenarios), Robot integration tests (3 cases),
  and ASV benchmarks for the unified hierarchy
- Updated Known Limitations table in docs/reference/acms.md

ISSUES CLOSED: #569
This commit was merged in pull request #598.
This commit is contained in:
2026-03-05 06:02:46 +00:00
parent d990fc1b41
commit fae438a7a7
19 changed files with 991 additions and 157 deletions
+13 -3
View File
@@ -22,9 +22,15 @@ import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.application.services.skeleton_compressor import ( # noqa: E402
ContextFragment,
SkeletonCompressorService,
)
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
ContextFragment,
FragmentProvenance,
)
# Default provenance for skeleton benchmark fragments.
_SKEL_PROV = FragmentProvenance(resource_uri="bench-skeleton://default")
def _build_fragments(count: int, tokens_each: int = 100) -> list[ContextFragment]:
@@ -32,10 +38,14 @@ def _build_fragments(count: int, tokens_each: int = 100) -> list[ContextFragment
return [
ContextFragment(
fragment_id=f"bench-{i:05d}",
uko_node=f"bench-skeleton://file/{i}",
content="x" * tokens_each,
token_count=tokens_each,
relevance=round(1.0 - (i / max(count, 1)), 4),
source_decision_id=f"01HX{'B' * 22}{i:02d}" if i < 99 else None,
relevance_score=round(1.0 - (i / max(count, 1)), 4),
provenance=_SKEL_PROV,
metadata=(
{"source_decision_id": f"01HX{'B' * 22}{i:02d}"} if i < 99 else {}
),
)
for i in range(count)
]
+122
View File
@@ -0,0 +1,122 @@
"""ASV benchmarks for unified CRP/core context model hierarchy.
Measures the construction throughput for core types that extend CRP base
types via Pydantic v2 inheritance, and the isinstance-check overhead.
"""
from __future__ import annotations
import importlib
import sys
from pathlib import Path
# Ensure the local *source* tree is importable even when ASV has an
# older build of the package installed.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
# Force-reload the top-level package so Python picks up the source tree
# version instead of the potentially stale installed copy.
import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.domain.models.acms.crp import ( # noqa: E402
AssembledContext as CRPAssembledContext,
ContextBudget as CRPContextBudget,
ContextFragment as CRPContextFragment,
FragmentProvenance as CRPFragmentProvenance,
)
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
ContextBudget,
ContextFragment,
ContextPayload,
FragmentProvenance,
)
class TimeUnifiedModelCreation:
"""Benchmark unified model construction throughput."""
timeout = 60
def setup(self) -> None:
"""Prepare reusable inputs."""
self.provenance = FragmentProvenance(
resource_uri="uko-py:module/bench",
strategy="benchmark-strategy",
resource_type="git-checkout",
)
def time_fragment_provenance(self) -> None:
"""Create core FragmentProvenance (inherits CRP)."""
for _ in range(1000):
FragmentProvenance(
resource_uri="uko-py:module/bench",
strategy="arce",
resource_type="git-checkout",
)
def time_context_fragment(self) -> None:
"""Create core ContextFragment (inherits CRP)."""
for _ in range(1000):
ContextFragment(
uko_node="project://bench",
content="benchmark fragment",
token_count=100,
tier="hot",
provenance=self.provenance,
)
def time_context_budget(self) -> None:
"""Create core ContextBudget (inherits CRP)."""
for _ in range(1000):
b = ContextBudget(max_tokens=8192, reserved_tokens=1024)
_ = b.available_tokens
def time_context_payload(self) -> None:
"""Create core ContextPayload (inherits CRP AssembledContext)."""
for _ in range(1000):
ContextPayload(plan_id="01JQBENCHPN00000000000000AA")
class TimeIsinstanceChecks:
"""Benchmark isinstance compatibility checks."""
timeout = 60
def setup(self) -> None:
"""Prepare instances."""
prov = FragmentProvenance(resource_uri="uko-py:module/bench")
self.provenance = prov
self.fragment = ContextFragment(
uko_node="project://bench",
content="benchmark fragment",
token_count=100,
provenance=prov,
)
self.budget = ContextBudget(max_tokens=4096, reserved_tokens=512)
self.payload = ContextPayload(
plan_id="01JQBENCHPN00000000000000AA",
)
def time_isinstance_provenance(self) -> None:
"""isinstance check: FragmentProvenance against CRP base."""
for _ in range(10000):
isinstance(self.provenance, CRPFragmentProvenance)
def time_isinstance_fragment(self) -> None:
"""isinstance check: ContextFragment against CRP base."""
for _ in range(10000):
isinstance(self.fragment, CRPContextFragment)
def time_isinstance_budget(self) -> None:
"""isinstance check: ContextBudget against CRP base."""
for _ in range(10000):
isinstance(self.budget, CRPContextBudget)
def time_isinstance_payload(self) -> None:
"""isinstance check: ContextPayload against CRP AssembledContext."""
for _ in range(10000):
isinstance(self.payload, CRPAssembledContext)
+1
View File
@@ -164,6 +164,7 @@ milestones:
| `FragmentScorer.score()` | Missing `plan_context` param; returns `Sequence[ContextFragment]` | Returns `list[ScoredFragment]` with `plan_context` | Future milestone |
| `PreambleGenerator.generate()` | Missing `strategies_used`, `budget_used`, `max_tokens` params | Full parameter set per spec §42795 | Future milestone |
| `provenance_map` keying | Keyed by `fragment_id` (ULID) | Spec keys by `uko_node` (UKO URI) | Under review — `fragment_id` avoids collisions when multiple fragments share a `uko_node` |
| CRP base model mutability | `FragmentProvenance`, `ContextFragment`, `ContextBudget`, `AssembledContext` are `frozen=True` | CRP models may be mutable | Core types extend CRP bases via Pydantic v2 inheritance; freezing CRP bases ensures consistent immutability across the hierarchy. No CRP consumer mutates these instances. |
## Extension Points
+17
View File
@@ -132,3 +132,20 @@ Feature: Skeleton compressor
Given a skeleton metadata with ratio 0.5 and 1000 original tokens and 500 compressed
When I attach skeleton_metadata to a plan
Then the plan should expose skeleton metadata in cli dict
# --- _validate_fragments edge-cases (lines 153-163) --------------------
Scenario: Reject fragment with negative token_count
Given a context fragment constructed with negative token_count
When I validate the invalid fragments
Then the compressor should raise a ValueError mentioning "token_count must be >= 0"
Scenario: Reject fragment with out-of-range relevance_score
Given a context fragment constructed with relevance_score 1.5
When I validate the invalid fragments
Then the compressor should raise a ValueError mentioning "relevance_score must be in"
Scenario: Reject fragment with empty fragment_id
Given a context fragment constructed with empty fragment_id
When I validate the invalid fragments
Then the compressor should raise a ValueError mentioning "fragment_id must be non-empty"
@@ -218,7 +218,9 @@ def step_new_cov_output_contains(context: Context, text: str) -> None:
@then("new_cov the returned object should be a PlanApplyService instance")
def step_new_cov_result_is_pas(context: Context) -> None:
assert context.new_cov_apply_result is context.new_cov_pas_instance
assert context.new_cov_apply_result is context.new_cov_pas_instance, (
f"Expected the mock PAS instance, got {type(context.new_cov_apply_result).__name__}"
)
@then("new_cov PlanApplyService was constructed with the lifecycle service")
+6 -2
View File
@@ -121,8 +121,12 @@ def step_call_lifecycle_service_helper(context) -> None:
@then("the lifecycle service helper should return a plan lifecycle service")
def step_lifecycle_service_helper_returns(context) -> None:
assert isinstance(context.helper_service, PlanLifecycleService)
assert context.helper_service.settings is context.helper_settings
svc = context.helper_service
assert isinstance(svc, PlanLifecycleService), (
f"Expected PlanLifecycleService, got {type(svc).__name__} "
f"(module={type(svc).__module__})"
)
assert svc.settings is context.helper_settings
@when("I run plan lifecycle use with parsed arguments")
@@ -276,33 +276,21 @@ def step_execute_project_status_error(context):
def step_run_project_list(context):
"""Run project list command.
Ensures the default database path is writable by creating the
``.cleveragents`` directory and bootstrapping the schema so the
``list`` sub-command can query the ``ns_projects`` table even
when no projects exist.
Mocks the project repository to return an empty list so the test
is fully isolated from database state left by prior scenarios
(which caused flaky failures in sequential / coverage mode).
"""
import os
from unittest.mock import MagicMock, patch
from sqlalchemy import create_engine
from cleveragents.infrastructure.database.models import Base
# Ensure the fallback database directory exists inside the temp CWD
db_dir = Path.cwd() / ".cleveragents"
db_dir.mkdir(parents=True, exist_ok=True)
db_path = db_dir / "db.sqlite"
# Point the container at this database so it doesn't fail on open
db_url = f"sqlite:///{db_path.absolute()}"
os.environ["CLEVERAGENTS_DATABASE_URL"] = db_url
# Bootstrap the schema so the table exists for the query
engine = create_engine(db_url, echo=False)
Base.metadata.create_all(engine)
engine.dispose()
mock_repo = MagicMock()
mock_repo.list_projects.return_value = []
runner = CliRunner()
result = runner.invoke(project.app, ["list"])
with patch(
"cleveragents.cli.commands.project._get_namespaced_project_repo",
return_value=mock_repo,
):
result = runner.invoke(project.app, ["list"])
context.result = result
+9 -1
View File
@@ -45,7 +45,15 @@ def step_create_tracker(context):
context.tracker = AsyncResourceTracker()
context.resources = {}
context.log_handler = _CapturingHandler()
logging.getLogger("cleveragents.core.async_cleanup").addHandler(context.log_handler)
_logger = logging.getLogger("cleveragents.core.async_cleanup")
# Remove stale handlers from previous scenarios (sequential mode)
for h in list(_logger.handlers):
if isinstance(h, _CapturingHandler):
_logger.removeHandler(h)
_logger.addHandler(context.log_handler)
# Ensure the logger propagates warnings even if a prior test disabled them.
_logger.setLevel(logging.DEBUG)
_logger.disabled = False
context.warnings_logged = context.log_handler.records
+160 -39
View File
@@ -4,12 +4,16 @@ from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.application.services.skeleton_compressor import (
DEFAULT_SKELETON_RATIO,
ContextFragment,
SkeletonCompressorService,
)
from cleveragents.domain.models.core.context_fragment import (
ContextFragment,
FragmentProvenance,
)
from cleveragents.domain.models.core.plan import (
NamespacedName,
Plan,
@@ -21,6 +25,31 @@ from cleveragents.domain.models.core.skeleton_metadata import SkeletonMetadata
# --- helpers ---------------------------------------------------------------
# Default provenance for skeleton test fragments.
_SKEL_PROV = FragmentProvenance(resource_uri="skeleton://test")
def _make_skel_fragment(
fragment_id: str,
content: str,
token_count: int,
relevance_score: float,
source_decision_id: str | None = None,
) -> ContextFragment:
"""Create a ContextFragment suitable for skeleton compression tests."""
metadata: dict[str, str] = {}
if source_decision_id is not None:
metadata["source_decision_id"] = source_decision_id
return ContextFragment(
fragment_id=fragment_id,
uko_node=f"skeleton://{fragment_id}",
content=content,
token_count=token_count,
relevance_score=relevance_score,
provenance=_SKEL_PROV,
metadata=metadata,
)
def _make_fragments(total_tokens: int, count: int = 4) -> list[ContextFragment]:
"""Create *count* fragments summing to *total_tokens*."""
@@ -31,11 +60,11 @@ def _make_fragments(total_tokens: int, count: int = 4) -> list[ContextFragment]:
for i in range(count):
tokens = base + (remainder if i == 0 else 0)
frags.append(
ContextFragment(
_make_skel_fragment(
fragment_id=f"frag-{i:03d}",
content=f"content-{i}" * max(1, tokens // 10),
token_count=tokens,
relevance=relevances[i % len(relevances)],
relevance_score=relevances[i % len(relevances)],
source_decision_id=f"01HX{'A' * 22}{i}" if i < 3 else None,
)
)
@@ -52,6 +81,7 @@ def _make_plan_id() -> str:
@given("a skeleton compressor service")
def step_create_service(context: Context) -> None:
context.service = SkeletonCompressorService()
context.compressor_error = None
# --- Fragment setup --------------------------------------------------------
@@ -65,11 +95,11 @@ def step_fragments_total(context: Context, total: int) -> None:
@given("three fragments with equal relevance {rel:g}")
def step_equal_relevance(context: Context, rel: float) -> None:
context.fragments = [
ContextFragment(
_make_skel_fragment(
fragment_id=f"frag-{chr(ord('c') - i)}",
content="x" * 50,
token_count=100,
relevance=rel,
relevance_score=rel,
)
for i in range(3)
]
@@ -78,27 +108,33 @@ def step_equal_relevance(context: Context, rel: float) -> None:
@given("fragments with relevances 0.9, 0.3, and 0.7")
def step_varied_relevances(context: Context) -> None:
context.fragments = [
ContextFragment(fragment_id="f-1", content="a", token_count=100, relevance=0.9),
ContextFragment(fragment_id="f-2", content="b", token_count=100, relevance=0.3),
ContextFragment(fragment_id="f-3", content="c", token_count=100, relevance=0.7),
_make_skel_fragment(
fragment_id="f-1", content="a", token_count=100, relevance_score=0.9
),
_make_skel_fragment(
fragment_id="f-2", content="b", token_count=100, relevance_score=0.3
),
_make_skel_fragment(
fragment_id="f-3", content="c", token_count=100, relevance_score=0.7
),
]
@given("fragments with known decision IDs")
def step_known_ids(context: Context) -> None:
context.fragments = [
ContextFragment(
_make_skel_fragment(
fragment_id="f-1",
content="a",
token_count=100,
relevance=0.9,
relevance_score=0.9,
source_decision_id="01HXDECISION00000000000001",
),
ContextFragment(
_make_skel_fragment(
fragment_id="f-2",
content="b",
token_count=100,
relevance=0.5,
relevance_score=0.5,
source_decision_id="01HXDECISION00000000000002",
),
]
@@ -112,25 +148,30 @@ def step_empty_frags(context: Context) -> None:
@given("a single fragment with {tokens:d} tokens")
def step_single_frag(context: Context, tokens: int) -> None:
context.fragments = [
ContextFragment(
_make_skel_fragment(
fragment_id="only",
content="x" * tokens,
token_count=tokens,
relevance=0.8,
relevance_score=0.8,
),
]
@given("a fragment with negative token count")
def step_neg_tokens(context: Context) -> None:
context.fragments = [
ContextFragment(
fragment_id="bad",
content="x",
token_count=-10,
relevance=0.5,
),
]
try:
context.fragments = [
_make_skel_fragment(
fragment_id="bad",
content="x",
token_count=-10,
relevance_score=0.5,
),
]
except (ValueError, ValidationError) as exc:
# Model-level validation now catches this at construction time.
context.compressor_error = exc
context.fragments = []
@given("a fragment with empty fragment_id")
@@ -138,23 +179,30 @@ def step_empty_id(context: Context) -> None:
context.fragments = [
ContextFragment(
fragment_id="",
uko_node="skeleton://empty-id",
content="x",
token_count=10,
relevance=0.5,
relevance_score=0.5,
provenance=_SKEL_PROV,
),
]
@given("a fragment with relevance {rel:g}")
def step_bad_relevance(context: Context, rel: float) -> None:
context.fragments = [
ContextFragment(
fragment_id="bad",
content="x",
token_count=10,
relevance=rel,
),
]
try:
context.fragments = [
_make_skel_fragment(
fragment_id="bad",
content="x",
token_count=10,
relevance_score=rel,
),
]
except (ValueError, ValidationError) as exc:
# Model-level validation now catches this at construction time.
context.compressor_error = exc
context.fragments = []
@given(
@@ -174,6 +222,12 @@ def step_make_metadata(context: Context, ratio: float, orig: int, comp: int) ->
@when("I compress with skeleton_ratio {ratio:g}")
def step_compress_ratio(context: Context, ratio: float) -> None:
# If model-level validation already caught an error during fragment
# construction (Given step), propagate it as the compress error.
if getattr(context, "compressor_error", None) is not None:
context.error = context.compressor_error
context.result = None
return
try:
context.result = context.service.compress(
context.fragments, skeleton_ratio=ratio
@@ -205,8 +259,11 @@ def step_compress_non_fragment_item(context: Context) -> None:
try:
context.result = context.service.compress(
[
ContextFragment(
fragment_id="ok", content="x", token_count=10, relevance=0.5
_make_skel_fragment(
fragment_id="ok",
content="x",
token_count=10,
relevance_score=0.5,
),
"not-a-fragment",
], # type: ignore[list-item]
@@ -279,8 +336,8 @@ def step_all_returned(context: Context) -> None:
def step_top_one(context: Context) -> None:
assert context.result is not None
assert len(context.result.fragments) == 1
top_relevance = max(f.relevance for f in context.fragments)
assert context.result.fragments[0].relevance == top_relevance
top_relevance = max(f.relevance_score for f in context.fragments)
assert context.result.fragments[0].relevance_score == top_relevance
@then("compressed tokens should be at most {limit:d}")
@@ -306,13 +363,13 @@ def step_ordered_by_id(context: Context) -> None:
@then("the first fragment should have relevance {rel:g}")
def step_first_relevance(context: Context, rel: float) -> None:
assert context.result is not None
assert context.result.fragments[0].relevance == rel
assert context.result.fragments[0].relevance_score == rel
@then("the last fragment should have relevance {rel:g}")
def step_last_relevance(context: Context, rel: float) -> None:
assert context.result is not None
assert context.result.fragments[-1].relevance == rel
assert context.result.fragments[-1].relevance_score == rel
@then("metadata original_tokens should be {expected:d}")
@@ -337,9 +394,9 @@ def step_compressed_at_most(context: Context, limit: int) -> None:
def step_all_decision_ids(context: Context) -> None:
assert context.result is not None
expected_ids = {
f.source_decision_id
f.metadata["source_decision_id"]
for f in context.fragments
if f.source_decision_id is not None
if "source_decision_id" in f.metadata
}
actual_ids = set(context.result.metadata.source_decision_ids)
assert expected_ids == actual_ids
@@ -417,3 +474,67 @@ def step_plan_cli_dict(context: Context) -> None:
assert skel["ratio"] == context.skel_meta.ratio
assert skel["original_tokens"] == context.skel_meta.original_tokens
assert skel["compressed_tokens"] == context.skel_meta.compressed_tokens
# --- _validate_fragments edge-case steps --------------------------------
@given("a context fragment constructed with negative token_count")
def step_frag_negative_token(context: Context) -> None:
frag = ContextFragment.model_construct(
fragment_id="neg-tc",
uko_node="skeleton://neg-tc",
content="x",
token_count=-5,
relevance_score=0.5,
provenance=_SKEL_PROV,
)
context.invalid_fragments = [frag]
@given("a context fragment constructed with relevance_score {score}")
def step_frag_bad_relevance(context: Context, score: str) -> None:
frag = ContextFragment.model_construct(
fragment_id="bad-rel",
uko_node="skeleton://bad-rel",
content="x",
token_count=10,
relevance_score=float(score),
provenance=_SKEL_PROV,
)
context.invalid_fragments = [frag]
@given("a context fragment constructed with empty fragment_id")
def step_frag_empty_id(context: Context) -> None:
frag = ContextFragment.model_construct(
fragment_id="",
uko_node="skeleton://empty",
content="x",
token_count=10,
relevance_score=0.5,
provenance=_SKEL_PROV,
)
context.invalid_fragments = [frag]
@when("I validate the invalid fragments")
def step_validate_invalid(context: Context) -> None:
try:
SkeletonCompressorService._validate_fragments(context.invalid_fragments)
context.validation_error = None
except (TypeError, ValueError) as exc:
context.validation_error = exc
@then('the compressor should raise a ValueError mentioning "{text}"')
def step_check_validation_error(context: Context, text: str) -> None:
assert context.validation_error is not None, (
"Expected a ValueError but none was raised"
)
assert isinstance(context.validation_error, ValueError), (
f"Expected ValueError, got {type(context.validation_error).__name__}"
)
assert text in str(context.validation_error), (
f"Expected '{text}' in error message: {context.validation_error}"
)
@@ -0,0 +1,277 @@
"""Step definitions for features/unified_context_models.feature.
Tests isinstance compatibility and construction of the unified
ContextFragment model hierarchy.
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.domain.models.acms.crp import (
AssembledContext as CRPAssembledContext,
)
from cleveragents.domain.models.acms.crp import (
ContextBudget as CRPContextBudget,
)
from cleveragents.domain.models.acms.crp import (
ContextFragment as CRPContextFragment,
)
from cleveragents.domain.models.acms.crp import (
FragmentProvenance as CRPFragmentProvenance,
)
from cleveragents.domain.models.core.context_fragment import (
ContextBudget,
ContextFragment,
ContextPayload,
FragmentProvenance,
)
# Default provenance for test fragments.
_DEFAULT_PROVENANCE = FragmentProvenance(resource_uri="test://unified")
# ---------------------------------------------------------------------------
# Given — Module availability
# ---------------------------------------------------------------------------
@given("the unified model modules are available")
def step_modules_available(context: Context) -> None:
pass
# ---------------------------------------------------------------------------
# Given — FragmentProvenance
# ---------------------------------------------------------------------------
@given('a core FragmentProvenance with resource_uri "{uri}"')
def step_core_provenance(context: Context, uri: str) -> None:
context.provenance = FragmentProvenance(resource_uri=uri)
@given('a core FragmentProvenance for strategy "{strategy}" with resource_uri "{uri}"')
def step_core_provenance_with_strategy(
context: Context, strategy: str, uri: str
) -> None:
context.provenance = FragmentProvenance(resource_uri=uri, strategy=strategy)
@given('a CRP FragmentProvenance with resource_uri "{uri}"')
def step_crp_provenance(context: Context, uri: str) -> None:
context.crp_provenance = CRPFragmentProvenance(resource_uri=uri)
# ---------------------------------------------------------------------------
# Given — ContextFragment
# ---------------------------------------------------------------------------
@given('a core ContextFragment with uko_node "{uko_node}" and content "{content}"')
def step_core_fragment(context: Context, uko_node: str, content: str) -> None:
context.unified_fragment = ContextFragment(
uko_node=uko_node,
content=content,
token_count=len(content),
provenance=_DEFAULT_PROVENANCE,
)
@given("two core ContextFragments with identical fields")
def step_two_identical_fragments(context: Context) -> None:
fixed_time = datetime.now(UTC)
frag_a = ContextFragment(
fragment_id="FIXEDIDFOREQUALITYTEST12345",
uko_node="project://eq-test",
content="equal",
token_count=5,
provenance=_DEFAULT_PROVENANCE,
created_at=fixed_time,
)
frag_b = ContextFragment(
fragment_id="FIXEDIDFOREQUALITYTEST12345",
uko_node="project://eq-test",
content="equal",
token_count=5,
provenance=_DEFAULT_PROVENANCE,
created_at=fixed_time,
)
context.frag_a = frag_a
context.frag_b = frag_b
# ---------------------------------------------------------------------------
# Given — ContextBudget
# ---------------------------------------------------------------------------
@given("a core ContextBudget with max_tokens {max_tok:d} and reserved_tokens {res:d}")
def step_core_budget(context: Context, max_tok: int, res: int) -> None:
context.unified_budget = ContextBudget(max_tokens=max_tok, reserved_tokens=res)
# ---------------------------------------------------------------------------
# Given — ContextPayload
# ---------------------------------------------------------------------------
@given('a core ContextPayload with plan_id "{plan_id}"')
def step_core_payload(context: Context, plan_id: str) -> None:
context.unified_payload = ContextPayload(plan_id=plan_id)
# ---------------------------------------------------------------------------
# When — Mutation attempts
# ---------------------------------------------------------------------------
@when("I attempt to mutate the fragment uko_node")
def step_mutate_fragment(context: Context) -> None:
context.mutation_error = None
try:
context.unified_fragment.uko_node = "project://mutated" # type: ignore[misc]
except ValidationError as exc:
context.mutation_error = exc
@when("I attempt to mutate the budget max_tokens")
def step_mutate_budget(context: Context) -> None:
context.mutation_error = None
try:
context.unified_budget.max_tokens = 9999 # type: ignore[misc]
except ValidationError as exc:
context.mutation_error = exc
@when("I attempt to mutate the CRP provenance resource_uri")
def step_mutate_crp_provenance(context: Context) -> None:
context.mutation_error = None
try:
context.crp_provenance.resource_uri = "test://mutated" # type: ignore[misc]
except ValidationError as exc:
context.mutation_error = exc
@when("I create a core ContextBudget with reserved_tokens equal to max_tokens")
def step_create_budget_equal(context: Context) -> None:
context.strict_budget_error = None
try:
ContextBudget(max_tokens=100, reserved_tokens=100)
except ValidationError as exc:
context.strict_budget_error = exc
# ---------------------------------------------------------------------------
# Then — isinstance checks
# ---------------------------------------------------------------------------
@then("the provenance should be an instance of CRP FragmentProvenance")
def step_provenance_isinstance(context: Context) -> None:
assert isinstance(context.provenance, CRPFragmentProvenance), (
f"Expected isinstance of CRP FragmentProvenance, got {type(context.provenance)}"
)
@then("the fragment should be an instance of CRP ContextFragment")
def step_fragment_isinstance(context: Context) -> None:
assert isinstance(context.unified_fragment, CRPContextFragment), (
f"Expected isinstance of CRP ContextFragment, "
f"got {type(context.unified_fragment)}"
)
@then("the budget should be an instance of CRP ContextBudget")
def step_budget_isinstance(context: Context) -> None:
assert isinstance(context.unified_budget, CRPContextBudget), (
f"Expected isinstance of CRP ContextBudget, got {type(context.unified_budget)}"
)
@then("the payload should be an instance of CRP AssembledContext")
def step_payload_isinstance(context: Context) -> None:
assert isinstance(context.unified_payload, CRPAssembledContext), (
f"Expected isinstance of CRP AssembledContext, "
f"got {type(context.unified_payload)}"
)
# ---------------------------------------------------------------------------
# Then — Field value assertions
# ---------------------------------------------------------------------------
@then('the provenance resource_type should be "{value}"')
def step_provenance_resource_type(context: Context, value: str) -> None:
assert context.provenance.resource_type == value
@then('the provenance strategy should be "{value}"')
def step_provenance_strategy(context: Context, value: str) -> None:
assert context.provenance.strategy == value
@then('the unified fragment tier should be "{tier}"')
def step_fragment_tier(context: Context, tier: str) -> None:
assert context.unified_fragment.tier == tier
@then("the unified fragment fragment_id should be non-empty")
def step_fragment_id_nonempty(context: Context) -> None:
assert context.unified_fragment.fragment_id
assert len(context.unified_fragment.fragment_id) > 0
@then("the unified fragment fragment_id should be {length:d} characters")
def step_fragment_id_length(context: Context, length: int) -> None:
fid = context.unified_fragment.fragment_id
assert len(fid) == length, (
f"Expected fragment_id of {length} chars, got {len(fid)}: {fid!r}"
)
@then("the unified fragment created_at should be a recent datetime")
def step_fragment_created_at_recent(context: Context) -> None:
created = context.unified_fragment.created_at
assert isinstance(created, datetime)
now = datetime.now(UTC)
assert now - created < timedelta(seconds=60)
@then("the unified budget available_tokens should be {tokens:d}")
def step_budget_available(context: Context, tokens: int) -> None:
assert context.unified_budget.available_tokens == tokens
# ---------------------------------------------------------------------------
# Then — Mutation error assertions
# ---------------------------------------------------------------------------
@then("a frozen mutation error should be raised")
def step_frozen_mutation_error(context: Context) -> None:
assert context.mutation_error is not None, (
"Expected a frozen mutation error but none was raised"
)
# ---------------------------------------------------------------------------
# Then — Equality assertions
# ---------------------------------------------------------------------------
@then("the two fragments should be equal")
def step_fragments_equal(context: Context) -> None:
assert context.frag_a == context.frag_b
@then("a validation error should be raised for strict budget")
def step_strict_budget_error(context: Context) -> None:
assert context.strict_budget_error is not None, (
"Expected a validation error for reserved == max but none was raised"
)
+67
View File
@@ -0,0 +1,67 @@
Feature: Unified context fragment model hierarchies
The core ContextFragment, FragmentProvenance, ContextBudget, and
ContextPayload types extend their CRP base counterparts, ensuring
isinstance compatibility across the model hierarchy.
Background:
Given the unified model modules are available
# ---------------------------------------------------------------
# isinstance compatibility
# ---------------------------------------------------------------
Scenario: Core FragmentProvenance is instance of CRP FragmentProvenance
Given a core FragmentProvenance with resource_uri "project://test"
Then the provenance should be an instance of CRP FragmentProvenance
And the provenance resource_type should be "unknown"
And the provenance strategy should be empty
Scenario: Core FragmentProvenance inherits CRP strategy field
Given a core FragmentProvenance for strategy "tier_retrieval" with resource_uri "project://test"
Then the provenance strategy should be "tier_retrieval"
And the provenance resource_type should be "unknown"
Scenario: Core ContextFragment is instance of CRP ContextFragment
Given a core ContextFragment with uko_node "project://app/main.py" and content "hello"
Then the fragment should be an instance of CRP ContextFragment
And the unified fragment tier should be "warm"
And the unified fragment fragment_id should be non-empty
Scenario: Core ContextFragment has ULID fragment_id by default
Given a core ContextFragment with uko_node "project://test" and content "test content"
Then the unified fragment fragment_id should be 26 characters
Scenario: Core ContextFragment created_at is auto-populated
Given a core ContextFragment with uko_node "project://test" and content "test content"
Then the unified fragment created_at should be a recent datetime
# ---------------------------------------------------------------
# Immutability (frozen)
# ---------------------------------------------------------------
Scenario: Core ContextFragment is frozen
Given a core ContextFragment with uko_node "project://frozen" and content "frozen"
When I attempt to mutate the fragment uko_node
Then a frozen mutation error should be raised
Scenario: Core ContextBudget is frozen
Given a core ContextBudget with max_tokens 4096 and reserved_tokens 512
When I attempt to mutate the budget max_tokens
Then a frozen mutation error should be raised
Scenario: CRP FragmentProvenance is frozen
Given a CRP FragmentProvenance with resource_uri "test://crp"
When I attempt to mutate the CRP provenance resource_uri
Then a frozen mutation error should be raised
# ---------------------------------------------------------------
# Equality across hierarchy
# ---------------------------------------------------------------
Scenario: Two core fragments with same data are equal
Given two core ContextFragments with identical fields
Then the two fragments should be equal
Scenario: Core ContextBudget strict validation rejects equal reserved and max
When I create a core ContextBudget with reserved_tokens equal to max_tokens
Then a validation error should be raised for strict budget
+37 -9
View File
@@ -18,33 +18,61 @@ if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.application.services.skeleton_compressor import ( # noqa: E402
ContextFragment,
SkeletonCompressorService,
)
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
ContextFragment,
FragmentProvenance,
)
# Default provenance for skeleton test fragments.
_SKEL_PROV = FragmentProvenance(resource_uri="skeleton://robot-test")
def _make_skel_fragment(
fragment_id: str,
content: str,
token_count: int,
relevance_score: float,
source_decision_id: str | None = None,
) -> ContextFragment:
"""Create a ContextFragment suitable for skeleton compression tests."""
metadata: dict[str, str] = {}
if source_decision_id is not None:
metadata["source_decision_id"] = source_decision_id
return ContextFragment(
fragment_id=fragment_id,
uko_node=f"skeleton://{fragment_id}",
content=content,
token_count=token_count,
relevance_score=relevance_score,
provenance=_SKEL_PROV,
metadata=metadata,
)
def _sample_fragments() -> list[ContextFragment]:
"""Build a repeatable set of sample fragments."""
return [
ContextFragment(
_make_skel_fragment(
fragment_id="frag-001",
content="High relevance content " * 20,
token_count=400,
relevance=0.9,
relevance_score=0.9,
source_decision_id="01HXDECISION00000000000001",
),
ContextFragment(
_make_skel_fragment(
fragment_id="frag-002",
content="Medium relevance content " * 15,
token_count=300,
relevance=0.6,
relevance_score=0.6,
source_decision_id="01HXDECISION00000000000002",
),
ContextFragment(
_make_skel_fragment(
fragment_id="frag-003",
content="Low relevance content " * 15,
token_count=300,
relevance=0.3,
relevance_score=0.3,
source_decision_id="01HXDECISION00000000000003",
),
]
@@ -114,11 +142,11 @@ def cmd_stable_ordering() -> None:
"""Verify fragments come out in deterministic order."""
svc = SkeletonCompressorService()
frags = [
ContextFragment(
_make_skel_fragment(
fragment_id=f"frag-{chr(ord('c') - i)}",
content="x",
token_count=10,
relevance=0.5,
relevance_score=0.5,
)
for i in range(3)
]
+178
View File
@@ -0,0 +1,178 @@
"""Robot Framework helper for unified context model integration tests.
Verifies isinstance compatibility across the CRP/core model hierarchy
and basic construction/immutability contracts.
Usage:
python robot/helper_unified_context_models.py isinstance-check
python robot/helper_unified_context_models.py construction-check
python robot/helper_unified_context_models.py immutability-check
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from pathlib import Path
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.domain.models.acms.crp import ( # noqa: E402
AssembledContext as CRPAssembledContext,
)
from cleveragents.domain.models.acms.crp import ( # noqa: E402
ContextBudget as CRPContextBudget,
)
from cleveragents.domain.models.acms.crp import ( # noqa: E402
ContextFragment as CRPContextFragment,
)
from cleveragents.domain.models.acms.crp import ( # noqa: E402
FragmentProvenance as CRPFragmentProvenance,
)
from cleveragents.domain.models.core.context_fragment import ( # noqa: E402
ContextBudget,
ContextFragment,
ContextPayload,
FragmentProvenance,
)
_DEFAULT_PROV = FragmentProvenance(resource_uri="test://robot-unified")
def _cmd_isinstance_check() -> int:
"""Verify isinstance compatibility for all four model types."""
prov = FragmentProvenance(resource_uri="test://robot")
if not isinstance(prov, CRPFragmentProvenance):
print("FAIL: FragmentProvenance is not instance of CRP type")
return 1
frag = ContextFragment(
uko_node="test://robot",
content="hello",
token_count=5,
provenance=prov,
)
if not isinstance(frag, CRPContextFragment):
print("FAIL: ContextFragment is not instance of CRP type")
return 1
budget = ContextBudget(max_tokens=4096, reserved_tokens=512)
if not isinstance(budget, CRPContextBudget):
print("FAIL: ContextBudget is not instance of CRP type")
return 1
payload = ContextPayload(plan_id="01JQTESTPN00000000000000AA")
if not isinstance(payload, CRPAssembledContext):
print("FAIL: ContextPayload is not instance of CRP AssembledContext")
return 1
print("unified-isinstance-ok")
return 0
def _cmd_construction_check() -> int:
"""Verify construction with inherited and extended fields."""
prov = FragmentProvenance(
resource_uri="test://construct",
strategy="test_strategy",
resource_type="git-checkout",
)
if prov.strategy != "test_strategy":
print(f"FAIL: expected strategy='test_strategy', got {prov.strategy!r}")
return 1
if prov.resource_type != "git-checkout":
print(
f"FAIL: expected resource_type='git-checkout', got {prov.resource_type!r}"
)
return 1
frag = ContextFragment(
uko_node="project://test",
content="constructed fragment",
token_count=20,
tier="hot",
provenance=prov,
)
if frag.tier != "hot":
print(f"FAIL: expected tier='hot', got {frag.tier!r}")
return 1
if not frag.fragment_id:
print("FAIL: fragment_id is empty")
return 1
if len(frag.fragment_id) != 26:
print(f"FAIL: expected ULID (26 chars), got {len(frag.fragment_id)}")
return 1
budget = ContextBudget(max_tokens=8192, reserved_tokens=1024)
if budget.available_tokens != 7168:
print(f"FAIL: expected available=7168, got {budget.available_tokens}")
return 1
print("unified-construction-ok")
return 0
def _cmd_immutability_check() -> int:
"""Verify that frozen models reject mutation."""
frag = ContextFragment(
uko_node="project://immut",
content="test",
token_count=4,
provenance=_DEFAULT_PROV,
)
try:
frag.uko_node = "project://mutated" # type: ignore[misc]
print("FAIL: expected frozen error for ContextFragment mutation")
return 1
except Exception:
pass
budget = ContextBudget(max_tokens=4096, reserved_tokens=512)
try:
budget.max_tokens = 9999 # type: ignore[misc]
print("FAIL: expected frozen error for ContextBudget mutation")
return 1
except Exception:
pass
prov = CRPFragmentProvenance(resource_uri="test://crp")
try:
prov.resource_uri = "test://mutated" # type: ignore[misc]
print("FAIL: expected frozen error for CRP FragmentProvenance mutation")
return 1
except Exception:
pass
print("unified-immutability-ok")
return 0
_COMMANDS: dict[str, Callable[[], int]] = {
"isinstance-check": _cmd_isinstance_check,
"construction-check": _cmd_construction_check,
"immutability-check": _cmd_immutability_check,
}
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 2:
print(
"Usage: helper_unified_context_models.py "
"<isinstance-check|construction-check|immutability-check>"
)
return 1
command = sys.argv[1]
handler = _COMMANDS.get(command)
if handler is None:
print(f"Unknown command: {command}")
return 1
return handler()
if __name__ == "__main__":
sys.exit(main())
+33
View File
@@ -0,0 +1,33 @@
*** Settings ***
Documentation Integration tests for unified CRP/core context model hierarchy
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_unified_context_models.py
*** Test Cases ***
Core Types Are Instances Of CRP Base Types
[Documentation] Verify that all four core types pass isinstance checks against CRP bases
${result}= Run Process ${PYTHON} ${HELPER} isinstance-check cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} unified-isinstance-ok
Unified Models Construct With Inherited And Extended Fields
[Documentation] Verify construction populates both CRP and core-specific fields
${result}= Run Process ${PYTHON} ${HELPER} construction-check cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} unified-construction-ok
Frozen Models Reject Mutation
[Documentation] Verify frozen configuration is inherited and enforced
${result}= Run Process ${PYTHON} ${HELPER} immutability-check cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} unified-immutability-ok
@@ -106,7 +106,6 @@ from cleveragents.application.services.session_service import (
)
from cleveragents.application.services.skeleton_compressor import (
CompressionResult,
ContextFragment,
SkeletonCompressorService,
)
from cleveragents.application.services.skill_registry_service import (
@@ -9,7 +9,7 @@ by ``skeleton_ratio``:
- **1.0** maximum compression; only the single highest-relevance
fragment is kept (with minimal content).
Fragments are sorted by ``relevance`` score in **descending** order
Fragments are sorted by ``relevance_score`` in **descending** order
(highest first). A stable secondary sort on ``fragment_id`` ensures
deterministic output for equal-relevance fragments.
@@ -20,6 +20,7 @@ from __future__ import annotations
from dataclasses import dataclass
from cleveragents.domain.models.core.context_fragment import ContextFragment
from cleveragents.domain.models.core.skeleton_metadata import SkeletonMetadata
# ---------------------------------------------------------------------------
@@ -27,27 +28,6 @@ from cleveragents.domain.models.core.skeleton_metadata import SkeletonMetadata
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class ContextFragment:
"""A single fragment of context to be compressed.
Attributes:
fragment_id: Stable identifier for this fragment.
content: The textual content of the fragment.
token_count: Number of tokens in ``content``.
relevance: A score in [0.0, 1.0] indicating how relevant
this fragment is (higher = more relevant).
source_decision_id: Optional decision ULID that produced
this fragment.
"""
fragment_id: str
content: str
token_count: int
relevance: float
source_decision_id: str | None = None
@dataclass(frozen=True, slots=True)
class CompressionResult:
"""Output of a skeleton compression pass.
@@ -86,7 +66,7 @@ class SkeletonCompressorService:
Args:
fragments: Context fragments to compress. Each must
have ``token_count >= 0`` and ``relevance`` in
have ``token_count >= 0`` and ``relevance_score`` in
``[0.0, 1.0]``.
skeleton_ratio: Compression ratio in ``[0.0, 1.0]``.
``None`` falls back to ``DEFAULT_SKELETON_RATIO``.
@@ -126,7 +106,7 @@ class SkeletonCompressorService:
# -- stable sort: relevance desc, fragment_id asc -----------------
sorted_fragments = sorted(
fragments,
key=lambda f: (-f.relevance, f.fragment_id),
key=lambda f: (-f.relevance_score, f.fragment_id),
)
# -- select fragments within budget -------------------------------
@@ -135,7 +115,9 @@ class SkeletonCompressorService:
compressed_tokens = sum(f.token_count for f in kept)
source_ids = tuple(
f.source_decision_id for f in kept if f.source_decision_id is not None
f.metadata["source_decision_id"]
for f in kept
if "source_decision_id" in f.metadata
)
metadata = SkeletonMetadata(
@@ -172,10 +154,10 @@ class SkeletonCompressorService:
raise ValueError(
f"fragments[{idx}].token_count must be >= 0, got {frag.token_count}"
)
if frag.relevance < 0.0 or frag.relevance > 1.0:
if frag.relevance_score < 0.0 or frag.relevance_score > 1.0:
raise ValueError(
f"fragments[{idx}].relevance must be in [0.0, 1.0], "
f"got {frag.relevance}"
f"fragments[{idx}].relevance_score must be in [0.0, 1.0], "
f"got {frag.relevance_score}"
)
if not frag.fragment_id:
raise ValueError(f"fragments[{idx}].fragment_id must be non-empty")
@@ -362,10 +362,10 @@ def _simulate_context_assembly(
)
return AssembledContext(
fragments=fragments,
fragments=tuple(fragments),
total_tokens=total_tokens,
budget_used=min(budget_used, 1.0),
strategies_used=strategies_used,
strategies_used=tuple(strategies_used),
context_hash=context_hash,
preamble=(
f"Simulated context for {project_name}"
+8 -12
View File
@@ -158,7 +158,7 @@ class DetailLevelMap(BaseModel):
# ---------------------------------------------------------------------------
class FragmentProvenance(BaseModel):
class FragmentProvenance(BaseModel, frozen=True):
"""Provenance trace for a context fragment.
Links a fragment back to the originating resource and location
@@ -181,7 +181,6 @@ class FragmentProvenance(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
)
@@ -190,7 +189,7 @@ class FragmentProvenance(BaseModel):
# ---------------------------------------------------------------------------
class ContextBudget(BaseModel):
class ContextBudget(BaseModel, frozen=True):
"""Token budget for context assembly.
Tracks the maximum token budget and any reserved portion, providing
@@ -227,7 +226,6 @@ class ContextBudget(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
)
@@ -236,7 +234,7 @@ class ContextBudget(BaseModel):
# ---------------------------------------------------------------------------
class ContextFragment(BaseModel):
class ContextFragment(BaseModel, frozen=True):
"""A single piece of context assembled by a strategy.
The atomic unit of context returned by strategies and consumed by
@@ -281,7 +279,6 @@ class ContextFragment(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
)
@@ -398,7 +395,7 @@ class ContextRequest(BaseModel):
# ---------------------------------------------------------------------------
class AssembledContext(BaseModel):
class AssembledContext(BaseModel, frozen=True):
"""The fused, budget-respecting context payload.
Output of a context assembly cycle -- the final payload delivered
@@ -406,8 +403,8 @@ class AssembledContext(BaseModel):
scoring, budget packing, and ordering.
"""
fragments: list[ContextFragment] = Field(
default_factory=list,
fragments: tuple[ContextFragment, ...] = Field(
default=(),
description="Ordered context fragments",
)
total_tokens: int = Field(
@@ -421,8 +418,8 @@ class AssembledContext(BaseModel):
le=1.0,
description="Fraction of budget consumed (0.0-1.0)",
)
strategies_used: list[str] = Field(
default_factory=list,
strategies_used: tuple[str, ...] = Field(
default=(),
description="Which strategies contributed",
)
context_hash: str = Field(
@@ -441,5 +438,4 @@ class AssembledContext(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
)
@@ -3,7 +3,13 @@
Defines the core value objects for the Adaptive Context Management System
(ACMS) context assembly pipeline: ``ContextFragment``, ``FragmentProvenance``,
``ContextBudget``, and ``ContextPayload``. All models are frozen Pydantic v2
value objects.
value objects that **extend** the mutable CRP base types defined in
:mod:`cleveragents.domain.models.acms.crp`.
The inheritance hierarchy ensures that every core ``ContextFragment`` is also
an ``isinstance`` of the CRP ``ContextFragment``, eliminating the previous
duplication and enabling strategies written against either interface to
interoperate seamlessly.
Based on ``docs/specification.md`` ~line 25075 (ContextFragment) and ~line
25092 (AssembledContext).
@@ -19,6 +25,18 @@ from typing import Any, Literal
from pydantic import BaseModel, Field, field_validator, model_validator
from ulid import ULID
from cleveragents.domain.models.acms.crp import (
AssembledContext as CRPAssembledContext,
)
from cleveragents.domain.models.acms.crp import (
ContextBudget as CRPContextBudget,
)
from cleveragents.domain.models.acms.crp import (
ContextFragment as CRPContextFragment,
)
from cleveragents.domain.models.acms.crp import (
FragmentProvenance as CRPFragmentProvenance,
)
from cleveragents.domain.models.core.plan import ULID_PATTERN
# Maximum content length (characters). Enforced to prevent unbounded memory
@@ -29,24 +47,16 @@ MAX_CONTENT_LENGTH: int = 1_000_000 # ~1 MB of text
MAX_METADATA_ENTRIES: int = 64
class FragmentProvenance(BaseModel, frozen=True):
class FragmentProvenance(CRPFragmentProvenance, frozen=True):
"""Provenance trace for a context fragment.
Records the originating resource and location so that fragments can be
traced back to their source for auditability and debugging.
Extends the CRP ``FragmentProvenance`` with an additional
``resource_type`` field for richer auditability. Frozen so that
provenance records are immutable value objects.
Based on ``docs/specification.md`` ~line 25088.
"""
resource_uri: str = Field(
...,
min_length=1,
description="URI of the originating resource (e.g. 'project://myapp/src/main.py')",
)
location: str = Field(
default="",
description="Location within the resource (e.g. line range, section name)",
)
resource_type: str = Field(
default="unknown",
description=(
@@ -55,20 +65,18 @@ class FragmentProvenance(BaseModel, frozen=True):
)
class ContextFragment(BaseModel, frozen=True):
class ContextFragment(CRPContextFragment, frozen=True):
"""A single piece of context assembled by the ACMS pipeline.
Extends the CRP ``ContextFragment`` with ``fragment_id``, ``tier``,
``created_at``, and stricter validation. Frozen so that fragments
are immutable value objects.
Based on ``docs/specification.md`` ~line 25081.
"""
fragment_id: str = Field(default_factory=lambda: str(ULID()))
uko_node: str = Field(
...,
min_length=1,
description="UKO URI of the source node",
)
content: str = Field(
...,
max_length=MAX_CONTENT_LENGTH,
@@ -84,12 +92,6 @@ class ContextFragment(BaseModel, frozen=True):
),
)
token_count: int = Field(
...,
ge=0,
description="Actual token count of content",
)
relevance_score: float = Field(
ge=0.0,
le=1.0,
@@ -132,12 +134,13 @@ class ContextFragment(BaseModel, frozen=True):
return dict(v)
class ContextBudget(BaseModel, frozen=True):
class ContextBudget(CRPContextBudget, frozen=True):
"""Token budget for context assembly.
``reserved_tokens`` must be strictly less than ``max_tokens``. The
effective minimum for ``max_tokens`` is therefore
``reserved_tokens + 1``.
Extends the CRP ``ContextBudget`` with stricter defaults and
validation: ``reserved_tokens`` must be strictly less than
``max_tokens``. The effective minimum for ``max_tokens`` is
therefore ``reserved_tokens + 1``.
"""
max_tokens: int = Field(ge=1, default=4096)
@@ -153,15 +156,13 @@ class ContextBudget(BaseModel, frozen=True):
raise ValueError(msg)
return self
@property
def available_tokens(self) -> int:
"""Return tokens available after reserving system-prompt space."""
return self.max_tokens - self.reserved_tokens
class ContextPayload(BaseModel, frozen=True):
class ContextPayload(CRPAssembledContext, frozen=True):
"""Assembled context payload ready for actor consumption.
Extends the CRP ``AssembledContext`` with ``payload_id``, ``plan_id``,
``budget``, ``assembled_at``, and immutable tuple-based collections.
Corresponds to the spec's ``AssembledContext`` (~line 25098). Includes
``budget_used`` fraction, ``strategies_used`` list, ``context_hash`` for
snapshot integrity, optional ``preamble``, and ``provenance_map``.