diff --git a/backend/app/services/agent_context.py b/backend/app/services/agent_context.py index a811623b4..6045863b2 100644 --- a/backend/app/services/agent_context.py +++ b/backend/app/services/agent_context.py @@ -234,13 +234,24 @@ async def _load_company_information(db, agent_id: uuid.UUID) -> str: _BASE_PROMPT_BEFORE_CAPABILITIES = """ # Clawith Environment -Clawith is a collaborative organization where human members and digital -employees work together. +You are a persistent digital employee. Complete authorized work in the current +tenant using the context and tools actually available in this model step. -You are a persistent member of this organization, not a stateless chatbot. -Use the context, capabilities, and permissions available to you to complete -authorized work for users and collaborators. Clawith provides persistent Memory, -Workspace, Focus, Trigger, and Directory mechanisms. +# Operating Contract + +Work in this order: understand the requested outcome, execute the necessary +actions, verify the result from objective evidence, then finish. + +- Extract every explicit requirement, constraint, deliverable, and requested + format before acting. Use explicit success criteria as the definition of done. +- Continue through recoverable errors. Inspect the failure, change the approach, + and retry safely; do not merely describe work that you can perform. +- Separate observed facts from assumptions. Never invent facts, identifiers, + links, files, Tool Results, actions, or completion. +- A successful Tool Call proves only that call succeeded. It does not by itself + prove that the user's outcome was achieved. +- Before finishing, read back or otherwise inspect important outputs and compare + them with the original request. Do not rely only on your own draft or plan. ## Memory @@ -257,7 +268,10 @@ async def _load_company_information(db, agent_id: uuid.UUID) -> str: - Use it for durable task artifacts such as documents, reports, datasets, and generated files. - Read actual files before relying on their contents. -- Base claims about file changes on successful tool results. +- Use Agent-root-relative paths exactly as Workspace tools expose them. Do not + assume that an execution tool's process path is the same visible path. +- When code creates or changes a deliverable, confirm it with a Workspace read or + listing before claiming it exists. - Tool names and file-operation parameters are defined by the current Tool Schema. ## Focus @@ -293,24 +307,6 @@ async def _load_company_information(db, agent_id: uuid.UUID) -> str: result; never guess recipients or reuse remembered identifiers as routing data. - Relationships and Memory are background context, not contact routes. -# Objective - -Complete the user's requested outcome accurately and fully. -When the active task supplies explicit success criteria, use them as the -definition of done. -Do not stop at explaining what should be done when the request requires an action -that you are authorized and able to perform. - -# Instructions - -1. Determine the actual requested outcome from the current input and relevant - conversation. -2. Use available context and tools when necessary to complete or verify it. -3. Continue until the outcome is complete, essential user input is required, or - a real blocker prevents further progress. -4. Distinguish verified facts, assumptions, and unresolved uncertainties. -5. Do not claim completion until the required result has been verified. - # Constraints - Stay within the current user's permissions, tenant, task scope, and active @@ -326,7 +322,9 @@ async def _load_company_information(db, agent_id: uuid.UUID) -> str: # Runtime Protocol -- When the task is complete, return the exact final answer as normal Assistant content. +- When the task is complete and verified, return the exact final answer as normal Assistant content. + Runtime independently checks it against the original task + and available evidence before marking the Run completed. - Do not return a final answer while required work or Tool Calls are still incomplete. - When progress genuinely requires user input, approval, another Agent result, or an external event, call `wait` with a concise reason. @@ -339,8 +337,6 @@ async def _load_company_information(db, agent_id: uuid.UUID) -> str: - Do not mention or call tools that are not supplied for the current step. - Use tools when current, private, external, or execution-backed information is required. -- Inspect whether the underlying operation actually succeeded; a successful tool - invocation alone does not prove business success. - Verify important changes through a safe read-back when appropriate. - If a side-effecting operation has an unknown outcome, reconcile it instead of blindly repeating it. @@ -361,10 +357,12 @@ async def _load_company_information(db, agent_id: uuid.UUID) -> str: # Verification Before returning the final Assistant response, verify that: -- Every material user requirement has been addressed. +- Every explicit requirement, constraint, deliverable, and format has been + addressed; partial progress is not completion. - Required tool actions actually succeeded. - Required files, records, messages, or other artifacts exist. -- Important claims are supported by available evidence. +- Important claims are supported by objective evidence from the current context, + Tool Results, or inspected artifacts. - No unresolved issue is represented as completed. - The final answer follows the requested format. """.strip() diff --git a/backend/app/services/agent_runtime/node_executor.py b/backend/app/services/agent_runtime/node_executor.py index bb9d380e7..8dfaf73a8 100644 --- a/backend/app/services/agent_runtime/node_executor.py +++ b/backend/app/services/agent_runtime/node_executor.py @@ -1196,14 +1196,29 @@ async def _verify( } ) elif verification.outcome == "repair": - lifecycle.pop("finish_delivery_intent", None) - attempts, verification_episode = _verification_repair_attempt( - state["lifecycle"], - verification, - ) + if verification.details.get("code") == "task_completion_repair_required": + attempts = _counter( + state["lifecycle"], + "verification_attempt_count", + ) + 1 + verification_episode = { + "fingerprint": "task_completion_repair_required", + "attempts": attempts, + "issue_code": "task_completion_repair_required", + } + else: + attempts, verification_episode = _verification_repair_attempt( + state["lifecycle"], + verification, + ) lifecycle["verification_attempt_count"] = attempts lifecycle["verification_repair_episode"] = verification_episode - if attempts > self._max_verification_repairs: + if ( + attempts > self._max_verification_repairs + and verification.details.get("code") + != "task_completion_repair_required" + ): + lifecycle.pop("finish_delivery_intent", None) lifecycle.pop("pending_group_at", None) lifecycle.update( { @@ -1216,7 +1231,59 @@ async def _verify( ), } ) + elif attempts > self._max_verification_repairs: + exhausted_details = { + **dict(verification.details), + "code": "completion_gate_exhausted", + "repair_attempts": self._max_verification_repairs, + "rejected_candidates": attempts, + "last_outcome": verification.outcome, + "last_reason": verification.reason, + } + exhausted = VerificationResult( + outcome="pass", + details=cast(JsonObject, exhausted_details), + ) + finalized = await self._finalizer.finalize( + state, + context, + candidate, + exhausted, + ) + delivery_request = ( + dict(finalized.delivery_request) + if finalized.delivery_request is not None + else None + ) + if raw_finish_delivery_intent is not None: + delivery_request = delivery_request or {} + delivery_request["content"] = candidate + delivery_request["group_handoff"] = dict( + raw_finish_delivery_intent + ) + lifecycle.pop("finish_delivery_intent", None) + lifecycle.pop("pending_group_at", None) + lifecycle["verification_result"] = { + "outcome": "exhausted", + "reason": verification.reason, + "details": cast(JsonObject, exhausted_details), + } + lifecycle.update( + { + "status": "completed", + "next_route": "terminal", + "reason": "completion_gate_exhausted", + "result_summary": dict(finalized.result_summary), + "session_context_delta": ( + dict(finalized.session_context_delta) + if finalized.session_context_delta is not None + else None + ), + "delivery_request": delivery_request, + } + ) else: + lifecycle.pop("finish_delivery_intent", None) lifecycle.update( { "status": "running", diff --git a/backend/app/services/agent_runtime/verification.py b/backend/app/services/agent_runtime/verification.py index 1769adb6e..48870dce7 100644 --- a/backend/app/services/agent_runtime/verification.py +++ b/backend/app/services/agent_runtime/verification.py @@ -6,6 +6,7 @@ from dataclasses import dataclass import json import re +from typing import Protocol from urllib.parse import quote, unquote, urlsplit import uuid @@ -14,10 +15,12 @@ from app.models.agent import Agent as AgentModel from app.models.agent_run import AgentRun from app.models.agent_tool_execution import AgentToolExecution +from app.models.llm import LLMModel from app.models.published_page import PublishedPage from app.services.agent_runtime.command_worker import RuntimeSessionFactory from app.services.agent_runtime.node_executor import VerificationResult -from app.services.agent_runtime.state import RuntimeContext, RuntimeGraphState +from app.services.agent_runtime.state import JsonObject, RuntimeContext, RuntimeGraphState +from app.services.agent_runtime.state import runtime_messages_as_json from app.services.agent_runtime.tool_result_store import ( ToolResultStore, ToolResultStoreError, @@ -25,6 +28,8 @@ from app.services.storage import agent_storage_key, get_storage_backend from app.services.storage_runtime.base import StorageBackend from app.services.workspace_collaboration import normalize_workspace_path +from app.services.llm.client import LLMMessage +from app.services.llm.single_step import LLMCompletionStep, complete_llm_once ReferenceExists = Callable[[str, uuid.UUID, uuid.UUID], Awaitable[bool]] @@ -32,6 +37,83 @@ _HTTP_EVIDENCE_TOOL_NAMES = frozenset( {"read_webpage", "upload_image", "publish_page"} ) +_TASK_COMPLETION_SYSTEM_PROMPT = """You are the independent completion gate for one Clawith Run. + +Decide whether the original task is fully completed from the supplied evidence. +The candidate answer is a claim, not evidence. Tool success is evidence only for +what that Tool Result objectively proves. Do not require work that the original +task did not request, and do not accept partial progress, plans, or unsupported +completion claims. + +Return exactly one JSON object with this schema: +{"verdict":"pass|repair","missing_requirements":["..."],"next_actions":["..."],"evidence":["..."]} + +Use "pass" only when every explicit requirement, constraint, deliverable, and +requested format is satisfied. Otherwise use "repair" and give concrete, +executable next actions. Do not use Markdown or add text outside the JSON.""" + + +class TaskCompletionPort(Protocol): + async def __call__( + self, + model: LLMModel, + messages: list[LLMMessage], + *, + tools: list[dict] | None = None, + agent_id: uuid.UUID | None = None, + supports_vision: bool = False, + ) -> LLMCompletionStep: ... + + +def _bounded_json(value: object, *, max_chars: int) -> str: + rendered = json.dumps(value, ensure_ascii=False, sort_keys=True, default=str) + if len(rendered) <= max_chars: + return rendered + return rendered[:max_chars] + "\n...[truncated by completion gate]" + + +def _completion_evidence(state: RuntimeGraphState) -> dict[str, object]: + messages = runtime_messages_as_json(state) + retained: list[dict[str, object]] = [] + remaining = 24000 + for message in reversed(messages): + compact = { + key: message[key] + for key in ("role", "name", "content", "tool_calls", "tool_call_id") + if key in message + } + size = len(_bounded_json(compact, max_chars=remaining)) + if size > remaining: + break + retained.append(compact) + remaining -= size + retained.reverse() + evidence: dict[str, object] = { + "initial_input": state["snapshots"].initial_input, + "trajectory": retained, + } + if state.get("thread_summary") is not None: + evidence["thread_summary"] = state["thread_summary"] + return evidence + + +def _parse_completion_decision(content: str | None) -> dict[str, object] | None: + raw = (content or "").strip() + if raw.startswith("```") and raw.endswith("```"): + raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.IGNORECASE) + try: + payload = json.loads(raw) + except (TypeError, json.JSONDecodeError): + return None + if not isinstance(payload, dict) or payload.get("verdict") not in {"pass", "repair"}: + return None + for key in ("missing_requirements", "next_actions", "evidence"): + value = payload.get(key) + if not isinstance(value, list) or any(not isinstance(item, str) for item in value): + return None + if payload["verdict"] == "pass" and payload["missing_requirements"]: + return None + return payload def _refs(metadata: object, field: str) -> tuple[str, ...] | None: @@ -470,6 +552,158 @@ async def reference_exists( return False +class TaskCompletionGate: + """Independently compare the original task with evidence before completion.""" + + def __init__( + self, + *, + session_factory: RuntimeSessionFactory, + completion: TaskCompletionPort = complete_llm_once, + ) -> None: + self._session_factory = session_factory + self._completion = completion + + @staticmethod + def _fail_open(code: str, *, error_class: str | None = None) -> VerificationResult: + details: JsonObject = { + "code": "completion_gate_error", + "gate_error_code": code, + } + if error_class is not None: + details["error_class"] = error_class + return VerificationResult(outcome="pass", details=details) + + async def verify( + self, + state: RuntimeGraphState, + context: RuntimeContext, + candidate: str, + ) -> VerificationResult: + try: + tenant_id = uuid.UUID(context.tenant_id) + model_id = uuid.UUID(context.model_id) + agent_id = uuid.UUID(context.agent_id or "") + except (TypeError, ValueError) as exc: + return self._fail_open( + "invalid_completion_gate_identity", + error_class=type(exc).__name__, + ) + + async with self._session_factory() as db: + result = await db.execute(select(LLMModel).where(LLMModel.id == model_id)) + model = result.scalar_one_or_none() + if ( + model is None + or not model.enabled + or model.tenant_id not in {None, tenant_id} + ): + return self._fail_open("completion_gate_model_unavailable") + + payload = { + "original_run_goal": context.goal, + "run_kind": context.run_kind, + "candidate_final_answer": candidate, + "available_evidence": _completion_evidence(state), + } + try: + step = await self._completion( + model, + [ + LLMMessage(role="system", content=_TASK_COMPLETION_SYSTEM_PROMPT), + LLMMessage( + role="user", + content=_bounded_json(payload, max_chars=36000), + ), + ], + tools=None, + agent_id=agent_id, + supports_vision=False, + ) + except Exception as exc: + return self._fail_open( + "completion_gate_call_failed", + error_class=type(exc).__name__, + ) + + decision = _parse_completion_decision(step.content) + if decision is None: + return self._fail_open("invalid_completion_gate_output") + if decision["verdict"] == "pass": + return VerificationResult( + outcome="pass", + details={ + "code": "task_completion_passed", + "evidence": decision["evidence"], + }, + ) + + missing = list(decision["missing_requirements"]) + actions = list(decision["next_actions"]) + reason_parts = [ + "The task is not complete yet. Continue working before finishing.", + ] + if missing: + reason_parts.append("Missing requirements: " + "; ".join(missing)) + if actions: + reason_parts.append("Next actions: " + "; ".join(actions)) + return VerificationResult( + outcome="repair", + reason="\n".join(reason_parts), + details={ + "code": "task_completion_repair_required", + "missing_requirements": missing, + "next_actions": actions, + "evidence": decision["evidence"], + }, + ) + + +class CompletionGateRuntimeVerifier: + """Require deterministic integrity and semantic task completion to pass.""" + + def __init__( + self, + *, + deterministic: ToolLedgerRuntimeVerifier, + completion_gate: TaskCompletionGate, + ) -> None: + self._deterministic = deterministic + self._completion_gate = completion_gate + + async def verify( + self, + state: RuntimeGraphState, + context: RuntimeContext, + candidate: str, + ) -> VerificationResult: + deterministic = await self._deterministic.verify(state, context, candidate) + if deterministic.outcome != "pass": + return deterministic + semantic = await self._completion_gate.verify(state, context, candidate) + if semantic.outcome != "pass": + return VerificationResult( + outcome=semantic.outcome, + reason=semantic.reason, + details={ + **dict(semantic.details), + "deterministic": dict(deterministic.details), + "artifact_refs": deterministic.details.get("artifact_refs", []), + "evidence_refs": deterministic.details.get("evidence_refs", []), + }, + ) + return VerificationResult( + outcome="pass", + details={ + "code": "completion_gates_passed", + "deterministic": dict(deterministic.details), + "task_completion": dict(semantic.details), + "artifact_refs": deterministic.details.get("artifact_refs", []), + "evidence_refs": deterministic.details.get("evidence_refs", []), + }, + ) + + class ToolLedgerRuntimeVerifier: """Verify only deterministic protocol, ledger, and reference facts.""" @@ -687,4 +921,9 @@ async def verify( ) -__all__ = ["RuntimeToolReferenceReader", "ToolLedgerRuntimeVerifier"] +__all__ = [ + "CompletionGateRuntimeVerifier", + "RuntimeToolReferenceReader", + "TaskCompletionGate", + "ToolLedgerRuntimeVerifier", +] diff --git a/backend/app/services/agent_runtime/worker_service.py b/backend/app/services/agent_runtime/worker_service.py index 9dd2c567d..b5f046941 100644 --- a/backend/app/services/agent_runtime/worker_service.py +++ b/backend/app/services/agent_runtime/worker_service.py @@ -93,7 +93,9 @@ ) from app.services.agent_runtime.trigger_completion import TriggerRuntimeCompletionHandler from app.services.agent_runtime.verification import ( + CompletionGateRuntimeVerifier, RuntimeToolReferenceReader, + TaskCompletionGate, ToolLedgerRuntimeVerifier, ) @@ -250,11 +252,17 @@ def build_runtime_worker_components( model_service=model_service, tool_service=tool_service, run_compactor=run_compactor, - verifier=ToolLedgerRuntimeVerifier( - session_factory=session_factory, - result_store=tool_result_store, - reference_exists=reference_reader.reference_exists, + verifier=CompletionGateRuntimeVerifier( + deterministic=ToolLedgerRuntimeVerifier( + session_factory=session_factory, + result_store=tool_result_store, + reference_exists=reference_reader.reference_exists, + ), + completion_gate=TaskCompletionGate( + session_factory=session_factory, + ), ), + max_verification_repairs=10, ) graph = build_agent_runtime_graph( checkpointer=checkpointer, diff --git a/backend/tests/test_agent_runtime_node_executor.py b/backend/tests/test_agent_runtime_node_executor.py index f47c6ae9f..1030b92c7 100644 --- a/backend/tests/test_agent_runtime_node_executor.py +++ b/backend/tests/test_agent_runtime_node_executor.py @@ -1645,6 +1645,53 @@ async def test_verification_repairs_are_bounded() -> None: assert verifier.calls == ["first", "second"] +@pytest.mark.asyncio +async def test_task_completion_gate_exhaustion_delivers_latest_candidate() -> None: + run_id = uuid.uuid4() + model = ModelService( + ModelStepResult(intent="finish", finish_content="first draft"), + ModelStepResult(intent="finish", finish_content="latest useful result"), + ) + verifier = Verifier( + VerificationResult( + outcome="repair", + reason="missing one requirement", + details={ + "code": "task_completion_repair_required", + "missing_requirements": ["include the source"], + "artifact_refs": [], + "evidence_refs": [], + }, + ), + VerificationResult( + outcome="repair", + reason="source still missing", + details={ + "code": "task_completion_repair_required", + "missing_requirements": ["include the source"], + "artifact_refs": [], + "evidence_refs": [], + }, + ), + ) + executor = _executor( + model, + verifier=verifier, + max_verification_repairs=1, + ) + + result = await _invoke(run_id, executor) + + lifecycle = result["lifecycle"] + assert lifecycle["status"] == "completed" + assert lifecycle["reason"] == "completion_gate_exhausted" + assert lifecycle["final_answer"] == "latest useful result" + assert lifecycle["verification_result"]["outcome"] == "exhausted" + assert lifecycle["verification_result"]["details"]["repair_attempts"] == 1 + assert lifecycle["verification_result"]["details"]["rejected_candidates"] == 2 + assert lifecycle["result_summary"]["summary"] == "latest useful result" + + @pytest.mark.asyncio async def test_new_verifier_issue_starts_a_fresh_episode() -> None: run_id = uuid.uuid4() diff --git a/backend/tests/test_agent_runtime_tool_outcome_contract.py b/backend/tests/test_agent_runtime_tool_outcome_contract.py index 74a5520f5..49a9f8d5d 100644 --- a/backend/tests/test_agent_runtime_tool_outcome_contract.py +++ b/backend/tests/test_agent_runtime_tool_outcome_contract.py @@ -13,6 +13,7 @@ from app.models.agent_tool_execution import AgentToolExecution from app.services import agent_tools +from app.models.llm import LLMModel from app.services.agent_runtime.state import ( RunInputSnapshots, RuntimeContext, @@ -29,8 +30,13 @@ ToolResultStore, ToolResultStoreError, ) -from app.services.agent_runtime.verification import ToolLedgerRuntimeVerifier +from app.services.agent_runtime.verification import ( + TaskCompletionGate, + ToolLedgerRuntimeVerifier, +) +from app.services.llm.single_step import LLMCompletionStep from app.services.storage_runtime.base import StorageBackend +from app.services.token_tracker import TokenUsage class _MemoryStorage(StorageBackend): @@ -956,6 +962,109 @@ async def test_verifier_uses_invocation_context_without_checkpoint_registry() -> assert passed.details["code"] == "deterministic_checks_passed" +@pytest.mark.asyncio +async def test_completion_gate_invalid_output_fails_open() -> None: + tenant_id = uuid.uuid4() + run_id = uuid.uuid4() + model_id = uuid.uuid4() + agent_id = uuid.uuid4() + model = LLMModel( + id=model_id, + tenant_id=tenant_id, + provider="openai", + model="judge-model", + api_key_encrypted="unused", + label="Judge", + enabled=True, + ) + + async def invalid_completion(*args, **kwargs): + del args, kwargs + return LLMCompletionStep( + content="not json", + tool_calls=(), + reasoning_content=None, + retry_instruction=None, + usage=TokenUsage(), + ) + + gate = TaskCompletionGate( + session_factory=_factory(_ScalarResult(model)), + completion=invalid_completion, + ) + context = RuntimeContext( + tenant_id=str(tenant_id), + run_id=str(run_id), + command_id="command-gate", + executor=object(), # type: ignore[arg-type] + goal="Produce the requested report", + model_id=str(model_id), + agent_id=str(agent_id), + ) + + result = await gate.verify(_state(tenant_id, run_id), context, "report result") + + assert result.outcome == "pass" + assert result.details == { + "code": "completion_gate_error", + "gate_error_code": "invalid_completion_gate_output", + } + + +@pytest.mark.asyncio +async def test_completion_gate_explicit_repair_is_actionable() -> None: + tenant_id = uuid.uuid4() + run_id = uuid.uuid4() + model_id = uuid.uuid4() + agent_id = uuid.uuid4() + model = LLMModel( + id=model_id, + tenant_id=tenant_id, + provider="openai", + model="judge-model", + api_key_encrypted="unused", + label="Judge", + enabled=True, + ) + + async def repair_completion(*args, **kwargs): + del args, kwargs + return LLMCompletionStep( + content=json.dumps( + { + "verdict": "repair", + "missing_requirements": ["The report file was not read back"], + "next_actions": ["Read the report and verify its contents"], + "evidence": ["write_file succeeded"], + } + ), + tool_calls=(), + reasoning_content=None, + retry_instruction=None, + usage=TokenUsage(), + ) + + gate = TaskCompletionGate( + session_factory=_factory(_ScalarResult(model)), + completion=repair_completion, + ) + context = RuntimeContext( + tenant_id=str(tenant_id), + run_id=str(run_id), + command_id="command-gate", + executor=object(), # type: ignore[arg-type] + goal="Produce and verify the requested report", + model_id=str(model_id), + agent_id=str(agent_id), + ) + + result = await gate.verify(_state(tenant_id, run_id), context, "report done") + + assert result.outcome == "repair" + assert result.details["code"] == "task_completion_repair_required" + assert "Read the report" in (result.reason or "") + + async def _true_reference( ref: str, tenant_id: uuid.UUID, diff --git a/backend/tests/test_agent_runtime_worker_service.py b/backend/tests/test_agent_runtime_worker_service.py index f2aa6be51..5f70c1970 100644 --- a/backend/tests/test_agent_runtime_worker_service.py +++ b/backend/tests/test_agent_runtime_worker_service.py @@ -35,7 +35,9 @@ from app.services.agent_runtime.tool_result_store import ToolResultReconcileResult from app.services.agent_runtime.trigger_completion import TriggerRuntimeCompletionHandler from app.services.agent_runtime.verification import ( + CompletionGateRuntimeVerifier, RuntimeToolReferenceReader, + TaskCompletionGate, ToolLedgerRuntimeVerifier, ) from app.services.agent_runtime.worker_service import ( @@ -293,13 +295,17 @@ def test_component_builder_installs_current_agent_and_planning_graphs() -> None: assert components.worker._checkpoint_reader is components.driver assert components.worker._command_executor is components.driver agent_executor = components.driver._node_executor._agent_executor - assert isinstance(agent_executor._verifier, ToolLedgerRuntimeVerifier) - reference_exists = agent_executor._verifier._reference_exists + assert isinstance(agent_executor._verifier, CompletionGateRuntimeVerifier) + assert isinstance(agent_executor._verifier._completion_gate, TaskCompletionGate) + deterministic = agent_executor._verifier._deterministic + assert isinstance(deterministic, ToolLedgerRuntimeVerifier) + reference_exists = deterministic._reference_exists assert reference_exists is not None assert isinstance(reference_exists.__self__, RuntimeToolReferenceReader) - assert agent_executor._verifier._result_store is not None + assert deterministic._result_store is not None + assert agent_executor._max_verification_repairs == 10 assert ( - agent_executor._verifier._result_store + deterministic._result_store is agent_executor._tool_service._tool_result_store ) assert (