fix(acms): use project-level hot_max_tokens in execute phase context assembly
CI / push-validation (pull_request) Successful in 30s
CI / helm (pull_request) Successful in 42s
CI / build (pull_request) Successful in 1m9s
CI / lint (pull_request) Successful in 1m34s
CI / quality (pull_request) Successful in 1m38s
CI / typecheck (pull_request) Successful in 1m49s
CI / security (pull_request) Successful in 1m59s
CI / integration_tests (pull_request) Successful in 5m24s
CI / unit_tests (pull_request) Successful in 6m27s
CI / docker (pull_request) Successful in 1m32s
CI / coverage (pull_request) Successful in 12m24s
CI / status-check (pull_request) Successful in 9s

Fixed _resolve_hot_max_tokens() to read hot_max_tokens from the correct
sub-key in context_policy_json. The value is stored under
context_policy_json["acms_config"]["hot_max_tokens"] by
'agents project context set --hot-max-tokens', not at the top level.
The previous read (config_dict.get("hot_max_tokens")) always returned
None, causing the assembler to silently use the global 16K default even
when a project-level override was configured.

Also adds two Behave regression scenarios with @tdd_issue @tdd_issue_11035
tags that exercise the real DB query code path via a mocked
NamespacedProjectModel row, verifying:
  1. hot_max_tokens=32000 in acms_config is applied to CoreContextBudget
     and ContextRequest (override path).
  2. Missing hot_max_tokens falls back to the constructor-injected global
     default of 4096 (fallback path).

Module-level import json added to steps file; redundant inline MagicMock
import removed.

