From 711e8671128422000cba5311fe5338c164789bf9 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Tue, 3 Mar 2026 00:43:58 +0000 Subject: [PATCH] feat(acms): add skeleton compressor Implemented SkeletonCompressorService for ACMS context inheritance, producing compressed context representations for propagation from parent plans to child plans. Key design decisions and implementation details: - SkeletonMetadata (frozen Pydantic model): records ratio, original_tokens, compressed_tokens, and source_decision_ids for full auditability of each compression pass. Persisted on Plan.skeleton_metadata. - SkeletonCompressorService: stateless service accepting a list of ContextFragment objects and a skeleton_ratio in [0.0, 1.0]. Fragments are sorted by relevance descending with a stable secondary sort on fragment_id to guarantee deterministic output. Token budget is original_tokens*(1-ratio); fragments are greedily selected until budget is exhausted. - Ratio semantics: 0.0 = no compression (pass-through), 1.0 = maximum compression (single top fragment only), None = default 0.3. - Integration: Plan model gains optional skeleton_metadata field exposed in as_cli_dict() under the 'skeleton' key. Service registered in DI container as skeleton_compressor_service (Singleton, stateless). - Tests: 22 BDD scenarios (features/skeleton_compressor.feature) covering ratio validation, stable ordering, metadata correctness, edge cases, and plan model integration. 6 Robot Framework smoke tests. ASV benchmark suites at 10/100/1000 fragment scales. - Documentation: docs/reference/skeleton_compressor.md with ratio table, algorithm description, metadata schema, and multi-decision plan example. ISSUES CLOSED: #194 --- CHANGELOG.md | 5 + benchmarks/skeleton_compressor_bench.py | 97 ++++ docs/reference/skeleton_compressor.md | 109 +++++ features/skeleton_compressor.feature | 134 ++++++ features/steps/skeleton_compressor_steps.py | 419 ++++++++++++++++++ robot/helper_skeleton_compressor.py | 163 +++++++ robot/skeleton_compressor.robot | 49 ++ src/cleveragents/application/container.py | 8 + .../application/services/__init__.py | 8 + .../services/skeleton_compressor.py | 226 ++++++++++ .../domain/models/core/__init__.py | 4 + src/cleveragents/domain/models/core/plan.py | 17 + .../domain/models/core/skeleton_metadata.py | 79 ++++ vulture_whitelist.py | 13 + 14 files changed, 1331 insertions(+) create mode 100644 benchmarks/skeleton_compressor_bench.py create mode 100644 docs/reference/skeleton_compressor.md create mode 100644 features/skeleton_compressor.feature create mode 100644 features/steps/skeleton_compressor_steps.py create mode 100644 robot/helper_skeleton_compressor.py create mode 100644 robot/skeleton_compressor.robot create mode 100644 src/cleveragents/application/services/skeleton_compressor.py create mode 100644 src/cleveragents/domain/models/core/skeleton_metadata.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 240559434..9ad6a3c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ resource types with `DevcontainerHandler` and auto-discovery logic that scans for `.devcontainer/` directories when `git-checkout` or `fs-directory` resources are linked. Includes CLI support, Behave/Robot/ASV tests, and reference documentation. (#511) +- Added skeleton compressor service (`SkeletonCompressorService`) for ACMS context inheritance. + Compresses parent plan context fragments by `skeleton_ratio` (0.0–1.0) for propagation to + child plans. Persists `SkeletonMetadata` (ratio, token counts, source decision IDs) on the + plan model for auditability. Includes stable fragment ordering, ratio validation with default + handling, and compression summary. (#194) - Replaced behave-parallel subprocess-per-feature execution model (342 Python interpreter startups) with in-process execution via behave's `Runner` API. Sequential mode runs all features in a single `Runner.run()` call; parallel mode uses `multiprocessing.Pool` with diff --git a/benchmarks/skeleton_compressor_bench.py b/benchmarks/skeleton_compressor_bench.py new file mode 100644 index 000000000..271862e89 --- /dev/null +++ b/benchmarks/skeleton_compressor_bench.py @@ -0,0 +1,97 @@ +"""ASV benchmarks for skeleton compressor overhead. + +Measures the time to compress context fragments at various ratios +and fragment counts. The benchmark covers the hot path that runs +during subplan context inheritance. +""" + +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) + +import cleveragents # noqa: E402 + +importlib.reload(cleveragents) + +from cleveragents.application.services.skeleton_compressor import ( # noqa: E402 + ContextFragment, + SkeletonCompressorService, +) + + +def _build_fragments(count: int, tokens_each: int = 100) -> list[ContextFragment]: + """Generate *count* fragments for benchmarking.""" + return [ + ContextFragment( + fragment_id=f"bench-{i:05d}", + 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, + ) + for i in range(count) + ] + + +class SkeletonCompressorSmallSuite: + """Benchmark compression of a small fragment set (10 fragments).""" + + def setup(self) -> None: + self._svc = SkeletonCompressorService() + self._fragments = _build_fragments(10) + + def time_compress_ratio_0(self) -> None: + """No compression (pass-through).""" + self._svc.compress(self._fragments, skeleton_ratio=0.0) + + def time_compress_ratio_03(self) -> None: + """Default ratio compression.""" + self._svc.compress(self._fragments, skeleton_ratio=0.3) + + def time_compress_ratio_05(self) -> None: + """Moderate compression.""" + self._svc.compress(self._fragments, skeleton_ratio=0.5) + + def time_compress_ratio_1(self) -> None: + """Maximum compression.""" + self._svc.compress(self._fragments, skeleton_ratio=1.0) + + +class SkeletonCompressorMediumSuite: + """Benchmark compression of a medium fragment set (100 fragments).""" + + def setup(self) -> None: + self._svc = SkeletonCompressorService() + self._fragments = _build_fragments(100) + + def time_compress_ratio_03(self) -> None: + """Default ratio compression.""" + self._svc.compress(self._fragments, skeleton_ratio=0.3) + + def time_compress_ratio_05(self) -> None: + """Moderate compression.""" + self._svc.compress(self._fragments, skeleton_ratio=0.5) + + +class SkeletonCompressorLargeSuite: + """Benchmark compression of a large fragment set (1000 fragments).""" + + def setup(self) -> None: + self._svc = SkeletonCompressorService() + self._fragments = _build_fragments(1000) + + def time_compress_ratio_05(self) -> None: + """Moderate compression over 1000 fragments.""" + self._svc.compress(self._fragments, skeleton_ratio=0.5) + + def time_compress_ratio_08(self) -> None: + """Heavy compression over 1000 fragments.""" + self._svc.compress(self._fragments, skeleton_ratio=0.8) diff --git a/docs/reference/skeleton_compressor.md b/docs/reference/skeleton_compressor.md new file mode 100644 index 000000000..55120d4ea --- /dev/null +++ b/docs/reference/skeleton_compressor.md @@ -0,0 +1,109 @@ +# Skeleton Compressor + +## Overview + +The **Skeleton Compressor** produces a compressed representation of a +plan's accumulated context for propagation to child plans as inherited +context. Compression is governed by the `skeleton_ratio` budget +parameter set on a project's context policy. + +The compressor lives in +`cleveragents.application.services.skeleton_compressor.SkeletonCompressorService` +and is registered in the DI container as `skeleton_compressor_service`. + +## Skeleton Ratio + +| Ratio | Meaning | Behaviour | +|------:|:--------|:----------| +| `0.0` | No compression | All fragments pass through unchanged. | +| `0.3` | Default | ~70 % of tokens retained (top-relevance first). | +| `0.5` | Moderate | ~50 % of tokens retained. | +| `0.8` | Heavy | ~20 % of tokens retained. | +| `1.0` | Maximum | Only the single highest-relevance fragment is kept. | + +The ratio is validated to the closed interval `[0.0, 1.0]`. Values +outside this range raise `ValueError`. + +### Default Handling + +When a plan or project context policy does not set `skeleton_ratio`, +the service applies the constant `DEFAULT_SKELETON_RATIO = 0.3`. + +## Fragment Ordering + +Fragments are sorted by **relevance descending** with a stable +secondary sort on **fragment_id ascending**. This guarantees +deterministic output: identical inputs always produce identical +compressed payloads regardless of the order in which fragments +arrive. + +## Compression Algorithm + +1. Validate all inputs (ratio, fragment fields). +2. Compute `original_tokens` — sum of `token_count` across all + fragments. +3. Sort fragments by `(-relevance, fragment_id)`. +4. Compute a token budget: `budget = original_tokens * (1 - ratio)`. +5. Iterate sorted fragments, accumulating tokens until the budget is + exhausted. +6. Return the kept fragments and a `SkeletonMetadata` record. + +## Metadata + +Every compression pass produces a frozen `SkeletonMetadata`: + +| Field | Type | Description | +|:------|:-----|:------------| +| `ratio` | `float` | The ratio applied. | +| `original_tokens` | `int` | Tokens before compression. | +| `compressed_tokens` | `int` | Tokens after compression. | +| `source_decision_ids` | `tuple[str, ...]` | Decision ULIDs included. | + +The metadata is persisted on the `Plan` model via the +`skeleton_metadata` field, making compression auditable. + +### Compression Summary + +The metadata doubles as a compression summary: compare +`original_tokens` to `compressed_tokens` to see how much context was +removed. The summary is included in `plan status` CLI output under +the `skeleton` key. + +## Example: Multi-Decision Plan + +Consider a plan with three decision context fragments: + +```text +Fragment A (relevance=0.9, tokens=400, decision=01HX...) +Fragment B (relevance=0.6, tokens=300, decision=01HY...) +Fragment C (relevance=0.3, tokens=300, decision=01HZ...) +``` + +With `skeleton_ratio = 0.5` the token budget is +`1000 * (1 - 0.5) = 500` tokens: + +- Fragment A (400 tokens, cumulative 400) — kept. +- Fragment B (300 tokens, cumulative 700) — exceeds budget; skipped. + +**Result:** + +```text +Compressed fragments: [A] +original_tokens: 1000 +compressed_tokens: 400 +source_decision_ids: (01HX...) +``` + +## Integration + +The skeleton output feeds into the **subplan context inheritance** +pipeline. When a parent plan spawns a child, the strategy coordinator +calls the compressor on the parent's accumulated context, stores the +resulting `SkeletonMetadata` on the child plan, and passes the +compressed fragments as the child's inherited context budget. + +## CLI + +The `--skeleton-ratio` flag on `agents project context set` sets the +ratio for a project's context policy. The `plan status` command +displays the compression summary when `skeleton_metadata` is present. diff --git a/features/skeleton_compressor.feature b/features/skeleton_compressor.feature new file mode 100644 index 000000000..9a6488e99 --- /dev/null +++ b/features/skeleton_compressor.feature @@ -0,0 +1,134 @@ +Feature: Skeleton compressor + As an ACMS subsystem + I want to compress context fragments for subplan inheritance + So that child plans receive relevant context within a token budget + + Background: + Given a skeleton compressor service + + # --- ratio validation ------------------------------------------------- + + Scenario: Reject ratio below 0.0 + Given context fragments with total tokens 1000 + When I compress with skeleton_ratio -0.1 + Then the compressor should raise a ValueError for invalid ratio + + Scenario: Reject ratio above 1.0 + Given context fragments with total tokens 1000 + When I compress with skeleton_ratio 1.5 + Then the compressor should raise a ValueError for invalid ratio + + Scenario: Accept ratio 0.0 + Given context fragments with total tokens 1000 + When I compress with skeleton_ratio 0.0 + Then all fragments should be returned unchanged + + Scenario: Accept ratio 1.0 + Given context fragments with total tokens 1000 + When I compress with skeleton_ratio 1.0 + Then only the highest-relevance fragment should be returned + + Scenario: Accept ratio at boundary 0.5 + Given context fragments with total tokens 1000 + When I compress with skeleton_ratio 0.5 + Then compressed tokens should be at most 500 + + # --- default handling ------------------------------------------------- + + Scenario: Default ratio applied when None + Given context fragments with total tokens 1000 + When I compress with skeleton_ratio not specified + Then the metadata ratio should equal the default 0.3 + + # --- stable ordering -------------------------------------------------- + + Scenario: Fragments with equal relevance are ordered by id + Given three fragments with equal relevance 0.5 + When I compress with skeleton_ratio 0.0 + Then fragments should be ordered by fragment_id ascending + + Scenario: Fragments are ordered by relevance descending + Given fragments with relevances 0.9, 0.3, and 0.7 + When I compress with skeleton_ratio 0.0 + Then the first fragment should have relevance 0.9 + And the last fragment should have relevance 0.3 + + # --- metadata ---------------------------------------------------------- + + Scenario: Metadata records correct token counts + Given context fragments with total tokens 1000 + When I compress with skeleton_ratio 0.5 + Then metadata original_tokens should be 1000 + And metadata compressed_tokens should be at most 500 + + Scenario: Metadata records source decision IDs + Given fragments with known decision IDs + When I compress with skeleton_ratio 0.0 + Then metadata should contain all source decision IDs + + Scenario: Metadata ratio matches input + Given context fragments with total tokens 1000 + When I compress with skeleton_ratio 0.7 + Then metadata ratio should be 0.7 + + # --- edge cases ------------------------------------------------------- + + Scenario: Empty fragment list compresses to empty + Given an empty fragment list + When I compress with skeleton_ratio 0.5 + Then the result should contain zero fragments + And metadata original_tokens should be 0 + And metadata compressed_tokens should equal 0 + + Scenario: Single fragment at ratio 0.5 + Given a single fragment with 100 tokens + When I compress with skeleton_ratio 0.5 + Then the result should contain one fragment + + # --- argument validation ---------------------------------------------- + + Scenario: Reject non-list fragments argument + When I compress with a non-list fragments argument + Then the compressor should raise a TypeError + + Scenario: Reject fragment with negative token count + Given a fragment with negative token count + When I compress with skeleton_ratio 0.5 + Then the compressor should raise a ValueError for invalid fragment + + Scenario: Reject fragment with empty id + Given a fragment with empty fragment_id + When I compress with skeleton_ratio 0.5 + Then the compressor should raise a ValueError for invalid fragment + + Scenario: Reject fragment with relevance out of range + Given a fragment with relevance 1.5 + When I compress with skeleton_ratio 0.5 + Then the compressor should raise a ValueError for invalid fragment + + Scenario: Reject non-ContextFragment item in list + When I compress with a list containing a non-fragment item + Then the compressor should raise a TypeError for invalid item + + Scenario: Reject non-numeric skeleton_ratio + Given context fragments with total tokens 1000 + When I compress with a non-numeric skeleton_ratio + Then the compressor should raise a TypeError for invalid ratio type + + Scenario: Reject skeleton metadata with compressed exceeding original + When I create skeleton metadata with compressed exceeding original + Then a validation error should be raised for compressed exceeding original + + # --- compression summary (original vs compressed) ---------------------- + + Scenario: Compression summary stored in plan metadata + Given context fragments with total tokens 1000 + When I compress with skeleton_ratio 0.6 + Then compressed_tokens should be less than original_tokens + + # --- skeleton_ratio integration with plan model ------------------------ + + Scenario: Plan model accepts skeleton_metadata + 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 diff --git a/features/steps/skeleton_compressor_steps.py b/features/steps/skeleton_compressor_steps.py new file mode 100644 index 000000000..a808d6890 --- /dev/null +++ b/features/steps/skeleton_compressor_steps.py @@ -0,0 +1,419 @@ +"""Step definitions for the Skeleton Compressor feature.""" + +from __future__ import annotations + +from behave import given, then, when +from behave.runner import Context + +from cleveragents.application.services.skeleton_compressor import ( + DEFAULT_SKELETON_RATIO, + ContextFragment, + SkeletonCompressorService, +) +from cleveragents.domain.models.core.plan import ( + NamespacedName, + Plan, + PlanIdentity, + PlanPhase, + ProcessingState, +) +from cleveragents.domain.models.core.skeleton_metadata import SkeletonMetadata + +# --- helpers --------------------------------------------------------------- + + +def _make_fragments(total_tokens: int, count: int = 4) -> list[ContextFragment]: + """Create *count* fragments summing to *total_tokens*.""" + base = total_tokens // count + remainder = total_tokens - base * count + relevances = [0.9, 0.7, 0.5, 0.3] + frags: list[ContextFragment] = [] + for i in range(count): + tokens = base + (remainder if i == 0 else 0) + frags.append( + ContextFragment( + fragment_id=f"frag-{i:03d}", + content=f"content-{i}" * max(1, tokens // 10), + token_count=tokens, + relevance=relevances[i % len(relevances)], + source_decision_id=f"01HX{'A' * 22}{i}" if i < 3 else None, + ) + ) + return frags + + +def _make_plan_id() -> str: + return "01HXAAAAAAAAAAAAAAAAAAAAAA" + + +# --- Background ------------------------------------------------------------ + + +@given("a skeleton compressor service") +def step_create_service(context: Context) -> None: + context.service = SkeletonCompressorService() + + +# --- Fragment setup -------------------------------------------------------- + + +@given("context fragments with total tokens {total:d}") +def step_fragments_total(context: Context, total: int) -> None: + context.fragments = _make_fragments(total) + + +@given("three fragments with equal relevance {rel:g}") +def step_equal_relevance(context: Context, rel: float) -> None: + context.fragments = [ + ContextFragment( + fragment_id=f"frag-{chr(ord('c') - i)}", + content="x" * 50, + token_count=100, + relevance=rel, + ) + for i in range(3) + ] + + +@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), + ] + + +@given("fragments with known decision IDs") +def step_known_ids(context: Context) -> None: + context.fragments = [ + ContextFragment( + fragment_id="f-1", + content="a", + token_count=100, + relevance=0.9, + source_decision_id="01HXDECISION00000000000001", + ), + ContextFragment( + fragment_id="f-2", + content="b", + token_count=100, + relevance=0.5, + source_decision_id="01HXDECISION00000000000002", + ), + ] + + +@given("an empty fragment list") +def step_empty_frags(context: Context) -> None: + context.fragments = [] + + +@given("a single fragment with {tokens:d} tokens") +def step_single_frag(context: Context, tokens: int) -> None: + context.fragments = [ + ContextFragment( + fragment_id="only", + content="x" * tokens, + token_count=tokens, + relevance=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, + ), + ] + + +@given("a fragment with empty fragment_id") +def step_empty_id(context: Context) -> None: + context.fragments = [ + ContextFragment( + fragment_id="", + content="x", + token_count=10, + relevance=0.5, + ), + ] + + +@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, + ), + ] + + +@given( + "a skeleton metadata with ratio {ratio:g} and {orig:d} original tokens and {comp:d} compressed" +) +def step_make_metadata(context: Context, ratio: float, orig: int, comp: int) -> None: + context.skel_meta = SkeletonMetadata( + ratio=ratio, + original_tokens=orig, + compressed_tokens=comp, + source_decision_ids=("01HXDECISION00000000000001",), + ) + + +# --- When clauses ---------------------------------------------------------- + + +@when("I compress with skeleton_ratio {ratio:g}") +def step_compress_ratio(context: Context, ratio: float) -> None: + try: + context.result = context.service.compress( + context.fragments, skeleton_ratio=ratio + ) + context.error = None + except (ValueError, TypeError) as exc: + context.error = exc + context.result = None + + +@when("I compress with skeleton_ratio not specified") +def step_compress_default(context: Context) -> None: + context.result = context.service.compress(context.fragments) + context.error = None + + +@when("I compress with a non-list fragments argument") +def step_compress_non_list(context: Context) -> None: + try: + context.result = context.service.compress("not-a-list", skeleton_ratio=0.5) # type: ignore[arg-type] + context.error = None + except TypeError as exc: + context.error = exc + context.result = None + + +@when("I compress with a list containing a non-fragment item") +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 + ), + "not-a-fragment", + ], # type: ignore[list-item] + skeleton_ratio=0.5, + ) + context.error = None + except TypeError as exc: + context.error = exc + context.result = None + + +@when("I compress with a non-numeric skeleton_ratio") +def step_compress_non_numeric_ratio(context: Context) -> None: + try: + context.result = context.service.compress( + context.fragments, + skeleton_ratio="bad", # type: ignore[arg-type] + ) + context.error = None + except TypeError as exc: + context.error = exc + context.result = None + + +@when("I create skeleton metadata with compressed exceeding original") +def step_create_bad_metadata(context: Context) -> None: + try: + context.bad_meta = SkeletonMetadata( + ratio=0.5, + original_tokens=100, + compressed_tokens=200, + ) + context.meta_error = None + except Exception as exc: + context.meta_error = exc + context.bad_meta = None + + +@when("I attach skeleton_metadata to a plan") +def step_attach_to_plan(context: Context) -> None: + context.plan = Plan( + identity=PlanIdentity(plan_id=_make_plan_id()), + namespaced_name=NamespacedName.parse("local/test-plan"), + description="Test plan", + action_name="local/test-action", + phase=PlanPhase.STRATEGIZE, + processing_state=ProcessingState.QUEUED, + skeleton_metadata=context.skel_meta, + ) + + +# --- Then clauses ---------------------------------------------------------- + + +@then("the compressor should raise a ValueError for invalid ratio") +def step_check_value_error_ratio(context: Context) -> None: + assert context.error is not None, "Expected ValueError" + assert isinstance(context.error, ValueError), ( + f"Expected ValueError, got {type(context.error)}" + ) + + +@then("all fragments should be returned unchanged") +def step_all_returned(context: Context) -> None: + assert context.result is not None + assert len(context.result.fragments) == len(context.fragments) + + +@then("only the highest-relevance fragment should be returned") +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 + + +@then("compressed tokens should be at most {limit:d}") +def step_tokens_limit(context: Context, limit: int) -> None: + assert context.result is not None + assert context.result.metadata.compressed_tokens <= limit + + +@then("the metadata ratio should equal the default {expected:g}") +def step_default_ratio(context: Context, expected: float) -> None: + assert context.result is not None + assert context.result.metadata.ratio == expected + assert expected == DEFAULT_SKELETON_RATIO + + +@then("fragments should be ordered by fragment_id ascending") +def step_ordered_by_id(context: Context) -> None: + assert context.result is not None + ids = [f.fragment_id for f in context.result.fragments] + assert ids == sorted(ids), f"Expected sorted IDs, got {ids}" + + +@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 + + +@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 + + +@then("metadata original_tokens should be {expected:d}") +def step_original_tokens(context: Context, expected: int) -> None: + assert context.result is not None + assert context.result.metadata.original_tokens == expected + + +@then("metadata compressed_tokens should equal {expected:d}") +def step_compressed_equals(context: Context, expected: int) -> None: + assert context.result is not None + assert context.result.metadata.compressed_tokens == expected + + +@then("metadata compressed_tokens should be at most {limit:d}") +def step_compressed_at_most(context: Context, limit: int) -> None: + assert context.result is not None + assert context.result.metadata.compressed_tokens <= limit + + +@then("metadata should contain all source decision IDs") +def step_all_decision_ids(context: Context) -> None: + assert context.result is not None + expected_ids = { + f.source_decision_id + for f in context.fragments + if f.source_decision_id is not None + } + actual_ids = set(context.result.metadata.source_decision_ids) + assert expected_ids == actual_ids + + +@then("metadata ratio should be {expected:g}") +def step_ratio_value(context: Context, expected: float) -> None: + assert context.result is not None + assert context.result.metadata.ratio == expected + + +@then("the result should contain zero fragments") +def step_zero_frags(context: Context) -> None: + assert context.result is not None + assert len(context.result.fragments) == 0 + + +@then("the result should contain one fragment") +def step_one_frag(context: Context) -> None: + assert context.result is not None + assert len(context.result.fragments) == 1 + + +@then("the compressor should raise a TypeError") +def step_type_error(context: Context) -> None: + assert context.error is not None, "Expected TypeError" + assert isinstance(context.error, TypeError), ( + f"Expected TypeError, got {type(context.error)}" + ) + + +@then("the compressor should raise a ValueError for invalid fragment") +def step_frag_value_error(context: Context) -> None: + assert context.error is not None, "Expected ValueError" + assert isinstance(context.error, ValueError), ( + f"Expected ValueError, got {type(context.error)}" + ) + + +@then("the compressor should raise a TypeError for invalid item") +def step_type_error_item(context: Context) -> None: + assert context.error is not None, "Expected TypeError" + assert isinstance(context.error, TypeError), ( + f"Expected TypeError, got {type(context.error)}" + ) + + +@then("the compressor should raise a TypeError for invalid ratio type") +def step_type_error_ratio_type(context: Context) -> None: + assert context.error is not None, "Expected TypeError" + assert isinstance(context.error, TypeError), ( + f"Expected TypeError, got {type(context.error)}" + ) + + +@then("a validation error should be raised for compressed exceeding original") +def step_validation_error_compressed(context: Context) -> None: + assert context.meta_error is not None, "Expected validation error" + + +@then("compressed_tokens should be less than original_tokens") +def step_less_tokens(context: Context) -> None: + assert context.result is not None + assert ( + context.result.metadata.compressed_tokens + < context.result.metadata.original_tokens + ) + + +@then("the plan should expose skeleton metadata in cli dict") +def step_plan_cli_dict(context: Context) -> None: + cli_dict = context.plan.as_cli_dict() + assert "skeleton" in cli_dict + skel = cli_dict["skeleton"] + 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 diff --git a/robot/helper_skeleton_compressor.py b/robot/helper_skeleton_compressor.py new file mode 100644 index 000000000..ab68fc5c7 --- /dev/null +++ b/robot/helper_skeleton_compressor.py @@ -0,0 +1,163 @@ +"""Helper script for skeleton compressor Robot Framework tests. + +Usage: + python helper_skeleton_compressor.py compress + python helper_skeleton_compressor.py validate-ratio-bounds + python helper_skeleton_compressor.py metadata-fields + python helper_skeleton_compressor.py stable-ordering +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +# Ensure source tree is importable +_SRC = str(Path(__file__).resolve().parents[1] / "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from cleveragents.application.services.skeleton_compressor import ( # noqa: E402 + ContextFragment, + SkeletonCompressorService, +) + + +def _sample_fragments() -> list[ContextFragment]: + """Build a repeatable set of sample fragments.""" + return [ + ContextFragment( + fragment_id="frag-001", + content="High relevance content " * 20, + token_count=400, + relevance=0.9, + source_decision_id="01HXDECISION00000000000001", + ), + ContextFragment( + fragment_id="frag-002", + content="Medium relevance content " * 15, + token_count=300, + relevance=0.6, + source_decision_id="01HXDECISION00000000000002", + ), + ContextFragment( + fragment_id="frag-003", + content="Low relevance content " * 15, + token_count=300, + relevance=0.3, + source_decision_id="01HXDECISION00000000000003", + ), + ] + + +def cmd_compress(ratio_str: str) -> None: + """Compress sample fragments and print summary.""" + ratio = float(ratio_str) + svc = SkeletonCompressorService() + result = svc.compress(_sample_fragments(), skeleton_ratio=ratio) + meta = result.metadata + print(f"skeleton-compress-ok ratio={meta.ratio}") + print(f"original_tokens={meta.original_tokens}") + print(f"compressed_tokens={meta.compressed_tokens}") + print(f"fragment_count={len(result.fragments)}") + print(f"decision_ids={len(meta.source_decision_ids)}") + + +def cmd_validate_ratio_bounds() -> None: + """Verify that out-of-range ratios raise ValueError.""" + svc = SkeletonCompressorService() + frags = _sample_fragments() + + for bad in (-0.1, 1.5, 2.0): + try: + svc.compress(frags, skeleton_ratio=bad) + print(f"FAIL: ratio {bad} did not raise") + sys.exit(1) + except ValueError: + pass + + # Valid bounds + for good in (0.0, 0.5, 1.0): + svc.compress(frags, skeleton_ratio=good) + + print("skeleton-ratio-bounds-ok") + + +def cmd_metadata_fields() -> None: + """Verify metadata fields are populated correctly.""" + svc = SkeletonCompressorService() + result = svc.compress(_sample_fragments(), skeleton_ratio=0.5) + meta = result.metadata + + checks_passed = True + + if meta.ratio != 0.5: + print(f"FAIL: ratio={meta.ratio}") + checks_passed = False + if meta.original_tokens != 1000: + print(f"FAIL: original_tokens={meta.original_tokens}") + checks_passed = False + if meta.compressed_tokens > 500: + print(f"FAIL: compressed_tokens={meta.compressed_tokens} > 500") + checks_passed = False + if not meta.source_decision_ids: + print("FAIL: no decision IDs") + checks_passed = False + + if checks_passed: + print("skeleton-metadata-ok") + else: + sys.exit(1) + + +def cmd_stable_ordering() -> None: + """Verify fragments come out in deterministic order.""" + svc = SkeletonCompressorService() + frags = [ + ContextFragment( + fragment_id=f"frag-{chr(ord('c') - i)}", + content="x", + token_count=10, + relevance=0.5, + ) + for i in range(3) + ] + + r1 = svc.compress(frags, skeleton_ratio=0.0) + r2 = svc.compress(list(reversed(frags)), skeleton_ratio=0.0) + + ids1 = [f.fragment_id for f in r1.fragments] + ids2 = [f.fragment_id for f in r2.fragments] + + if ids1 == ids2: + print("skeleton-stable-ordering-ok") + else: + print(f"FAIL: {ids1} != {ids2}") + sys.exit(1) + + +def main() -> None: + """Dispatch subcommand.""" + if len(sys.argv) < 2: + print("Usage: helper_skeleton_compressor.py [args]") + sys.exit(1) + + cmd = sys.argv[1] + if cmd == "compress": + if len(sys.argv) < 3: + print("Usage: compress ") + sys.exit(1) + cmd_compress(sys.argv[2]) + elif cmd == "validate-ratio-bounds": + cmd_validate_ratio_bounds() + elif cmd == "metadata-fields": + cmd_metadata_fields() + elif cmd == "stable-ordering": + cmd_stable_ordering() + else: + print(f"Unknown command: {cmd}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/robot/skeleton_compressor.robot b/robot/skeleton_compressor.robot new file mode 100644 index 000000000..996acc238 --- /dev/null +++ b/robot/skeleton_compressor.robot @@ -0,0 +1,49 @@ +*** Settings *** +Documentation Smoke tests for skeleton compressor service +Resource ${CURDIR}/common.resource +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Variables *** +${HELPER} ${CURDIR}/helper_skeleton_compressor.py + +*** Test Cases *** +Compress Fragments At Ratio 0.5 + [Documentation] Compress sample fragments at 50% ratio and verify output + ${result}= Run Process ${PYTHON} ${HELPER} compress 0.5 cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} skeleton-compress-ok + +Compress Fragments At Ratio 0.0 + [Documentation] No compression — all fragments should survive + ${result}= Run Process ${PYTHON} ${HELPER} compress 0.0 cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} skeleton-compress-ok + Should Contain ${result.stdout} fragment_count=3 + +Compress Fragments At Ratio 1.0 + [Documentation] Maximum compression — only top fragment should survive + ${result}= Run Process ${PYTHON} ${HELPER} compress 1.0 cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} skeleton-compress-ok + Should Contain ${result.stdout} fragment_count=1 + +Validate Ratio Bounds + [Documentation] Out-of-range ratios must raise ValueError + ${result}= Run Process ${PYTHON} ${HELPER} validate-ratio-bounds cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} skeleton-ratio-bounds-ok + +Verify Metadata Fields + [Documentation] Metadata should record ratio, tokens, and decision IDs + ${result}= Run Process ${PYTHON} ${HELPER} metadata-fields cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} skeleton-metadata-ok + +Verify Stable Fragment Ordering + [Documentation] Fragments with equal relevance must be ordered deterministically + ${result}= Run Process ${PYTHON} ${HELPER} stable-ordering cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} skeleton-stable-ordering-ok diff --git a/src/cleveragents/application/container.py b/src/cleveragents/application/container.py index c75eba5e5..71650ee8b 100644 --- a/src/cleveragents/application/container.py +++ b/src/cleveragents/application/container.py @@ -25,6 +25,9 @@ from cleveragents.application.services.project_service import ProjectService from cleveragents.application.services.resource_registry_service import ( ResourceRegistryService, ) +from cleveragents.application.services.skeleton_compressor import ( + SkeletonCompressorService, +) from cleveragents.application.services.subplan_service import SubplanService from cleveragents.application.services.vector_store_service import VectorStoreService from cleveragents.config.settings import Settings, get_settings @@ -296,6 +299,11 @@ class Container(containers.DeclarativeContainer): database_url=database_url, ) + # Skeleton Compressor Service - stateless, Singleton is sufficient + skeleton_compressor_service = providers.Singleton( + SkeletonCompressorService, + ) + # Autonomy Guardrail Service - Singleton so all callers share state autonomy_guardrail_service = providers.Singleton( AutonomyGuardrailService, diff --git a/src/cleveragents/application/services/__init__.py b/src/cleveragents/application/services/__init__.py index 733698046..de3faaf46 100644 --- a/src/cleveragents/application/services/__init__.py +++ b/src/cleveragents/application/services/__init__.py @@ -57,6 +57,11 @@ from cleveragents.application.services.semantic_validation_service import ( from cleveragents.application.services.session_service import ( PersistentSessionService, ) +from cleveragents.application.services.skeleton_compressor import ( + CompressionResult, + ContextFragment, + SkeletonCompressorService, +) from cleveragents.application.services.skill_registry_service import ( SkillRegistryService, ) @@ -106,9 +111,11 @@ __all__ = [ "AttachmentScope", "AutonomyGuardrailService", "BrokenReferenceRule", + "CompressionResult", "ConfigEntry", "ConfigLevel", "ConfigService", + "ContextFragment", "CorrectionService", "DecisionService", "DefaultValidationRunner", @@ -133,6 +140,7 @@ __all__ = [ "SemanticValidationRule", "SemanticValidationService", "SemanticValidationSeverity", + "SkeletonCompressorService", "SkillRegistryService", "SpawnEntry", "SpawnMetadata", diff --git a/src/cleveragents/application/services/skeleton_compressor.py b/src/cleveragents/application/services/skeleton_compressor.py new file mode 100644 index 000000000..563b34eeb --- /dev/null +++ b/src/cleveragents/application/services/skeleton_compressor.py @@ -0,0 +1,226 @@ +"""Skeleton compressor service for ACMS context inheritance. + +The ``SkeletonCompressorService`` takes a collection of context +fragments from a parent plan and produces a compressed representation +suitable for propagation to child plans. The compression is governed +by ``skeleton_ratio``: + +- **0.0** — no compression; all fragments pass through unchanged. +- **1.0** — maximum compression; only the single highest-relevance + fragment is kept (with minimal content). + +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. + +Based on ``docs/specification.md`` ACMS Skeleton section. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from cleveragents.domain.models.core.skeleton_metadata import SkeletonMetadata + +# --------------------------------------------------------------------------- +# Public data structures +# --------------------------------------------------------------------------- + + +@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. + + Attributes: + fragments: The compressed (filtered/truncated) fragments, + ordered by relevance descending then fragment_id ascending. + metadata: Auditable metadata for the compression pass. + """ + + fragments: tuple[ContextFragment, ...] + metadata: SkeletonMetadata + + +# --------------------------------------------------------------------------- +# Service +# --------------------------------------------------------------------------- + +# Default skeleton_ratio when the caller does not specify one. +DEFAULT_SKELETON_RATIO: float = 0.3 + + +class SkeletonCompressorService: + """Compress context fragments for subplan context inheritance. + + The service is stateless; all state lives in the arguments and + the returned ``CompressionResult``. + """ + + def compress( + self, + fragments: list[ContextFragment], + skeleton_ratio: float | None = None, + ) -> CompressionResult: + """Compress *fragments* according to *skeleton_ratio*. + + Args: + fragments: Context fragments to compress. Each must + have ``token_count >= 0`` and ``relevance`` in + ``[0.0, 1.0]``. + skeleton_ratio: Compression ratio in ``[0.0, 1.0]``. + ``None`` falls back to ``DEFAULT_SKELETON_RATIO``. + + Returns: + A ``CompressionResult`` containing the filtered fragments + and associated ``SkeletonMetadata``. + + Raises: + ValueError: If *skeleton_ratio* is outside ``[0.0, 1.0]`` + or any fragment has invalid fields. + TypeError: If *fragments* is not a list. + """ + # -- argument validation ------------------------------------------ + if not isinstance(fragments, list): + raise TypeError(f"fragments must be a list, got {type(fragments).__name__}") + + effective_ratio = ( + skeleton_ratio if skeleton_ratio is not None else DEFAULT_SKELETON_RATIO + ) + + if not isinstance(effective_ratio, (int, float)): + raise TypeError( + f"skeleton_ratio must be a float, got {type(effective_ratio).__name__}" + ) + + if effective_ratio < 0.0 or effective_ratio > 1.0: + raise ValueError( + f"skeleton_ratio must be in [0.0, 1.0], got {effective_ratio}" + ) + + self._validate_fragments(fragments) + + # -- compute totals ----------------------------------------------- + original_tokens = sum(f.token_count for f in fragments) + + # -- stable sort: relevance desc, fragment_id asc ----------------- + sorted_fragments = sorted( + fragments, + key=lambda f: (-f.relevance, f.fragment_id), + ) + + # -- select fragments within budget ------------------------------- + kept = self._select_fragments(sorted_fragments, effective_ratio) + + 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 + ) + + metadata = SkeletonMetadata( + ratio=effective_ratio, + original_tokens=original_tokens, + compressed_tokens=compressed_tokens, + source_decision_ids=source_ids, + ) + + return CompressionResult( + fragments=tuple(kept), + metadata=metadata, + ) + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + @staticmethod + def _validate_fragments(fragments: list[ContextFragment]) -> None: + """Validate every fragment in the list. + + Raises: + TypeError: If an element is not a ``ContextFragment``. + ValueError: If a fragment has invalid field values. + """ + for idx, frag in enumerate(fragments): + if not isinstance(frag, ContextFragment): + raise TypeError( + f"fragments[{idx}] must be a ContextFragment, " + f"got {type(frag).__name__}" + ) + if frag.token_count < 0: + raise ValueError( + f"fragments[{idx}].token_count must be >= 0, got {frag.token_count}" + ) + if frag.relevance < 0.0 or frag.relevance > 1.0: + raise ValueError( + f"fragments[{idx}].relevance must be in [0.0, 1.0], " + f"got {frag.relevance}" + ) + if not frag.fragment_id: + raise ValueError(f"fragments[{idx}].fragment_id must be non-empty") + + @staticmethod + def _select_fragments( + sorted_fragments: list[ContextFragment], + ratio: float, + ) -> list[ContextFragment]: + """Select which fragments to keep given the compression ratio. + + When *ratio* is 0.0 every fragment is kept. When *ratio* is + 1.0 only the single highest-relevance fragment survives (or + none if the input is empty). For intermediate values the + token budget is ``original_tokens * (1 - ratio)``; fragments + are added in relevance order until the budget is exhausted. + """ + if not sorted_fragments: + return [] + + if ratio == 0.0: + return list(sorted_fragments) + + original_tokens = sum(f.token_count for f in sorted_fragments) + + # Budget: fraction of tokens to *keep* + budget = int(original_tokens * (1.0 - ratio)) + + # At maximum compression keep at most one fragment + if ratio == 1.0: + budget = 0 + + kept: list[ContextFragment] = [] + used = 0 + for frag in sorted_fragments: + if used + frag.token_count > budget and kept: + # Already have at least one fragment and budget exceeded + break + kept.append(frag) + used += frag.token_count + if used >= budget and budget > 0: + break + + # At ratio 1.0, keep exactly the top fragment + if ratio == 1.0 and sorted_fragments: + return [sorted_fragments[0]] + + return kept diff --git a/src/cleveragents/domain/models/core/__init__.py b/src/cleveragents/domain/models/core/__init__.py index 2e9778603..0c7e51477 100644 --- a/src/cleveragents/domain/models/core/__init__.py +++ b/src/cleveragents/domain/models/core/__init__.py @@ -215,6 +215,9 @@ from cleveragents.domain.models.core.session import ( SessionTokenUsage, ) +# Skeleton compression metadata (M6 — ACMS) +from cleveragents.domain.models.core.skeleton_metadata import SkeletonMetadata + # Skill domain models from cleveragents.domain.models.core.skill import ( ResolvedToolEntry, @@ -376,6 +379,7 @@ __all__ = [ "SessionService", "SessionServiceError", "SessionTokenUsage", + "SkeletonMetadata", "Skill", "SkillAgentSource", "SkillCapabilitySummary", diff --git a/src/cleveragents/domain/models/core/plan.py b/src/cleveragents/domain/models/core/plan.py index b52efd4bb..82ac52c04 100644 --- a/src/cleveragents/domain/models/core/plan.py +++ b/src/cleveragents/domain/models/core/plan.py @@ -64,6 +64,7 @@ from typing import Any, ClassVar from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from cleveragents.domain.models.core.cost_metadata import CostMetadata +from cleveragents.domain.models.core.skeleton_metadata import SkeletonMetadata # ULID is 26 characters, all uppercase alphanumeric (Crockford's base32) ULID_PATTERN = r"^[0-9A-HJKMNP-TV-Z]{26}$" @@ -648,6 +649,15 @@ class Plan(BaseModel): description="Token/cost tracking and budget exhaustion events", ) + # Skeleton compression metadata (M6 — ACMS context inheritance) + skeleton_metadata: SkeletonMetadata | None = Field( + default=None, + description=( + "Metadata from skeleton compression pass used for " + "subplan context inheritance auditability" + ), + ) + # Metadata created_by: str | None = Field(None, description="User/session that created plan") tags: list[str] = Field(default_factory=list, description="Tags for organization") @@ -885,6 +895,13 @@ class Plan(BaseModel): result["subplan_count"] = len(self.subplan_statuses) if self.cost_metadata is not None: result["cost"] = self.cost_metadata.as_display_dict() + if self.skeleton_metadata is not None: + result["skeleton"] = { + "ratio": self.skeleton_metadata.ratio, + "original_tokens": self.skeleton_metadata.original_tokens, + "compressed_tokens": self.skeleton_metadata.compressed_tokens, + "source_decision_ids": list(self.skeleton_metadata.source_decision_ids), + } if self.last_completed_step >= 0: result["last_completed_step"] = self.last_completed_step if self.last_checkpoint_id: diff --git a/src/cleveragents/domain/models/core/skeleton_metadata.py b/src/cleveragents/domain/models/core/skeleton_metadata.py new file mode 100644 index 000000000..6fe936231 --- /dev/null +++ b/src/cleveragents/domain/models/core/skeleton_metadata.py @@ -0,0 +1,79 @@ +"""Skeleton metadata model for ACMS context compression. + +A ``SkeletonMetadata`` captures the auditable state of a skeleton +compression pass: the ratio applied, token counts before and after +compression, and the decision IDs whose context fragments were +compressed. The model is frozen (immutable) so that once persisted +it cannot be accidentally mutated. + +Based on ``docs/specification.md`` ACMS Skeleton section. +""" + +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +class SkeletonMetadata(BaseModel): + """Immutable record of a skeleton compression pass. + + Persisted alongside the plan to provide full auditability of + context inheritance between parent and child plans. + + Attributes: + ratio: The skeleton_ratio applied (0.0 = no compression, + 1.0 = maximum compression). + original_tokens: Total token count before compression. + compressed_tokens: Total token count after compression. + source_decision_ids: Decision ULIDs whose context fragments + were included in the compression input. + """ + + ratio: float = Field( + ..., + ge=0.0, + le=1.0, + description=( + "Skeleton compression ratio applied " + "(0.0 = no compression, 1.0 = maximum compression)" + ), + ) + original_tokens: int = Field( + ..., + ge=0, + description="Total token count before compression", + ) + compressed_tokens: int = Field( + ..., + ge=0, + description="Total token count after compression", + ) + source_decision_ids: tuple[str, ...] = Field( + default_factory=tuple, + description=( + "Decision ULIDs whose context fragments " + "were included in the compression input" + ), + ) + + @field_validator("compressed_tokens") + @classmethod + def compressed_not_greater_than_original( + cls: type[SkeletonMetadata], + v: int, + info: object, + ) -> int: + """Ensure compressed tokens do not exceed original tokens.""" + # info.data contains already-validated fields at this point + data = getattr(info, "data", {}) + original = data.get("original_tokens") + if original is not None and v > original: + raise ValueError( + f"compressed_tokens ({v}) cannot exceed original_tokens ({original})" + ) + return v + + model_config = ConfigDict( + frozen=True, + str_strip_whitespace=True, + ) diff --git a/vulture_whitelist.py b/vulture_whitelist.py index b4ffcb0a5..e081510fe 100644 --- a/vulture_whitelist.py +++ b/vulture_whitelist.py @@ -505,6 +505,19 @@ get_spawn_decisions # noqa: B018, F821 build_spawn_entries # noqa: B018, F821 validate_spawn # noqa: B018, F821 +# Skeleton compressor — public API (M6 ACMS, issue #194) +SkeletonMetadata # noqa: B018, F821 +SkeletonCompressorService # noqa: B018, F821 +ContextFragment # noqa: B018, F821 +CompressionResult # noqa: B018, F821 +DEFAULT_SKELETON_RATIO # noqa: B018, F821 +skeleton_compressor_service # noqa: B018, F821 +skeleton_metadata # noqa: B018, F821 +source_decision_ids # noqa: B018, F821 +compressed_tokens # noqa: B018, F821 +original_tokens # noqa: B018, F821 +fragment_id # noqa: B018, F821 + # Server client stubs — public API for server mode (#201) ServerClient # noqa: B018, F821 RemoteExecutionClient # noqa: B018, F821 -- 2.52.0