diff --git a/CHANGELOG.md b/CHANGELOG.md index bdea20376..672c4835e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Changed `wf10_batch.robot` to be less likely to create files, and when separate `provider`/`model` keys are absent. Added validation to reject malformed combined values with empty provider or model halves. +- **Fixed plan tree reporting zero decision nodes after strategize** (#10813): The `plan tree` command showed no ``decision_id`` fields even though planning completed successfully. Root cause: the PlanExecutor's ``run_strategize()`` method produced strategy decisions via the StrategyActor but never persisted them as domain ``Decision`` objects through the DecisionService wiring. Added ``decision_service`` parameter to the PlanExecutor constructor, wired it from the CLI dependency-injection container in ``_get_plan_executor()``, and added ``_persist_strategy_decisions()`` that converts each strategy decision into a domain Decision with correct type mapping (prompt_definition, strategy_choice, subplan_spawn) so they appear in plan tree output. - **`task-implementor` posts work-started notification comments** (#11031): Both the `issue_impl` and `pr_fix` procedures now post an informational "work started" comment to the Forgejo issue/PR before beginning implementation. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index e97eb3f11..bfa0381b6 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -8,6 +8,7 @@ * Jeffrey Phillips Freeman * Luis Mendes * Rui Hu +* HAL 9000 has contributed fix for #10813 — wiring DecisionService into PlanExecutor for strategy decision persistence during strategize. # Details @@ -44,5 +45,6 @@ Below are some of the specific details of various contributions. * HAL 9000 has contributed database resource types (PostgreSQL, SQLite) with transaction-based sandbox strategy: implemented ``DatabaseResourceHandler`` providing full CRUD operations (`read`, `write`, `delete`, `list_children`) and connection validation with automatic credential masking for PostgreSQL and SQLite backends. Includes ``TransactionSandbox`` infrastructure wired into ``SandboxFactory``, BDD test coverage in ``features/database_resources.feature``, and Robot Framework integration tests in ``robot/database_resources.robot`` (PR #10591 / issue #8608, Epic #8568). * HAL 9000 has contributed the agents plan rollback command (PR #8674 / issue #8557): implemented checkpoint-based plan state restoration with the `agents plan rollback []` CLI command as part of Epic #8493, enabling plans to be restored to previous checkpoints, discarding post-checkpoint decisions, and resuming execution from the rolled-back state. Supported by `--yes/-y`, `--to-checkpoint`, and `--format/-f` flags. Includes comprehensive BDD test coverage (>= 97%) for rollback, decision discarding, and plan resume functionality. * HAL 9000 has contributed the PyYAML security upgrade (PR #11012 / issue #9055): added `pyyaml>=6.0.3` dependency constraint to address known YAML parsing vulnerabilities. +* HAL 9000 has contributed the DecisionService wiring for PlanExecutor strategize persistence fix (#10813): added decision_service to the PlanExecutor constructor and wired it from the CLI dependency-injection container in `_get_plan_executor()`, plus implemented `_persist_strategy_decisions()` to persist strategy decisions as domain `Decision` objects. * HAL 9000 has contributed the A2A module rename standardization BDD tests (PR #10583 / issue #8615): comprehensive Behave test suite validating that all 22 A2A symbols are properly exported from `cleveragents.a2a`, no legacy ACP references remain in the module source, and documentation uses correct A2A naming conventions — fixing inline imports, unused behave symbols, cross-scenario context dependencies, and missing type annotations. * HAL 9000 has contributed the `ActorSelectionOverlay._render` → `_refresh_display` rename fix (PR #11176 / issue #11039, Epic #8174): renamed `_render()` method to `_refresh_display()` to avoid shadowing Textual's `Widget._render()`, fixing a crash in textual >=1.0 where `get_content_height()` would receive `None` and raise `AttributeError: 'NoneType' object has no attribute 'get_height'`. diff --git a/src/cleveragents/application/services/execute_phase_context_assembler.py b/src/cleveragents/application/services/execute_phase_context_assembler.py index 144f18b8d..d0dc746af 100644 --- a/src/cleveragents/application/services/execute_phase_context_assembler.py +++ b/src/cleveragents/application/services/execute_phase_context_assembler.py @@ -3,6 +3,7 @@ from __future__ import annotations import fnmatch +import json from pathlib import PurePath from typing import Any, Protocol @@ -70,6 +71,72 @@ class ACMSExecutePhaseContextAssembler(ExecutePhaseContextAssembler): return ProjectContextPolicy().resolve_view("execute") return policy.resolve_view("execute") + def _resolve_hot_max_tokens(self, project_names: list[str]) -> int: + """Resolve the effective ``hot_max_tokens`` budget for *project_names*. + + Looks up each project's :attr:`ContextConfig.hot_max_tokens` from its + stored context configuration. Projects that do not have an explicit + setting are excluded from the aggregation so they do not affect + the computed value. + + Returns the **maximum** of all explicitly-set project-level values, + falling back to :attr:`_hot_max_tokens` (the caller-passed global + default) when no project overrides are present. + """ + from typing import cast + + candidates: list[int] = [] + for namespaced_name in project_names: + row = None + try: + session = self._project_repository._session() # type: ignore[attr-defined] + from cleveragents.infrastructure.database.models import ( + NamespacedProjectModel, + ) + + row = ( + session.query(NamespacedProjectModel) + .filter_by(namespaced_name=namespaced_name) + .first() + ) + session.close() + except AttributeError: + self._logger.warning( + "hot_max_tokens_session_factory_missing", + project_name=namespaced_name, + ) + continue + except Exception: + self._logger.warning( + "hot_max_tokens_lookup_failed", + project_name=namespaced_name, + exc_info=True, + ) + + 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") + if tokens is not None and isinstance(tokens, int) and tokens > 0: + candidates.append(tokens) + except (ValueError, TypeError): + self._logger.warning( + "hot_max_tokens_parse_failed", + project_name=namespaced_name, + ) + + if candidates: + effective = max(candidates) + self._logger.info( + "hot_max_tokens_resolved_from_projects", + project_names=project_names, + project_values=candidates, + effective=effective, + ) + return effective + # No project overrides --- use the caller-passed global default. + 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. @@ -226,14 +293,21 @@ class ACMSExecutePhaseContextAssembler(ExecutePhaseContextAssembler): ) return None - budget = CoreContextBudget(max_tokens=self._hot_max_tokens, reserved_tokens=0) + # Resolve effective hot_max_tokens: project-level overrides take precedence + # over the global default. When multiple projects have explicit values, + # use the maximum so all projects can contribute within their biggest budget. + effective_hot_max_tokens = self._resolve_hot_max_tokens(project_names) + + budget = CoreContextBudget( + max_tokens=effective_hot_max_tokens, 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_hot_max_tokens, ) payload = self._pipeline.assemble( plan_id=plan.identity.plan_id,