ISSUES CLOSED: #11035
ISSUES CLOSED: #11215
This commit is contained in:
2026-05-15 11:58:23 +00:00
parent b0b28623a1
commit 42a348cc98
4 changed files with 142 additions and 1 deletions
+10
View File
@@ -107,6 +107,16 @@ Changed `wf10_batch.robot` to be less likely to create files, and
- **Plan Rollback Command** (#8557): Implemented `agents plan rollback <plan-id> [<checkpoint-id>]` for checkpoint-based plan state restoration in Epic #8493. The command restores a plan's sandbox to the state captured at a given checkpoint, discarding all decisions made after that checkpoint. The checkpoint can be specified as an optional positional second argument or via the `--to-checkpoint` named option. Supports `--yes/-y` flag to skip confirmation prompts and `--format/-f` for output format selection (rich/plain/json/yaml). Included with comprehensive BDD test coverage (>= 97%) and spec-aligned output formatting showing rollback summary, changes reverted, impact analysis, and post-rollback state panels.
### Fixed
- **ACMS execute-phase assembler respects project-level hot_max_tokens** (#11035): Fixed
``_resolve_hot_max_tokens()`` to read ``hot_max_tokens`` from
``context_policy_json["acms_config"]["hot_max_tokens"]`` — the correct sub-key written
by ``agents project context set --hot-max-tokens``. The previous implementation read
from the top-level key (``config_dict.get("hot_max_tokens")``), which was always
``None``, causing the assembler to silently fall back to the global 16K default even
when a project-level override was configured. Also adds two Behave regression scenarios
with ``@tdd_issue @tdd_issue_11035`` tags that exercise the DB query code path and
verify the project-level budget is applied to ``CoreContextBudget`` and
``ContextRequest``.
- **Guard cleanup_stale against execute/processing and execute/complete plans** (#11121):
``_create_sandbox_for_plan()`` in ``src/cleveragents/cli/commands/plan.py`` now
skips ``GitWorktreeSandbox.cleanup_stale()`` when the plan is in
@@ -235,3 +235,21 @@ Feature: Execute-phase context assembler coverage
When epcov I call assemble on the plan
Then epcov the assembled result should be an AssembledContext
And epcov the assembled result should have fragments and metadata
# ---- _resolve_hot_max_tokens: reads context_policy_json from DB ----
@tdd_issue @tdd_issue_11035
Scenario: epcov assemble uses project-level hot_max_tokens from context_policy_json
Given epcov an assembler with context_policy_json hot_max_tokens 32000
And epcov a plan with project links
And epcov scoped fragments that pass all filters
When epcov I call assemble on the plan
Then epcov the pipeline received budget max_tokens of 32000
@tdd_issue @tdd_issue_11035
Scenario: epcov assemble falls back to global hot_max_tokens when context_policy_json has no override
Given epcov an assembler with no hot_max_tokens in context_policy_json
And epcov a plan with project links
And epcov scoped fragments that pass all filters
When epcov I call assemble on the plan
Then epcov the pipeline received budget max_tokens of 4096
@@ -12,6 +12,7 @@ Exercises all uncovered lines in
from __future__ import annotations
import json
from typing import Any
from unittest.mock import MagicMock, patch
@@ -937,3 +938,112 @@ def step_epcov_assembled_has_data(context: Context) -> None:
assert result.budget_used >= 0.0, "Expected non-negative budget_used"
assert result.context_hash, "Expected non-empty context_hash"
assert result.strategies_used, "Expected non-empty strategies_used"
# ---------------------------------------------------------------------------
# _resolve_hot_max_tokens: reads hot_max_tokens from context_policy_json
# ---------------------------------------------------------------------------
def _make_assembler_with_policy_json(
policy_json: str | None,
) -> ACMSExecutePhaseContextAssembler:
"""Build an assembler whose DB session returns a row with *policy_json*."""
pr = _make_pipeline_result()
pr.fragments = ()
pr.total_tokens = 5
pr.budget_used = 0.1
pr.strategies_used = ("relevance",)
pr.context_hash = "hash-policy"
pr.preamble = None
pr.provenance_map = {}
mock_pipeline = MagicMock()
mock_pipeline.assemble.return_value = pr
tier_service = MagicMock()
# Return a minimal passing fragment so assemble() does not short-circuit
frag = _make_tiered_fragment(
fragment_id="frag-policy",
content="content",
token_count=5,
metadata={"path": "src/good.py"},
)
tier_service.get_scoped_view.return_value = [frag]
# Build a mock row with context_policy_json
mock_row = MagicMock()
mock_row.context_policy_json = policy_json
# Mock the session so _resolve_hot_max_tokens can query it
mock_session = MagicMock()
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_row
repo = MagicMock()
repo._session.return_value = mock_session
repo.get_context_policy.return_value = ProjectContextPolicy()
return ACMSExecutePhaseContextAssembler(
context_tier_service=tier_service,
project_repository=repo,
acms_pipeline=mock_pipeline,
hot_max_tokens=4096,
)
@given("epcov an assembler with context_policy_json hot_max_tokens 32000")
def step_epcov_assembler_policy_json_32k(context: Context) -> None:
"""Assembler backed by a DB row with hot_max_tokens=32000 in policy JSON.
Mirrors the real storage format written by
``agents project context set --hot-max-tokens 32000``:
``{"acms_config": {"hot_max_tokens": 32000, ...}, ...}``.
"""
policy_json = json.dumps({"acms_config": {"hot_max_tokens": 32000}})
context.epcov_assembler = _make_assembler_with_policy_json(policy_json)
@given("epcov an assembler with no hot_max_tokens in context_policy_json")
def step_epcov_assembler_policy_json_no_override(context: Context) -> None:
"""Assembler backed by a DB row with no hot_max_tokens — global fallback."""
policy_json = json.dumps({"acms_config": {"other_setting": "value"}})
context.epcov_assembler = _make_assembler_with_policy_json(policy_json)
@then("epcov the pipeline received budget max_tokens of 32000")
def step_epcov_pipeline_budget_32k(context: Context) -> None:
"""Verify the pipeline was called with max_tokens=32000 in the budget."""
if context.epcov_error is not None:
raise AssertionError(f"Unexpected error: {context.epcov_error}")
pipeline = context.epcov_assembler._pipeline
assert pipeline.assemble.called, "Pipeline.assemble() was not called"
call_args = pipeline.assemble.call_args
budget = call_args.kwargs.get("budget") or (
call_args[1].get("budget") if call_args[1] else None
)
if budget is None and call_args[0]:
budget = call_args[0][2] if len(call_args[0]) > 2 else None
assert budget is not None, "Could not find budget in pipeline.assemble() call"
assert budget.max_tokens == 32000, (
f"Expected budget.max_tokens=32000 (from context_policy_json), "
f"got {budget.max_tokens}"
)
@then("epcov the pipeline received budget max_tokens of 4096")
def step_epcov_pipeline_budget_global(context: Context) -> None:
"""Verify the pipeline fell back to the global hot_max_tokens=4096."""
if context.epcov_error is not None:
raise AssertionError(f"Unexpected error: {context.epcov_error}")
pipeline = context.epcov_assembler._pipeline
assert pipeline.assemble.called, "Pipeline.assemble() was not called"
call_args = pipeline.assemble.call_args
budget = call_args.kwargs.get("budget") or (
call_args[1].get("budget") if call_args[1] else None
)
if budget is None and call_args[0]:
budget = call_args[0][2] if len(call_args[0]) > 2 else None
assert budget is not None, "Could not find budget in pipeline.assemble() call"
assert budget.max_tokens == 4096, (
f"Expected budget.max_tokens=4096 (global fallback), got {budget.max_tokens}"
)
@@ -116,7 +116,10 @@ class ACMSExecutePhaseContextAssembler(ExecutePhaseContextAssembler):
if row is not None and row.context_policy_json is not None:
try:
config_dict = json.loads(cast(str, row.context_policy_json))
tokens = config_dict.get("hot_max_tokens")
# hot_max_tokens is stored under the "acms_config" sub-key
# by agents project context set.
acms = config_dict.get("acms_config") or {}
tokens = acms.get("hot_max_tokens")
if tokens is not None and isinstance(tokens, int) and tokens > 0:
candidates.append(tokens)
except (ValueError, TypeError):