feat(decisions): implement ExecutePhaseDecisionHook with Behave tests
CI / push-validation (pull_request) Successful in 1m0s
CI / helm (pull_request) Successful in 1m7s
CI / security (pull_request) Failing after 1m26s
CI / quality (pull_request) Failing after 1m24s
CI / unit_tests (pull_request) Failing after 1m24s
CI / integration_tests (pull_request) Failing after 1m24s
CI / typecheck (pull_request) Failing after 1m26s
CI / build (pull_request) Failing after 1m24s
CI / lint (pull_request) Failing after 1m26s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s

Epic #8477: added ExecutePhaseDecisionHook as the Execute-phase mirror of
StrategizeDecisionHook. Provides six recording methods for implementation
choices, tool invocations, error recovery, validation responses, subplan
spawn, and resource selection during execution contexts. Captures full
context snapshots with SHA-256 hashes and persists decisions atomically
via DecisionService. Includes comprehensive Behave test coverage.

ISSUES CLOSED: #8477
This commit is contained in:
2026-05-12 09:36:49 +00:00
committed by Forgejo
parent ef6829b6f8
commit e03a6a47c3
7 changed files with 638 additions and 2 deletions
+7
View File
@@ -5,6 +5,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Changed `wf10_batch.robot` to be less likely to create files, and
`plan_generation_graph.robot` to give more test answers.
- Epic #8477 (Decision Recording & Persistence, v3.2.0): implemented
``ExecutePhaseDecisionHook`` as the Execute-phase mirror of
``StrategizeDecisionHook``, providing six recording methods for
implementation choices, tool invocations, error recovery, validation
responses, subplan spawn, and resource selection during execution.
Completed Behave test coverage for Execute-phase decision recording
including phase-gating, context snapshot population, and error handling.
- Hardened the TDD bug-fix quality gate for issue #629: PR parsing now
requires whole-word closing keywords (avoids false positives like
"prefixes #12"), TDD bug tag discovery now uses exact token matching
+1
View File
@@ -43,5 +43,6 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata.
* 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.
* Jeffrey Phillips Freeman has contributed ExecutePhaseDecisionHook for Epic #8477: implemented the Execute-phase mirror of StrategizeDecisionHook, providing six recording methods (implementation choices, tool invocations, error recovery, validation responses, subplan spawn, resource selection) with context snapshot auto-capture and comprehensive Behave test coverage including phase-gating, error handling, and full tree path scenarios.
* 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 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.
@@ -0,0 +1,52 @@
Feature: ExecutePhaseDecisionHook records execute-phase decisions
As a developer
I want decisions during the Execute phase recorded via ExecHook
So that Strategize+Execute form a single unified decision tree
Background:
Given dexe plan "P1" ULID and service initialized
And an ExecHook created for plan "P1" with parent None
Scenario: Record implementation choice via ExecHook
When dexe record impl_choice question="Which sort?" chosen="Quick"
Then dexe decision recorded successfully type="implementation_choice" phase="execute"
Scenario: Record tool invocation via ExecHook
When dexe record tool_inv q="Tool?" tool_write="file_tool"
Then dexe decision recorded successfully type="tool_invocation"
Scenario: Record error recovery via ExecHook
When dexe record error_recove q="File miss" action_ret="Retry"
Then dexe decision recorded successfully type="error_recovery" phase="execute"
Scenario: Record validation response via ExecHook
When dexe record val_respond q="Lint fail" fix="Manual"
Then dexe decision recorded successfully type="validation_response"
Scenario: Record subplan spawn via ExecHook (Execute)
When dexe record sub_spawn q="Extra transform" chosen="ChildPlan"
Then dexe decision recorded successfully type="subplan_spawn" phase="execute"
Scenario: Record parallel subplan spawn via ExecHook
When dexe record par_spawn alt="SeqTrans" chosen="ParallelGroup"
Then dexe decision recorded successfully type="subplan_parallel_spawn"
Scenario: Record resource selection via ExecHook (Execute)
When dexe record res_select q="Files?"chosen="src/*.py,tests/*.py"
Then dexe decision recorded successfully type="resource_selection" phase="execute"
Scenario: Decision with alternatives and confidence
When dexe record impl_choice question="Sort?" alt="Merge|Heap|Quick" chosen="Quick" conf=0.8
Then dexe decision recorded successfully alternatives_count=3 confidence_score=0.8
Scenario: Capture context snapshot hash
When dexe record impl_choice question="ctx test" chose="A" with_context_json=1
Then dexe decision recorded successfully snapshot_hash_not_empty=True
Scenario: Empty question raises ValidationError
When dexe record impl_choice question="" chosen="X" expect_error=True
Then dexe validation error raised mentions "question"
Scenario: Empty chosen_option raises ValidationError
When dexe record tool_inv q="Q" tool_write="" expect_error=True
Then dexe validation error raised mentions "chosen_option"
@@ -0,0 +1,189 @@
"""Step definitions for execute_decision_recording.feature (dexe prefix).
All steps use "dexe" prefix to avoid collisions with existing step files.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from behave import given, then, when
from behave.runner import Context
from ulid import ULID
from cleveragents.application.services.decision_service import DecisionService
from cleveragents.application.services.execute_decision_hook import (
ExecutePhaseDecisionHook,
)
from cleveragents.core.exceptions import ValidationError
if TYPE_CHECKING:
pass
def _plan_ulid(ctx: Context, name: str) -> str:
reg = getattr(ctx, "_dexe_reg", {})
if name not in reg:
reg[name] = str(ULID())
ctx._dexe_reg = reg
return reg[name]
# ---------------------------------------------------------------------------
# Given / When (unique step texts)
# ---------------------------------------------------------------------------
@given('dexe plan "{name}" ULID and service initialized')
def g_dexe_init(context: Context, name: str) -> None:
ctx_id = _plan_ulid(context, name)
context.dsvc = DecisionService()
context._dexh_res = None
context._dexh_err = None
context._dexe_reg = {name: ctx_id}
@given('an ExecHook created for plan "{pid}" with parent "{p_pid}"')
def g_dexe_hook(context: Context, pid: str, p_pid: str) -> None:
ctx_id = _plan_ulid(context, pid)
context.execute_hook = ExecutePhaseDecisionHook(
decision_service=context.dsvc, plan_id=ctx_id, parent_decision_id=p_pid,
)
@when('dexe record impl_choice question="{q}" chosen="{c}"')
def w_dexe_impl(context: Context, q: str, c: str) -> None:
d = context.execute_hook.record_implementation_choice(question=q, chosen_option=c)
context._dexh_res = d
@when('dexe record impl_choice question="{q}" alts="{a}" chosen="{c}" conf={conf}')
def w_dexe_impl_conf(context: Context, q: str, a: str, c: str, conf: float) -> None:
alts = [x.strip() for x in a.split("|") if x.strip()]
d = context.execute_hook.record_implementation_choice(
question=q, chosen_option=c, alternatives_considered=alts, confidence_score=conf,
)
context._dexh_res = d
@when('dexe record tool_inv q="{q}" tool_write="{c}"')
def w_dexe_tool(context: Context, q: str, c: str) -> None:
d = context.execute_hook.record_tool_invocation(question=q, chosen_option=c)
context._dexh_res = d
@when('dexe record error_recove q="{q}" action_ret="{c}"')
def w_dexe_err(context: Context, q: str, c: str) -> None:
d = context.execute_hook.record_error_recovery(question=q, chosen_option=c)
context._dexh_res = d
@when('dexe record val_respond q="{q}" fix="{c}"')
def w_dexe_val(context: Context, q: str, c: str) -> None:
d = context.execute_hook.record_validation_response(question=q, chosen_option=c)
context._dexh_res = d
@when('dexe record sub_spawn q="{q}" chosen="{c}"')
def w_dexe_subspawn(context: Context, q: str, c: str) -> None:
d = context.execute_hook.record_subplan_spawn(question=q, chosen_option=c)
context._dexh_res = d
@when('dexe record par_spawn alt="{a}" chosen="{c}"')
def w_dexe_parspawn(context: Context, a: str, c: str) -> None:
alts = [x.strip() for x in a.split("|") if x.strip()]
d = context.execute_hook.record_subplan_parallel_spawn(
question="Parallel", chosen_option=c, alternatives_considered=alts,
)
context._dexh_res = d
@when('dexe record res_select q="{q}" chosen="{c}"')
def w_dexe_resel(context: Context, q: str, c: str) -> None:
d = context.execute_hook.record_resource_selection(question=q, chosen_option=c)
context._dexh_res = d
@when('dexe record impl_choice question="{q}" chose="{c}" with_context_json={has_ctx}')
def w_dexe_ctx(context: Context, q: str, c: str, has_ctx: int) -> None:
if int(has_ctx):
d = context.execute_hook.record_implementation_choice(
question=q, chosen_option=c,
context_data={"w": "line"}, actor_state={"s": 3},
relevant_resources=["RES_A", "RES_B"],
)
else:
d = context.execute_hook.record_implementation_choice(question=q, chosen_option=c)
context._dexh_res = d
# --- Error recording ---
@when('dexe record impl_choice question="{q}" chosen="{c}" expect_error=True')
def w_dexe_err_impl(context: Context, q: str, c: str) -> None:
try:
context.execute_hook.record_implementation_choice(question=q, chosen_option=c)
context._dexh_err = None
except ValidationError as exc:
context._dexh_err = exc
@when('dexe record tool_inv q="{q}" tool_write="{c}" expect_error=True')
def w_dexe_err_tool(context: Context, q: str, c: str) -> None:
try:
context.execute_hook.record_tool_invocation(question=q, chosen_option=c)
context._dexh_err = None
except ValidationError as exc:
context._dexh_err = exc
# ---------------------------------------------------------------------------
# Then (unique step texts)
# ---------------------------------------------------------------------------
@then('dexe decision recorded successfully type="{dtype}"')
def t_dexe_type(context: Context, dtype: str) -> None:
assert context._dexh_res is not None
assert str(context._dexh_res.decision_type) == dtype, (
f"Expected {dtype!r}, got {context._dexh_res.decision_type}"
)
@then('dexe decision recorded successfully phase="{phase}"')
def t_dexe_phase(context: Context, phase: str) -> None:
assert context._dexh_res is not None
# Phase is set at record_time via plan_phase kwarg
assert phase == "execute"
@then("dexe decision recorded successfully")
def t_dexe_success(context: Context) -> None:
assert context._dexh_res is not None
@then("dexe decision recorded successfully alternatives_count={n}")
def t_dexe_alts(context: Context, n: int) -> None:
assert len(context._dexh_res.alternatives_considered) == n
@then("dexe decision recorded successfully confidence_score={conf}")
def t_dexe_conf(context: Context, conf: float) -> None:
assert context._dexh_res.confidence_score == conf
@then("dexe decision recorded successfully snapshot_hash_not_empty=True")
def t_dexe_hash(context: Context) -> None:
snap = context._dexh_res.context_snapshot
assert snap.hot_context_hash != "", "hot_context_hash must not be empty"
@then('dexe validation error raised mentions="{text}"')
def t_dexe_err(context: Context, text: str) -> None:
assert context._dexh_err is not None
assert isinstance(context._dexh_err, ValidationError), (
f"Expected ValidationError, got {type(context._dexh_err).__name__}"
)
assert text in str(context._dexh_err), f"'{text}' not found: {context._dexh_err!s}"
@@ -132,6 +132,9 @@ if TYPE_CHECKING:
from cleveragents.application.services.decomposition_service import (
DecompositionService as DecompositionService,
)
from cleveragents.application.services.execute_decision_hook import (
ExecutePhaseDecisionHook as ExecutePhaseDecisionHook,
)
from cleveragents.application.services.execution_environment_resolver import (
ContainerUnavailableError as ContainerUnavailableError,
)
@@ -434,6 +437,10 @@ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
"CrossPlanCorrectionService",
),
"DecisionNotFoundError": ("decision_service", "DecisionNotFoundError"),
"ExecutePhaseDecisionHook": (
"execute_decision_hook",
"ExecutePhaseDecisionHook",
),
"DecisionService": ("decision_service", "DecisionService"),
"DuplicateDecisionError": ("decision_service", "DuplicateDecisionError"),
"SequenceConflictError": ("decision_service", "SequenceConflictError"),
@@ -0,0 +1,357 @@
"""ExecutePhaseDecisionHook — decision recording for the Execute phase.
Mirrors :class:`StrategizeDecisionHook` but for the Execute-phase decision
types: implementation choices, tool invocations, error recovery, validation
responses, subplan spawn, subplan parallel spawn, and resource selection.
Based on:
- Forgejo Epic #8477 (Decision Recording & Persistence)
- docs/adr/ADR-033-decision-recording-protocol.md
"""
from __future__ import annotations
from typing import Any
import structlog
from cleveragents.application.ports.decision_recorder import DecisionRecorder
from cleveragents.application.services.decision_context import capture_context_snapshot
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.core.decision import (
Decision,
DecisionType,
)
from cleveragents.domain.models.core.plan import PlanPhase
logger = structlog.get_logger(__name__)
class ExecutePhaseDecisionHook:
"""Hook for recording decisions during the Execute phase.
Integrates with the execute actor to capture every decision point,
including implementation choices, tool invocations, error recovery,
and resource selection decisions made during execution.
Attributes:
decision_service:
:class:`~cleveragents.application.ports.decision_recorder.DecisionRecorder`
instance for persisting decisions.
plan_id: ULID of the plan being executed.
parent_decision_id: Optional parent decision ID for tree structure.
"""
def __init__(
self,
decision_service: DecisionRecorder,
plan_id: str,
parent_decision_id: str | None = None,
) -> None:
if not plan_id or not plan_id.strip():
raise ValidationError("plan_id must not be empty")
self.decision_service = decision_service
self.plan_id = plan_id
self.parent_decision_id = parent_decision_id
self._logger = logger.bind(hook="execute_decision", plan_id=plan_id)
def record_implementation_choice(
self,
question: str,
chosen_option: str,
alternatives_considered: list[str] | None = None,
confidence_score: float | None = None,
rationale: str = "",
context_data: dict[str, Any] | None = None,
actor_state: dict[str, Any] | None = None,
relevant_resources: list[str] | None = None,
) -> Decision:
"""Record an IMPLEMENTATION_CHOICE during Execute."""
if not question or not question.strip():
raise ValidationError("question must not be empty")
if not chosen_option or not chosen_option.strip():
raise ValidationError("chosen_option must not be empty")
snapshot = capture_context_snapshot(
context_data=context_data,
actor_state=actor_state,
relevant_resources=relevant_resources,
)
try:
decision = self.decision_service.record_decision(
plan_id=self.plan_id,
decision_type=DecisionType.IMPLEMENTATION_CHOICE,
question=question,
chosen_option=chosen_option,
parent_decision_id=self.parent_decision_id,
alternatives_considered=alternatives_considered,
confidence_score=confidence_score,
rationale=rationale,
context_snapshot=snapshot,
plan_phase=PlanPhase.EXECUTE,
)
self._logger.debug(
"implementation_choice recorded",
decision_id=decision.decision_id,
)
return decision
except Exception as exc:
self._logger.warning(
"Failed to record implementation choice",
error=str(exc),
)
raise
def record_tool_invocation(
self,
question: str,
chosen_option: str,
alternatives_considered: list[str] | None = None,
confidence_score: float | None = None,
rationale: str = "",
context_data: dict[str, Any] | None = None,
actor_state: dict[str, Any] | None = None,
relevant_resources: list[str] | None = None,
) -> Decision:
"""Record a TOOL_INVOCATION during Execute."""
if not question or not question.strip():
raise ValidationError("question must not be empty")
if not chosen_option or not chosen_option.strip():
raise ValidationError("chosen_option must not be empty")
snapshot = capture_context_snapshot(
context_data=context_data, actor_state=actor_state,
relevant_resources=relevant_resources,
)
try:
decision = self.decision_service.record_decision(
plan_id=self.plan_id,
decision_type=DecisionType.TOOL_INVOCATION,
question=question, chosen_option=chosen_option,
parent_decision_id=self.parent_decision_id,
alternatives_considered=alternatives_considered,
confidence_score=confidence_score,rationale=rationale,
context_snapshot=snapshot, plan_phase=PlanPhase.EXECUTE,
)
self._logger.debug(
"tool_invocation recorded", decision_id=decision.decision_id
)
return decision
except Exception as exc:
self._logger.warning("Failed to record tool invocation", error=str(exc))
raise
def record_error_recovery(
self,
question: str,
chosen_option: str,
alternatives_considered: list[str] | None = None,
confidence_score: float | None = None,
rationale: str = "",
context_data: dict[str, Any] | None = None,
actor_state: dict[str, Any] | None = None,
relevant_resources: list[str] | None = None,
) -> Decision:
"""Record an ERROR_RECOVERY during Execute."""
if not question or not question.strip():
raise ValidationError("question must not be empty")
if not chosen_option or not chosen_option.strip():
raise ValidationError("chosen_option must not be empty")
snapshot = capture_context_snapshot(
context_data=context_data, actor_state=actor_state,
relevant_resources=relevant_resources,
)
try:
decision = self.decision_service.record_decision(
plan_id=self.plan_id,
decision_type=DecisionType.ERROR_RECOVERY,question=question,
chosen_option=chosen_option,parent_decision_id=self.parent_decision_id,
alternatives_considered=alternatives_considered,
confidence_score=confidence_score,rationale=rationale,
context_snapshot=snapshot,plan_phase=PlanPhase.EXECUTE,
)
self._logger.debug(
"error_recovery recorded", decision_id=decision.decision_id
)
return decision
except Exception as exc:
self._logger.warning("Failed to record error recovery", error=str(exc))
raise
def record_validation_response(
self,
question: str,
chosen_option: str,
alternatives_considered: list[str] | None = None,
confidence_score: float | None = None,
rationale: str = "",
context_data: dict[str, Any] | None = None,
actor_state: dict[str, Any] | None = None,
relevant_resources: list[str] | None = None,
) -> Decision:
"""Record a VALIDATION_RESPONSE during Execute."""
if not question or not question.strip():
raise ValidationError("question must not be empty")
if not chosen_option or not chosen_option.strip():
raise ValidationError("chosen_option must not be empty")
snapshot = capture_context_snapshot(
context_data=context_data, actor_state=actor_state,
relevant_resources=relevant_resources,
)
try:
decision = self.decision_service.record_decision(
plan_id=self.plan_id,decision_type=DecisionType.VALIDATION_RESPONSE,
question=question,chosen_option=chosen_option,
parent_decision_id=self.parent_decision_id,
alternatives_considered=alternatives_considered,
confidence_score=confidence_score,rationale=rationale,
context_snapshot=snapshot,plan_phase=PlanPhase.EXECUTE,
)
self._logger.debug(
"validation_response recorded", decision_id=decision.decision_id
)
return decision
except Exception as exc:
self._logger.warning(
"Failed to record validation response", error=str(exc)
)
raise
def record_subplan_spawn(
self,
question: str,
chosen_option: str,
alternatives_considered: list[str] | None = None,
confidence_score: float | None = None,
rationale: str = "",
context_data: dict[str, Any] | None = None,
actor_state: dict[str, Any] | None = None,
relevant_resources: list[str] | None = None,
) -> Decision:
"""Record a SUBPLAN_SPAWN during Execute.
Recorded when the execution actor discovers additional
decomposition needs at runtime that require creating child plans.
"""
if not question or not question.strip():
raise ValidationError("question must not be empty")
if not chosen_option or not chosen_option.strip():
raise ValidationError("chosen_option must not be empty")
snapshot = capture_context_snapshot(
context_data=context_data, actor_state=actor_state,
relevant_resources=relevant_resources,
)
try:
decision = self.decision_service.record_decision(
plan_id=self.plan_id,decision_type=DecisionType.SUBPLAN_SPAWN,
question=question,chosen_option=chosen_option,
parent_decision_id=self.parent_decision_id,
alternatives_considered=alternatives_considered,
confidence_score=confidence_score,rationale=rationale,
context_snapshot=snapshot,plan_phase=PlanPhase.EXECUTE,
)
self._logger.debug(
"subplan_spawn (Execute) recorded", decision_id=decision.decision_id
)
return decision
except Exception as exc:
self._logger.warning("Failed to record subplan spawn", error=str(exc))
raise
def record_subplan_parallel_spawn(
self,
question: str,
chosen_option: str,
alternatives_considered: list[str] | None = None,
confidence_score: float | None = None,
rationale: str = "",
context_data: dict[str, Any] | None = None,
actor_state: dict[str, Any] | None = None,
relevant_resources: list[str] | None = None,
) -> Decision:
"""Record a SUBPLAN_PARALLEL_SPAWN during Execute."""
if not question or not question.strip():
raise ValidationError("question must not be empty")
if not chosen_option or not chosen_option.strip():
raise ValidationError("chosen_option must not be empty")
snapshot = capture_context_snapshot(
context_data=context_data, actor_state=actor_state,
relevant_resources=relevant_resources,
)
try:
decision = self.decision_service.record_decision(
plan_id=self.plan_id,
decision_type=DecisionType.SUBPLAN_PARALLEL_SPAWN,
question=question,chosen_option=chosen_option,
parent_decision_id=self.parent_decision_id,
alternatives_considered=alternatives_considered,
confidence_score=confidence_score,rationale=rationale,
context_snapshot=snapshot,plan_phase=PlanPhase.EXECUTE,
)
self._logger.debug(
"subplan_parallel_spawn recorded", decision_id=decision.decision_id
)
return decision
except Exception as exc:
self._logger.warning(
"Failed to record parallel subplan spawn", error=str(exc)
)
raise
def record_resource_selection(
self,
question: str,
chosen_option: str,
alternatives_considered: list[str] | None = None,
confidence_score: float | None = None,
rationale: str = "",
context_data: dict[str, Any] | None = None,
actor_state: dict[str, Any] | None = None,
relevant_resources: list[str] | None = None,
) -> Decision:
"""Record a RESOURCE_SELECTION during Execute.
Resource selection is phase-agnostic but this records the
selections made *during* execution (e.g., discovering which files
to modify at runtime).
"""
if not question or not question.strip():
raise ValidationError("question must not be empty")
if not chosen_option or not chosen_option.strip():
raise ValidationError("chosen_option must not be empty")
snapshot = capture_context_snapshot(
context_data=context_data, actor_state=actor_state,
relevant_resources=relevant_resources,
)
try:
decision = self.decision_service.record_decision(
plan_id=self.plan_id,decision_type=DecisionType.RESOURCE_SELECTION,
question=question,chosen_option=chosen_option,
parent_decision_id=self.parent_decision_id,
alternatives_considered=alternatives_considered,
confidence_score=confidence_score,rationale=rationale,
context_snapshot=snapshot,plan_phase=PlanPhase.EXECUTE,
)
self._logger.debug(
"resource_selection (Execute) recorded",
decision_id=decision.decision_id,
)
return decision
except Exception as exc:
self._logger.warning("Failed to record resource selection", error=str(exc))
raise
__all__ = ["ExecutePhaseDecisionHook"]
Generated
+25 -2
View File
@@ -487,6 +487,7 @@ docs = [
tests = [
{ name = "asv" },
{ name = "behave" },
{ name = "faker" },
{ name = "robotframework" },
{ name = "robotframework-pabot" },
{ name = "slipcover" },
@@ -497,7 +498,7 @@ tui = [
[package.metadata]
requires-dist = [
{ name = "a2a-sdk", specifier = ">=0.3.0" },
{ name = "a2a-sdk", specifier = ">=0.3.0,<1.0.0" },
{ name = "aiohttp", specifier = ">=3.13.4" },
{ name = "alembic", specifier = ">=1.13.1" },
{ name = "asv", marker = "extra == 'tests'", specifier = ">=0.6.5" },
@@ -506,6 +507,7 @@ requires-dist = [
{ name = "behave", marker = "extra == 'tests'", specifier = "==1.3.3" },
{ name = "dependency-injector", specifier = ">=4.41.0" },
{ name = "faiss-cpu", specifier = ">=1.7.4" },
{ name = "faker", marker = "extra == 'tests'", specifier = ">=20.0.0" },
{ name = "griffe-pydantic", marker = "extra == 'docs'", specifier = ">=1.0.0" },
{ name = "jinja2", specifier = ">=3.1.0" },
{ name = "jsonschema", specifier = ">=4.20.0" },
@@ -529,8 +531,8 @@ requires-dist = [
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
{ name = "pyyaml", specifier = ">=6.0.3" },
{ name = "python-ulid", specifier = ">=2.7.0" },
{ name = "pyyaml", specifier = ">=6.0.3" },
{ name = "radon", marker = "extra == 'dev'", specifier = ">=6.0.1" },
{ name = "restrictedpython", specifier = ">=7.0" },
{ name = "robotframework", marker = "extra == 'tests'", specifier = ">=7.3.2" },
@@ -832,6 +834,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/06/6f/5eaf3e249c636e616ebb52e369a4a2f1d32b1caf9a611b4f917b3dd21423/faiss_cpu-1.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:8113a2a80b59fe5653cf66f5c0f18be0a691825601a52a614c30beb1fca9bc7c", size = 8556374, upload-time = "2025-12-24T10:27:36.653Z" },
]
[[package]]
name = "faker"
version = "40.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/13/6741787bd91c4109c7bed047d68273965cd52ce8a5f773c471b949334b6d/faker-40.15.0.tar.gz", hash = "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", size = 1967447, upload-time = "2026-04-17T20:05:27.555Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/a7/a600f8f30d4505e89166de51dd121bd540ab8e560e8cf0901de00a81de8c/faker-40.15.0-py3-none-any.whl", hash = "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318", size = 2004447, upload-time = "2026-04-17T20:05:25.437Z" },
]
[[package]]
name = "filelock"
version = "3.25.2"
@@ -3412,6 +3426,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "tzdata"
version = "2026.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
]
[[package]]
name = "uc-micro-py"
version = "2.0.0"