feat(decisions): implement ExecutePhaseDecisionHook with Behave tests #11154
@@ -67,6 +67,7 @@ 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 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.
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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"
|
||||
And dexe decision recorded successfully 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"
|
||||
And dexe decision recorded successfully 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"
|
||||
And dexe decision recorded successfully 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"
|
||||
And dexe decision recorded successfully phase="execute"
|
||||
|
||||
Scenario: Decision with alternatives and confidence
|
||||
When dexe record impl_choice question="Sort?" alts="Merge|Heap|Quick" chosen="Quick" conf=0.8
|
||||
Then dexe decision recorded successfully alternatives_count=3
|
||||
And dexe decision recorded successfully 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: Whitespace 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"
|
||||
|
||||
Scenario: Whitespace chosen_option raises ValidationError
|
||||
When dexe record tool_inv q="Q" tool_write=" " expect_error=True
|
||||
Then dexe validation error raised mentions="chosen_option"
|
||||
|
||||
Scenario: Empty plan_id raises ValidationError on construction
|
||||
When dexe construct hook with empty plan_id
|
||||
Then dexe validation error raised mentions="plan_id"
|
||||
|
||||
Scenario: Whitespace plan_id raises ValidationError on construction
|
||||
When dexe construct hook with whitespace plan_id
|
||||
Then dexe validation error raised mentions="plan_id"
|
||||
@@ -0,0 +1,253 @@
|
||||
"""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]
|
||||
|
||||
|
||||
def _coerce_parent(p_pid: str) -> str | None:
|
||||
"""Convert the feature-file literal ``None`` into a real ``None``."""
|
||||
if p_pid in ("None", "none", "null", ""):
|
||||
return None
|
||||
return p_pid
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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=_coerce_parent(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: str) -> 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=float(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: str) -> 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
|
||||
|
||||
|
||||
# Hardcoded for the empty-string case (behave's ``{q}`` placeholder needs >=1 char).
|
||||
@when('dexe record impl_choice question="" chosen="X" expect_error=True')
|
||||
def w_dexe_err_impl_empty_q(context: Context) -> None:
|
||||
try:
|
||||
context.execute_hook.record_implementation_choice(
|
||||
question="", chosen_option="X"
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
# Hardcoded for the empty-string case (behave's ``{c}`` placeholder needs >=1 char).
|
||||
@when('dexe record tool_inv q="Q" tool_write="" expect_error=True')
|
||||
def w_dexe_err_tool_empty_c(context: Context) -> None:
|
||||
try:
|
||||
context.execute_hook.record_tool_invocation(question="Q", chosen_option="")
|
||||
context._dexh_err = None
|
||||
except ValidationError as exc:
|
||||
context._dexh_err = exc
|
||||
|
||||
|
||||
@when("dexe construct hook with empty plan_id")
|
||||
def w_dexe_ctor_empty(context: Context) -> None:
|
||||
try:
|
||||
ExecutePhaseDecisionHook(decision_service=context.dsvc, plan_id="")
|
||||
context._dexh_err = None
|
||||
except ValidationError as exc:
|
||||
context._dexh_err = exc
|
||||
|
||||
|
||||
@when("dexe construct hook with whitespace plan_id")
|
||||
def w_dexe_ctor_ws(context: Context) -> None:
|
||||
try:
|
||||
ExecutePhaseDecisionHook(decision_service=context.dsvc, plan_id=" ")
|
||||
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
|
||||
actual = context._dexh_res.decision_type
|
||||
# Compare against either the enum-value or the str() form
|
||||
actual_str = getattr(actual, "value", str(actual))
|
||||
assert actual_str == dtype or str(actual) == dtype, (
|
||||
f"Expected {dtype!r}, got {actual!r}"
|
||||
)
|
||||
|
||||
|
||||
@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: str) -> None:
|
||||
assert context._dexh_res is not None
|
||||
assert len(context._dexh_res.alternatives_considered) == int(n)
|
||||
|
||||
|
||||
@then("dexe decision recorded successfully confidence_score={conf}")
|
||||
def t_dexe_conf(context: Context, conf: str) -> None:
|
||||
assert context._dexh_res is not None
|
||||
assert context._dexh_res.confidence_score == float(conf)
|
||||
|
||||
|
||||
@then("dexe decision recorded successfully snapshot_hash_not_empty=True")
|
||||
def t_dexe_hash(context: Context) -> None:
|
||||
assert context._dexh_res is not 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,288 @@
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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(
|
||||
self,
|
||||
*,
|
||||
decision_type: DecisionType,
|
||||
log_label: str,
|
||||
question: str,
|
||||
chosen_option: str,
|
||||
alternatives_considered: list[str] | None,
|
||||
confidence_score: float | None,
|
||||
rationale: str,
|
||||
context_data: dict[str, Any] | None,
|
||||
actor_state: dict[str, Any] | None,
|
||||
relevant_resources: list[str] | None,
|
||||
) -> Decision:
|
||||
"""Shared validate/snapshot/record/log path for every record_* method.
|
||||
|
||||
Centralising the pre/post boilerplate keeps the public surface
|
||||
slim and eliminates the duplicated validation + exception
|
||||
re-raise blocks that previously inflated uncovered line counts.
|
||||
"""
|
||||
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=decision_type,
|
||||
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,
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - pass-through logging
|
||||
self._logger.warning(f"Failed to record {log_label}", error=str(exc))
|
||||
raise
|
||||
|
||||
self._logger.debug(f"{log_label} recorded", decision_id=decision.decision_id)
|
||||
return decision
|
||||
|
||||
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."""
|
||||
return self._record(
|
||||
decision_type=DecisionType.IMPLEMENTATION_CHOICE,
|
||||
log_label="implementation_choice",
|
||||
question=question,
|
||||
chosen_option=chosen_option,
|
||||
alternatives_considered=alternatives_considered,
|
||||
confidence_score=confidence_score,
|
||||
rationale=rationale,
|
||||
context_data=context_data,
|
||||
actor_state=actor_state,
|
||||
relevant_resources=relevant_resources,
|
||||
)
|
||||
|
||||
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."""
|
||||
return self._record(
|
||||
decision_type=DecisionType.TOOL_INVOCATION,
|
||||
log_label="tool_invocation",
|
||||
question=question,
|
||||
chosen_option=chosen_option,
|
||||
alternatives_considered=alternatives_considered,
|
||||
confidence_score=confidence_score,
|
||||
rationale=rationale,
|
||||
context_data=context_data,
|
||||
actor_state=actor_state,
|
||||
relevant_resources=relevant_resources,
|
||||
)
|
||||
|
||||
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."""
|
||||
return self._record(
|
||||
decision_type=DecisionType.ERROR_RECOVERY,
|
||||
log_label="error_recovery",
|
||||
question=question,
|
||||
chosen_option=chosen_option,
|
||||
alternatives_considered=alternatives_considered,
|
||||
confidence_score=confidence_score,
|
||||
rationale=rationale,
|
||||
context_data=context_data,
|
||||
actor_state=actor_state,
|
||||
relevant_resources=relevant_resources,
|
||||
)
|
||||
|
||||
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."""
|
||||
return self._record(
|
||||
decision_type=DecisionType.VALIDATION_RESPONSE,
|
||||
log_label="validation_response",
|
||||
question=question,
|
||||
chosen_option=chosen_option,
|
||||
alternatives_considered=alternatives_considered,
|
||||
confidence_score=confidence_score,
|
||||
rationale=rationale,
|
||||
context_data=context_data,
|
||||
actor_state=actor_state,
|
||||
relevant_resources=relevant_resources,
|
||||
)
|
||||
|
||||
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.
|
||||
"""
|
||||
return self._record(
|
||||
decision_type=DecisionType.SUBPLAN_SPAWN,
|
||||
log_label="subplan_spawn",
|
||||
question=question,
|
||||
chosen_option=chosen_option,
|
||||
alternatives_considered=alternatives_considered,
|
||||
confidence_score=confidence_score,
|
||||
rationale=rationale,
|
||||
context_data=context_data,
|
||||
actor_state=actor_state,
|
||||
relevant_resources=relevant_resources,
|
||||
)
|
||||
|
||||
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."""
|
||||
return self._record(
|
||||
decision_type=DecisionType.SUBPLAN_PARALLEL_SPAWN,
|
||||
log_label="subplan_parallel_spawn",
|
||||
question=question,
|
||||
chosen_option=chosen_option,
|
||||
alternatives_considered=alternatives_considered,
|
||||
confidence_score=confidence_score,
|
||||
rationale=rationale,
|
||||
context_data=context_data,
|
||||
actor_state=actor_state,
|
||||
relevant_resources=relevant_resources,
|
||||
)
|
||||
|
||||
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).
|
||||
"""
|
||||
return self._record(
|
||||
decision_type=DecisionType.RESOURCE_SELECTION,
|
||||
log_label="resource_selection",
|
||||
question=question,
|
||||
chosen_option=chosen_option,
|
||||
alternatives_considered=alternatives_considered,
|
||||
confidence_score=confidence_score,
|
||||
rationale=rationale,
|
||||
context_data=context_data,
|
||||
actor_state=actor_state,
|
||||
relevant_resources=relevant_resources,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["ExecutePhaseDecisionHook"]
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user