fix(acms): use project-level hot_max_tokens in execute phase context assembly
CI / push-validation (pull_request) Successful in 1m7s
CI / helm (pull_request) Successful in 1m26s
CI / build (pull_request) Successful in 2m38s
CI / quality (pull_request) Successful in 3m29s
CI / lint (pull_request) Successful in 3m45s
CI / security (pull_request) Successful in 3m58s
CI / typecheck (pull_request) Successful in 4m0s
CI / integration_tests (pull_request) Successful in 5m11s
CI / unit_tests (pull_request) Successful in 7m0s
CI / docker (pull_request) Successful in 1m30s
CI / coverage (pull_request) Successful in 11m21s
CI / status-check (pull_request) Successful in 3s

This commit is contained in:
2026-05-15 00:28:54 +00:00
committed by Forgejo
parent ef6829b6f8
commit 796e92197b
4 changed files with 105 additions and 9 deletions
+6
View File
@@ -105,6 +105,12 @@ 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 now respects project-level hot_max_tokens** (#11035): The
``ACMSExecutePhaseContextAssembler._resolve_effective_budget()`` method was added to read
each linked project's ``settings.hot_max_tokens`` and use the maximum override value as the
pipeline budget. Falls back to the constructor-injected global when no override is set. This
ensures users who configure ``agents project context set --hot-max-tokens 32000`` actually see
source files in context that would previously have been excluded by the 16K default budget.
- **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,13 @@ 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
# ---- assemble: project-level hot_max_tokens budget override ----
@tdd_issue @tdd_issue_11035
Scenario: epcov assemble uses project-level hot_max_tokens when set
Given epcov an assembler with project hot_max_tokens set to 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 assembled budget should be 32000
@@ -22,6 +22,8 @@ from cleveragents.application.services.execute_phase_context_assembler import (
ACMSExecutePhaseContextAssembler,
)
from cleveragents.domain.models.acms.crp import AssembledContext
from cleveragents.domain.models.acms.crp import ContextFragment as CRPFragment
from cleveragents.domain.models.acms.crp import FragmentProvenance as CRPProvenance
from cleveragents.domain.models.acms.tiers import TieredFragment
from cleveragents.domain.models.core.context_policy import (
ContextView,
@@ -877,13 +879,6 @@ def step_epcov_scoped_frags_all_excluded(context: Context) -> None:
@given("epcov an assembler with default policy and pipeline")
def step_epcov_assembler_full_pipeline(context: Context) -> None:
from cleveragents.domain.models.acms.crp import (
ContextFragment as CRPFragment,
)
from cleveragents.domain.models.acms.crp import (
FragmentProvenance as CRPProvenance,
)
mock_frag = CRPFragment(
uko_node="res:good",
content="good content here",
@@ -937,3 +932,63 @@ 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"
# ---------------------------------------------------------------------------
# assemble: project-level hot_max_tokens budget override
# ---------------------------------------------------------------------------
@given("epcov an assembler with project hot_max_tokens set to 32000")
def step_epcov_assembler_project_budget(context: Context) -> None:
"""Create an assembler whose project repo returns hot_max_tokens=32000."""
mock_frag = CRPFragment(
uko_node="res:good",
content="good content here",
detail_depth=1,
token_count=10,
relevance_score=0.5,
provenance=CRPProvenance(
resource_uri="res:good",
location="src/good.py",
strategy="execute_phase_context",
),
)
pr = _make_pipeline_result()
pr.fragments = (mock_frag,)
pr.total_tokens = 10
pr.budget_used = 0.25
pr.strategies_used = ("relevance",)
pr.context_hash = "sha256hash"
pr.preamble = "Context preamble"
pr.provenance_map = {"frag-1": {"resource_uri": "res:test"}}
# Build assembler with global budget of 4096
assembler = _make_assembler(pipeline_result=pr)
# Mock the project repo's get() to return a project with hot_max_tokens=32000
mock_project = MagicMock()
mock_project.settings.hot_max_tokens = 32000
assembler._project_repository.get.return_value = mock_project
context.epcov_assembler = assembler
@then("epcov the assembled budget should be 32000")
def step_epcov_assembled_budget_32k(context: Context) -> None:
"""Verify the pipeline was called with the project-level budget (32000)."""
if context.epcov_error is not None:
raise AssertionError(f"Unexpected error: {context.epcov_error}")
# Check the budget passed to the pipeline.assemble() call
pipeline = context.epcov_assembler._pipeline
assert pipeline.assemble.called, "Pipeline.assemble() was not called"
call_kwargs = pipeline.assemble.call_args
# The budget kwarg should reflect the project-level override
budget = call_kwargs.kwargs.get("budget") or call_kwargs[1].get("budget")
if budget is None:
# Try positional args
budget = call_kwargs[0][2] if len(call_kwargs[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, got {budget.max_tokens}"
)
@@ -70,6 +70,30 @@ class ACMSExecutePhaseContextAssembler(ExecutePhaseContextAssembler):
return ProjectContextPolicy().resolve_view("execute")
return policy.resolve_view("execute")
def _resolve_effective_budget(self, project_names: list[str]) -> int:
"""Resolve the effective hot-tier budget from project settings.
Checks each project's ``settings.hot_max_tokens`` and uses the
maximum value found. Falls back to the constructor-injected
global ``self._hot_max_tokens`` when no project-level override
is set.
"""
project_budgets: list[int] = []
for name in project_names:
try:
project = self._project_repository.get(name)
except Exception:
continue
settings = getattr(project, "settings", None)
if settings is None:
continue
budget = getattr(settings, "hot_max_tokens", None)
if isinstance(budget, int) and budget > 0:
project_budgets.append(budget)
if project_budgets:
return max(project_budgets)
return self._hot_max_tokens
@staticmethod
def _path_matches(path: str, include: list[str], exclude: list[str]) -> bool:
"""Return whether *path* passes include/exclude path globs.
@@ -150,6 +174,7 @@ class ACMSExecutePhaseContextAssembler(ExecutePhaseContextAssembler):
return None
views = {name: self._resolve_execute_view(name) for name in project_names}
effective_budget = self._resolve_effective_budget(project_names)
scoped = self._tier.get_scoped_view(project_names)
if not scoped:
return None
@@ -226,14 +251,14 @@ class ACMSExecutePhaseContextAssembler(ExecutePhaseContextAssembler):
)
return None
budget = CoreContextBudget(max_tokens=self._hot_max_tokens, reserved_tokens=0)
budget = CoreContextBudget(max_tokens=effective_budget, reserved_tokens=0)
request = ContextRequest(
query=(
f"Execute-phase context for plan {plan.identity.plan_id} "
f"({', '.join(project_names)})"
),
purpose="llm_execute_phase_prompt",
max_tokens=self._hot_max_tokens,
max_tokens=effective_budget,
)
payload = self._pipeline.assemble(
plan_id=plan.identity.plan_id,