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
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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user