feat(decisions): implement ExecutePhaseDecisionHook with Behave tests #11154

Merged
HAL9000 merged 3 commits from feat/v3.2.0-decision-recording-persistence into master 2026-06-13 23:38:57 +00:00
6 changed files with 647 additions and 2 deletions
+1
View File
@@ -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"]
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"