fix: persist strategy decisions via DecisionService during strategize (#10813) #11194
@@ -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.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
|
||||
* Luis Mendes <luis.p.mendes@gmail.com>
|
||||
* Rui Hu <rui.hu@cleverthis.com>
|
||||
* HAL 9000 <hal9000@cleverthis.com> 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 <plan-id> [<checkpoint-id>]` 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'`.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user