diff --git a/CHANGELOG.md b/CHANGELOG.md index d6aa6cc2..28e7320e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ GitHub Releases page; `0.8.0` is the new starting line. - **Parallel streamed tool calls are now correlated safely.** Interleaved argument chunks stay attached to their indexed calls, malformed or truncated call streams stop before tool execution, and failed attempts are not retried after output has already been shown. - **Provider compatibility and Z.AI routing are now explicit.** Immutable compatibility profiles keep request-format quirks behind the chat-provider boundary, while independent Z.AI Coding Plan and API login routes use separate credentials, endpoints, model identities, catalog refresh, logout, and usage/rate-limit state. Curated GLM requests now apply exact context/output limits, thinking controls, reasoning replay, and tool-stream support without activating for local or unknown models. +- **Tool execution is now supervised as a terminal batch.** A private execution engine preserves the Toolset registry and legacy per-call API while centralizing ordered results, deduplication, callbacks, and batch summaries; cancellation is bounded, late work stays owned, and new batches fail closed until timed-out cleanup drains. ## 0.58.0 (2026-07-11) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe4ddbda..5f76f42b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,3 +48,19 @@ If you believe a new runtime dependency is genuinely necessary: automatically so reviewers know to look for the justification. Dev-only dependencies under `[dependency-groups]` are not subject to this policy. + +## Tool-execution characterization + +Changes to `soul/tool_execution.py`, execution scheduling, deduplication, the reader/writer gate, or +MCP publication should run the deterministic local characterization harness: + +```bash +uv run python scripts/benchmark_toolset.py --scenario all --runs 5 --output before-or-after.json +``` + +For before/after evidence, use the same machine, Python environment, fixture matrix, warm-up count, +and five-run command at both revisions. Keep the raw JSON and report timing regressions as well as +improvements; do not discard samples or use `--smoke` for final evidence. Confirm every scenario has +zero leaked tasks/processes/sessions, all cancellation and recovery flags are true, registry hashes +are stable, and deterministic decision states are derived by the harness. Treat results as directional +local engineering evidence, not universal product telemetry. diff --git a/docs/en/customization/agent-architecture.md b/docs/en/customization/agent-architecture.md index 51ffe707..851ed063 100644 --- a/docs/en/customization/agent-architecture.md +++ b/docs/en/customization/agent-architecture.md @@ -180,8 +180,9 @@ sequenceDiagram ```mermaid flowchart LR Core[pythinker_core.step] - Call[ToolCall] - Toolset[PythinkerToolset.handle] + Calls[Terminal ToolCall batch] + Toolset[PythinkerToolset.handle_batch] + Engine[ToolExecutionEngine] Parse[Parse JSON arguments] Pre[PreToolUse hooks] Execute[tool.call(arguments)] @@ -193,10 +194,13 @@ flowchart LR Post[PostToolUse or PostToolUseFailure hooks] Telemetry[Telemetry] Result[ToolResult] + Batch[ToolBatchHandle] + Step[StepResult.tool_results] + Soul[PythinkerSoul] Context[tool_result_to_message -> Context] Wire[Wire event stream] - Core --> Call --> Toolset --> Parse --> Pre --> Execute + Core --> Calls --> Toolset --> Engine --> Parse --> Pre --> Execute Execute --> Builtin Execute --> Plugin Execute --> MCP @@ -208,12 +212,26 @@ flowchart LR Builtin --> Result Approval --> Result Result --> Post --> Telemetry - Result --> Core + Result --> Batch --> Step --> Soul Result --> Wire - Result --> Context + Soul --> Context ``` -The toolset is both a registry and an execution boundary. It hides tools from the LLM when needed, validates tool names, parses JSON arguments, triggers hooks, converts exceptions to `ToolRuntimeError`, and returns async `ToolResult` tasks to `pythinker_core.step`. The advertised tool list is filtered by the active execution profile, subagent/root role, plan-mode state, and hard permission profile before each model call; tool-specific execution guards still run even if a hidden tool is somehow called. MCP tools are registered as local wrappers. Wire external tools are sent to the active Wire client as `ToolCallRequest` messages and wait for a client-provided result. +The toolset is both a registry and an execution boundary. For Pythinker Soul, `pythinker_core.step` +passes the complete terminal call batch and immutable `ToolBatchContext` to +`PythinkerToolset.handle_batch`. Its non-exported `ToolExecutionEngine` validates tool names, parses +JSON arguments, applies duplicate/consecutive-call policy, schedules calls through the reader-writer +gate, triggers hooks, converts exceptions to `ToolRuntimeError`, and supervises ordered results and +bounded cancellation through a `ToolBatchHandle`. `StepResult.tool_results()` settles that owned +batch before Soul appends the assistant and tool messages to `Context`. Core's per-call +`Toolset.handle()` path remains only as the compatibility fallback for toolsets that do not +implement `BatchToolset`. + +The advertised tool list is filtered by the active execution profile, subagent/root role, +plan-mode state, and hard permission profile before each model call; tool-specific execution guards +still run even if a hidden tool is somehow called. MCP tools are registered as local wrappers. Wire +external tools are sent to the active Wire client as `ToolCallRequest` messages and wait for a +client-provided result. ## Subagent graph diff --git a/docs/en/customization/architecture.md b/docs/en/customization/architecture.md index 28a31f7c..38fc2ad4 100644 --- a/docs/en/customization/architecture.md +++ b/docs/en/customization/architecture.md @@ -66,8 +66,10 @@ The end-to-end flow when a session starts and processes a turn: 4. **Core loop** — `src/pythinker_code/soul/pythinkersoul.py:PythinkerSoul.run` handles user input and slash commands, calls the LLM through `pythinker_core.step`, runs tools, gates side effects through approvals, injects dynamic reminders, and compacts the context. -5. **Tool execution** — `src/pythinker_code/soul/toolset.py:PythinkerToolset` loads built-in - and MCP tools, injects dependencies, executes calls, and returns structured results. +5. **Tool execution** — `src/pythinker_code/soul/toolset.py:PythinkerToolset` owns the + built-in/MCP registry, visibility, and dependency injection facade. The private + `src/pythinker_code/soul/tool_execution.py:ToolExecutionEngine` executes terminal batches, + deduplicates calls, orders results, and supervises bounded cancellation. 6. **Wire and UI** — `src/pythinker_code/soul/run_soul` connects the soul to `src/pythinker_code/wire/`; Shell, Print, ACP, Web, and Dashboard frontends consume Wire events. @@ -99,7 +101,8 @@ canonical list lives in `src/pythinker_code/wire/types.py` (`Event` union): `Ste | `src/pythinker_code/soul/pythinkersoul.py` | Core loop: user input, slash commands, LLM calls, tool runs, compaction, telemetry spans. | `PythinkerSoul`, `PythinkerSoul.run`, `FLOW_COMMAND_PREFIX` | | `src/pythinker_code/soul/agent.py` | `Runtime` and `Agent` construction, system-prompt assembly, AGENTS.md discovery. | `Runtime`, `Agent`, `load_agent`, `load_agents_md`, `BuiltinSystemPromptArgs` | | `src/pythinker_code/soul/context.py` | Conversation history, checkpoints, JSONL persistence. | `Context` | -| `src/pythinker_code/soul/toolset.py` | Loads built-in + MCP tools, injects deps, executes calls. | `PythinkerToolset` | +| `src/pythinker_code/soul/toolset.py` | Owns built-in + MCP registration, visibility, dependency injection, and the legacy per-call facade. | `PythinkerToolset` | +| `src/pythinker_code/soul/tool_execution.py` | Private batch execution state machine: preparation, deduplication, reader/writer scheduling, callbacks, ordered results, bounded cancellation, poison, and late-task recovery. | `ToolExecutionEngine` | | `src/pythinker_code/soul/slash.py` | Slash-command registry and dispatch. | `registry` | | `src/pythinker_code/soul/dynamic_injection.py` (+ `dynamic_injections/`) | Injects budgeted `` content per step: plan-mode, auto-mode, model-defense, LSP diagnostics. | `DynamicInjectionProvider` | | `src/pythinker_code/soul/permission.py` | Per-step permission profiles (`read_only`/`plan`/`ask`/`implement`/`review`/`verify`) and destructiveness classification. | `tool_destructive_reason`, `shell_command_signature` | @@ -299,7 +302,7 @@ Both frontends build with `tsc -b && vite build` and are synced into the Python | Path | Purpose | Key entry points and interfaces | | --- | --- | --- | -| `packages/pythinker-core/` | LLM abstraction: message models, streaming chat providers, tool abstractions, and the `generate`/`step` primitives. Independently versioned (1.x). | `generate`, `step`, `Message`, `ContentPart`, `ToolCall`, `ChatProvider`, `Toolset`, `ToolReturnValue`/`ToolOk`/`ToolError`, `CallableTool2`, `DisplayBlock`; contrib providers (`Anthropic`, `GoogleGenAI`, `OpenAIResponses`) and `LinearContext` | +| `packages/pythinker-core/` | LLM abstraction: message models, streaming chat providers, optional batch/legacy tool dispatch, and the `generate`/`step` primitives. Independently versioned (1.x). | `generate`, `step`, `StepResult`, `Message`, `ContentPart`, `ToolCall`, `ChatProvider`, `Toolset`, `BatchToolset`, `ToolBatchHandle`, `ToolReturnValue`/`ToolOk`/`ToolError`, `CallableTool2`, `DisplayBlock`; contrib providers (`Anthropic`, `GoogleGenAI`, `OpenAIResponses`) and `LinearContext` | | `packages/pythinker-host/` | OS abstraction for filesystem + shell across local and SSH backends via a context-var-dispatched `Host` protocol. | `Host`, `HostPath`, `LocalHost`, `HostProcess`, `get_current_host`/`set_current_host` | | `packages/pythinker-review/` | Standalone review/security/debug engine and stateful Reviewflow; strict Pydantic schemas with fail-closed evidence validation. State in `.pythinker-review/` and `.pythinker-review-flow/`. See `packages/pythinker-review/AGENTS.md`. | `run_engine`, `ReviewLLM`, `Finding`, `RawFinding`, `ReviewerOutput`, Reviewflow `init`/`map`/`review`/`fix` | | `packages/pythinker-code/` | Thin distribution package exposing the `pythinker-code` script. | — | diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md index 4dca28bf..437dfbf5 100644 --- a/docs/en/release-notes/changelog.md +++ b/docs/en/release-notes/changelog.md @@ -19,6 +19,7 @@ GitHub Releases page; `0.8.0` is the new starting line. - **Parallel streamed tool calls are now correlated safely.** Interleaved argument chunks stay attached to their indexed calls, malformed or truncated call streams stop before tool execution, and failed attempts are not retried after output has already been shown. - **Provider compatibility and Z.AI routing are now explicit.** Immutable compatibility profiles keep request-format quirks behind the chat-provider boundary, while independent Z.AI Coding Plan and API login routes use separate credentials, endpoints, model identities, catalog refresh, logout, and usage/rate-limit state. Curated GLM requests now apply exact context/output limits, thinking controls, reasoning replay, and tool-stream support without activating for local or unknown models. +- **Tool execution is now supervised as a terminal batch.** A private execution engine preserves the Toolset registry and legacy per-call API while centralizing ordered results, deduplication, callbacks, and batch summaries; cancellation is bounded, late work stays owned, and new batches fail closed until timed-out cleanup drains. ## 0.58.0 (2026-07-11) diff --git a/docs/superpowers/plans/2026-07-15-tool-execution-cancellation-state-rollback.md b/docs/superpowers/plans/2026-07-15-tool-execution-cancellation-state-rollback.md new file mode 100644 index 00000000..0d07b73e --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-tool-execution-cancellation-state-rollback.md @@ -0,0 +1,240 @@ +# Tool Execution Cancellation State Rollback Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Prevent cancelled or failed tool batches from polluting cross-step deduplication and consecutive-call state. + +**Architecture:** Keep each batch's call fingerprints uncommitted until watcher settlement succeeds. On cancellation or failure, clear only the engine's current-step state while preserving previously committed fingerprints, completed-result snapshots, and cancellation supervision. + +**Tech Stack:** Python 3.14, asyncio, Pythinker batch-tool contracts, pytest/pytest-asyncio, Ruff, Pyright, ty. + +## Global Constraints + +- Use `uv` or repository `make` targets for every Python command. +- Add no dependency, configuration key, public API, telemetry field, or persisted-data change. +- Preserve ordered results, callback suppression, completion snapshots, bounded cancellation, timeout poisoning, and original exception propagation. +- Keep the production change private to `ToolExecutionEngine` and `_ExecutionBatch`. +- Follow strict TDD: observe the regression test fail for the stale dedup state before editing production code. +- Do not weaken or remove existing cancellation, engine, core, or E2E coverage. + +--- + +### Task 1: Roll Back Unsettled Batch State + +**Files:** +- Modify: `src/pythinker_code/soul/tool_execution.py:395-400` +- Modify: `src/pythinker_code/soul/tool_execution.py:832-864` +- Test: `tests/core/test_tool_execution_cancellation.py` + +**Interfaces:** +- Consumes: `ToolExecutionEngine.begin_step(...)`, `ToolExecutionEngine.end_step()`, `_ExecutionBatch._run()`, `ToolBatchContext.prior_call_fingerprints`, and `ToolBatchSummary`. +- Produces: internal `ToolExecutionEngine.abort_step() -> None` on the non-exported engine; successful batches remain finalized through `end_step()`, while cancelled or failed batches discard only their current-step dedup state. + +- [x] **Step 1: Add the cancellation rollback regression test** + +```python +async def test_cancelled_batch_does_not_commit_cross_step_dedup_state() -> None: + stubborn = CancellationIgnoringTool() + immediate = ImmediateTool() + toolset = PythinkerToolset() + toolset.add(stubborn) + toolset.add(immediate) + + first = toolset.handle_batch( + [_call("first", "Immediate")], + ToolBatchContext(turn_id="turn", step_no=1), + ) + await first.results() + prior = first.summary.current_call_fingerprints + + cancelled = toolset.handle_batch( + [_call("cancelled", "Stubborn")], + ToolBatchContext( + turn_id="turn", + step_no=2, + prior_call_fingerprints=prior, + ), + ) + await stubborn.started.wait() + settlement = asyncio.create_task(cancelled.cancel_and_settle()) + await stubborn.cancel_seen.wait() + stubborn.release.set() + await settlement + + retry = toolset.handle_batch( + [_call("retry", "Stubborn")], + ToolBatchContext( + turn_id="turn", + step_no=2, + prior_call_fingerprints=prior, + ), + ) + assert [result.tool_call_id for result in await retry.results()] == ["retry"] + assert retry.summary.dedup_triggered is False + assert retry.summary.consecutive_identical_call_count == 1 + assert stubborn.invocations == 2 +``` + +- [x] **Step 2: Run the regression test and verify RED** + +Run: + +```bash +uv run pytest tests/core/test_tool_execution_cancellation.py::test_cancelled_batch_does_not_commit_cross_step_dedup_state -q +``` + +Expected: FAIL because the retry summary reports `dedup_triggered is True` and consecutive count 2, proving the cancelled call was committed. + +- [x] **Step 3: Add a private current-step abort operation** + +Add beside `end_step()`: + +```python +def abort_step(self) -> None: + """Discard uncommitted state for the current execution step.""" + if self._step_closed: + return + self._current_step_calls = [] + self._current_step_tasks = {} + self._dedup_triggered = False + self._step_closed = True +``` + +The method intentionally leaves `_seen_call_keys`, `_consecutive_key`, and `_consecutive_count` +unchanged because they represent prior successfully committed steps. + +- [x] **Step 4: Commit only after successful watcher settlement** + +Reshape `_ExecutionBatch._run()` so it awaits watcher results before finalizing: + +```python +self._watcher_tasks = [ + asyncio.create_task(self._watch(future)) for future in self._source_futures +] +results = list(await asyncio.gather(*self._watcher_tasks)) +self._engine.end_step() +self._summary = self._engine.summary +return results +``` + +In the `except BaseException` cleanup, retain the existing snapshot/cancel/gather sequence, then +replace fallback `end_step()` finalization with: + +```python +self._engine.abort_step() +raise +``` + +- [x] **Step 5: Run focused tests and verify GREEN** + +Run: + +```bash +uv run pytest tests/core/test_tool_execution_cancellation.py::test_cancelled_batch_does_not_commit_cross_step_dedup_state -q +uv run pytest tests/core/test_tool_execution_engine.py tests/core/test_tool_execution_cancellation.py -q +``` + +Expected: the regression passes; all engine and cancellation tests pass with only the documented +third-party Loguru deprecation warning. + +- [x] **Step 6: Run package verification** + +Run: + +```bash +make check-pythinker-core +make test-pythinker-core +make check-pythinker-code +make test-pythinker-code +git diff --check +``` + +Observed: every command exited 0. The core check passed Ruff, formatting, and Pyright; its +repository-configured non-blocking `ty` invocation continued to report existing third-party typing +diagnostics. The CLI check passed Ruff, formatting, Pyright, and blocking `ty`. Core tests reported +431 passed; CLI tests reported 7,066 passed, 9 skipped, and 1 expected xfail; separate E2E tests +reported 65 passed and 4 skipped. `git diff --check` produced no output. + +- [x] **Step 7: Commit the reviewed implementation** + +```bash +git add src/pythinker_code/soul/tool_execution.py \ + tests/core/test_tool_execution_cancellation.py +git commit -m "fix(tools): roll back cancelled batch state" +``` + +--- + +### Task 2: Bound Owned Async Callback Cancellation + +**Files:** +- Modify: `packages/pythinker-core/src/pythinker_core/__init__.py` +- Test: `packages/pythinker-core/tests/test_batch_toolset.py` +- Test: `tests/core/test_tool_execution_cancellation.py` + +**Contract:** A caller cancellation gets one finite ownership deadline. Batch settlement receives +the remaining budget, then owned async result callbacks receive only the budget still available. +Cancellation-resistant callbacks remain tracked and failure-observed after timeout, but cannot +block the caller indefinitely or hide an earlier batch timeout. + +- [x] Add a regression with an async result callback that catches `CancelledError` and waits for an + explicit release. Verify the current `tool_results()` cancellation remains pending past the + intended short bound. +- [x] Implement deadline-aware callback settlement with `asyncio.wait`, not `wait_for(gather(...))`, + because cancelling a gather can itself wait forever for cancellation-resistant tasks. +- [x] Preserve the first batch/cancellation failure while still cancelling callbacks; add truthful + timeout reporting for a callback-only timeout and retain pending callback futures until done. +- [x] Update the existing core timeout integration test to control the new owner deadline and run + the focused core/cancellation suites. + +### Task 3: Preserve Tool Lineage on Cancellation Timeout + +**Files:** +- Modify: `src/pythinker_code/soul/pythinkersoul.py` +- Test: `tests/core/test_pythinkersoul_turn_balance.py` or a focused sibling module + +**Contract:** Both `CancelledError` and `ToolCancellationTimeoutError` repair context before they +propagate. Completed results remain authoritative. Unfinished calls get an interrupted marker for +ordinary cancellation or an explicit completion-unknown/do-not-retry marker for timeout. + +- [x] Add a real Soul + real `PythinkerToolset` regression where one tool completes and another + ignores cancellation through a short deadline. Verify the current timeout path leaves the + assistant tool calls unanswered. +- [x] Persist the completed snapshot and timeout-specific unknown markers under a shielded context + write, then re-raise the original typed timeout. +- [x] Verify no duplicate context write, no `_last_tool_calls` commit for the unsettled batch, and + unchanged ordinary-cancellation wording. + +### Task 4: Integrate Late Engine Work into Runtime Cleanup + +**Files:** +- Modify: `src/pythinker_code/soul/tool_execution.py` +- Modify: `src/pythinker_code/soul/toolset.py` +- Test: `tests/core/test_tool_execution_cancellation.py` + +**Contract:** Toolset cleanup owns engine late-drain observers. It waits within a finite bound, +returns normally when late work drains, and raises `ToolCancellationTimeoutError` after closing MCP +resources when work survives the bound. It never detaches a still-running supervisor by cancelling +its drain observer. + +- [x] Add regressions for cleanup waiting until a timed-out tool is released and for bounded, + truthful failure while the tool remains cancellation-resistant. +- [x] Add an idempotent engine cleanup operation using bounded `asyncio.wait` over retained drain + tasks and explicit timeout validation/reporting. +- [x] Wire engine cleanup into `PythinkerToolset.cleanup()` while guaranteeing MCP close attempts + still run before any retained engine error propagates. +- [x] Preserve caller cancellation raised by engine cleanup until after MCP session/client teardown, + then re-raise the original `CancelledError`. + +### Task 5: Documentation, Full Gates, and Re-review + +**Files:** +- Modify: `docs/en/customization/agent-architecture.md` +- Modify: this plan, its design, `tasks/todo.md`, and `tasks/lessons.md` + +- [x] Update the detailed architecture flow to use `handle_batch()` and `ToolExecutionEngine` for + Pythinker Soul, with per-call `handle()` labeled as the legacy non-batch fallback. +- [x] Run focused tests for all three Important findings, then the core and CLI full package gates + and `git diff --check`. +- [x] Re-run task-scoped reviews and a fresh complete branch review. Push only with no open + Critical/Important findings. diff --git a/docs/superpowers/reports/2026-07-15-tool-execution-after.json b/docs/superpowers/reports/2026-07-15-tool-execution-after.json new file mode 100644 index 00000000..24c7256a --- /dev/null +++ b/docs/superpowers/reports/2026-07-15-tool-execution-after.json @@ -0,0 +1,3177 @@ +{ + "schema_version": 2, + "environment": { + "python_version": "3.14.6", + "python_implementation": "CPython", + "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O" + }, + "scenarios": [ + { + "fixture": { + "kind": "execution_safe", + "size": 1, + "concurrency": 1, + "payload_bytes": null, + "composition": "safe" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1688666, + 1546333, + 1564708, + 1755458, + 1690958 + ], + "median_ns": 1688666, + "p95_ns": 1755458, + "throughput_per_second": 592.1834157850043, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 14584, + 6958, + 7875, + 9583, + 8375 + ], + "median_ns": 8375, + "p95_ns": 14584, + "throughput_per_second": 119402.98507462686, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 12083, + 7292, + 7833, + 13500, + 7791 + ], + "median_ns": 7833, + "p95_ns": 13500, + "throughput_per_second": 127665.00702157538, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1676583, + 1539041, + 1556875, + 1741958, + 1683167 + ], + "median_ns": 1676583, + "p95_ns": 1741958, + "throughput_per_second": 596.4512344452974, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 107689, + "retained_object_delta": 93967, + "cancellation": { + "completed": true, + "completion_ns": 7902584, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_safe", + "size": 10, + "concurrency": 10, + "payload_bytes": null, + "composition": "safe" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 15266666, + 15930041, + 15244958, + 15623333, + 14660333 + ], + "median_ns": 15266666, + "p95_ns": 15930041, + "throughput_per_second": 655.0218626647102, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 59918, + 56583, + 59791, + 65584, + 53999 + ], + "median_ns": 59791, + "p95_ns": 65584, + "throughput_per_second": 167249.25155959927, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 68667, + 66126, + 57961, + 73584, + 54794 + ], + "median_ns": 66126, + "p95_ns": 73584, + "throughput_per_second": 151226.44648096059, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 15197999, + 15863915, + 15186997, + 15549749, + 14605539 + ], + "median_ns": 15197999, + "p95_ns": 15863915, + "throughput_per_second": 657.9813566246452, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 206389, + "retained_object_delta": 174949, + "cancellation": { + "completed": true, + "completion_ns": 7838750, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 10, + "operation_count": 50, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_safe", + "size": 100, + "concurrency": 100, + "payload_bytes": null, + "composition": "safe" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 154362875, + 156570875, + 158434375, + 156119292, + 157093875 + ], + "median_ns": 156570875, + "p95_ns": 158434375, + "throughput_per_second": 638.688389523275, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 500499, + 493754, + 517836, + 494035, + 510496 + ], + "median_ns": 500499, + "p95_ns": 517836, + "throughput_per_second": 199800.5990021958, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 521916, + 498340, + 525746, + 491167, + 504749 + ], + "median_ns": 504749, + "p95_ns": 525746, + "throughput_per_second": 198118.27264640445, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 153840959, + 156072535, + 157908629, + 155628125, + 156589126 + ], + "median_ns": 156072535, + "p95_ns": 157908629, + "throughput_per_second": 640.7277231705117, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 2329899, + "retained_object_delta": 2102649, + "cancellation": { + "completed": true, + "completion_ns": 7600042, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 100, + "operation_count": 500, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_exclusive", + "size": 1, + "concurrency": 1, + "payload_bytes": null, + "composition": "exclusive" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1512958, + 1495958, + 1534333, + 1526958, + 1501666 + ], + "median_ns": 1512958, + "p95_ns": 1534333, + "throughput_per_second": 660.9568804950303, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 4666, + 3958, + 3875, + 4334, + 3958 + ], + "median_ns": 3958, + "p95_ns": 4666, + "throughput_per_second": 252652.85497726125, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 5833, + 5375, + 5375, + 5708, + 5625 + ], + "median_ns": 5625, + "p95_ns": 5833, + "throughput_per_second": 177777.77777777778, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1507125, + 1490583, + 1528958, + 1521250, + 1496041 + ], + "median_ns": 1507125, + "p95_ns": 1528958, + "throughput_per_second": 663.5149705565232, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 76874, + "retained_object_delta": 61949, + "cancellation": { + "completed": true, + "completion_ns": 7361083, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_exclusive", + "size": 10, + "concurrency": 10, + "payload_bytes": null, + "composition": "exclusive" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 14227208, + 14658083, + 14810708, + 14360625, + 14773958 + ], + "median_ns": 14658083, + "p95_ns": 14810708, + "throughput_per_second": 682.2174495805489, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 38499, + 37916, + 37083, + 38750, + 42208 + ], + "median_ns": 38499, + "p95_ns": 42208, + "throughput_per_second": 259747.00641575106, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 52291, + 52583, + 50461, + 56291, + 56958 + ], + "median_ns": 52583, + "p95_ns": 56958, + "throughput_per_second": 190175.53201605083, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 14174917, + 14605500, + 14760247, + 14304334, + 14717000 + ], + "median_ns": 14605500, + "p95_ns": 14760247, + "throughput_per_second": 684.6735818698436, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 198764, + "retained_object_delta": 167339, + "cancellation": { + "completed": true, + "completion_ns": 7633334, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 10, + "operation_count": 50, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_exclusive", + "size": 100, + "concurrency": 100, + "payload_bytes": null, + "composition": "exclusive" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 145635208, + 148674167, + 158247000, + 151302000, + 150126208 + ], + "median_ns": 150126208, + "p95_ns": 158247000, + "throughput_per_second": 666.1062137798085, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 390830, + 405704, + 479126, + 435208, + 425133 + ], + "median_ns": 425133, + "p95_ns": 479126, + "throughput_per_second": 235220.50746472282, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 579170, + 661917, + 866379, + 686500, + 639122 + ], + "median_ns": 661917, + "p95_ns": 866379, + "throughput_per_second": 151076.3434086147, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 145056038, + 148012250, + 157380621, + 150615500, + 149487086 + ], + "median_ns": 149487086, + "p95_ns": 157380621, + "throughput_per_second": 668.9541061761014, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 2185965, + "retained_object_delta": 1978377, + "cancellation": { + "completed": true, + "completion_ns": 7616708, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 100, + "operation_count": 500, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_mixed", + "size": 1, + "concurrency": 2, + "payload_bytes": null, + "composition": "reader/writer pairs" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 3282292, + 3811083, + 3252958, + 3317125, + 3155667 + ], + "median_ns": 3282292, + "p95_ns": 3811083, + "throughput_per_second": 609.3303094301177, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 176833, + 222583, + 172126, + 170875, + 149458 + ], + "median_ns": 172126, + "p95_ns": 222583, + "throughput_per_second": 11619.39509429139, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 221667, + 307541, + 230375, + 249333, + 197709 + ], + "median_ns": 230375, + "p95_ns": 307541, + "throughput_per_second": 8681.497558328812, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 3060625, + 3503542, + 3022583, + 3067792, + 2957958 + ], + "median_ns": 3060625, + "p95_ns": 3503542, + "throughput_per_second": 653.4613028384725, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "8c718a5dc30b54eeabc4aba7d1a5910c7d3b727e3547c8f82451d70bb73c7ba6", + "allocation_peak_bytes": 113773, + "retained_object_delta": 97720, + "cancellation": { + "completed": true, + "completion_ns": 8202084, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 2, + "operation_count": 10, + "category_counts": { + "builtin": 2 + }, + "projection_counts": { + "visible": 2 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_mixed", + "size": 10, + "concurrency": 20, + "payload_bytes": null, + "composition": "reader/writer pairs" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 29411125, + 28984541, + 30004625, + 32291708, + 30750125 + ], + "median_ns": 30004625, + "p95_ns": 32291708, + "throughput_per_second": 666.5639047313539, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 26280625, + 26012124, + 26850708, + 29058083, + 27374250 + ], + "median_ns": 26850708, + "p95_ns": 29058083, + "throughput_per_second": 744.8593161863739, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1954624, + 1652622, + 1730874, + 1855085, + 2182667 + ], + "median_ns": 1855085, + "p95_ns": 2182667, + "throughput_per_second": 10781.17714282634, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 27456501, + 27331919, + 28273751, + 30436623, + 28567458 + ], + "median_ns": 28273751, + "p95_ns": 30436623, + "throughput_per_second": 707.369885233834, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "8c718a5dc30b54eeabc4aba7d1a5910c7d3b727e3547c8f82451d70bb73c7ba6", + "allocation_peak_bytes": 476209, + "retained_object_delta": 223541, + "cancellation": { + "completed": true, + "completion_ns": 8466709, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 20, + "operation_count": 100, + "category_counts": { + "builtin": 2 + }, + "projection_counts": { + "visible": 2 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_mixed", + "size": 100, + "concurrency": 200, + "payload_bytes": null, + "composition": "reader/writer pairs" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 302660416, + 311995375, + 303379667, + 381853125, + 377032750 + ], + "median_ns": 311995375, + "p95_ns": 381853125, + "throughput_per_second": 641.0351435498043, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 299289708, + 308480042, + 300198750, + 378263916, + 373211542 + ], + "median_ns": 308480042, + "p95_ns": 378263916, + "throughput_per_second": 648.3401606902012, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 16814244, + 17194213, + 19609082, + 34768838, + 18789082 + ], + "median_ns": 18789082, + "p95_ns": 34768838, + "throughput_per_second": 10644.479597246955, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 285846172, + 294801162, + 283770585, + 347084287, + 358243668 + ], + "median_ns": 294801162, + "p95_ns": 358243668, + "throughput_per_second": 678.4233774492382, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "8c718a5dc30b54eeabc4aba7d1a5910c7d3b727e3547c8f82451d70bb73c7ba6", + "allocation_peak_bytes": 2876006, + "retained_object_delta": 2121256, + "cancellation": { + "completed": true, + "completion_ns": 8153500, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 200, + "operation_count": 1000, + "category_counts": { + "builtin": 2 + }, + "projection_counts": { + "visible": 2 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "dedupe", + "size": 1024, + "concurrency": 2, + "payload_bytes": 1024, + "composition": "same-step duplicate payload" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1769750, + 1815583, + 1820500, + 1866125, + 1728584 + ], + "median_ns": 1815583, + "p95_ns": 1866125, + "throughput_per_second": 550.787267781203, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 11333, + 11209, + 9541, + 12208, + 9417 + ], + "median_ns": 11209, + "p95_ns": 12208, + "throughput_per_second": 89214.0244446427, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 17500, + 13333, + 12500, + 18416, + 10583 + ], + "median_ns": 13333, + "p95_ns": 18416, + "throughput_per_second": 75001.87504687617, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1752250, + 1802250, + 1808000, + 1847709, + 1718001 + ], + "median_ns": 1802250, + "p95_ns": 1847709, + "throughput_per_second": 554.8619780829519, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 99448, + "retained_object_delta": 80135, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 2, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "dedupe", + "size": 102400, + "concurrency": 2, + "payload_bytes": 102400, + "composition": "same-step duplicate payload" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2738000, + 2620375, + 2296750, + 2266167, + 2299250 + ], + "median_ns": 2299250, + "p95_ns": 2738000, + "throughput_per_second": 434.9244318799609, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 9625, + 13542, + 9292, + 10125, + 12000 + ], + "median_ns": 10125, + "p95_ns": 13542, + "throughput_per_second": 98765.43209876544, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 27792, + 17458, + 13958, + 13875, + 14541 + ], + "median_ns": 14541, + "p95_ns": 27792, + "throughput_per_second": 68771.06113747336, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2710208, + 2602917, + 2282792, + 2252292, + 2284709 + ], + "median_ns": 2284709, + "p95_ns": 2710208, + "throughput_per_second": 437.6925026338146, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 2137298, + "retained_object_delta": 1599623, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 2, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "dedupe", + "size": 1048576, + "concurrency": 2, + "payload_bytes": 1048576, + "composition": "same-step duplicate payload" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 7123625, + 7095667, + 6715500, + 7301458, + 6479458 + ], + "median_ns": 7095667, + "p95_ns": 7301458, + "throughput_per_second": 140.93107807905866, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 10542, + 11042, + 11334, + 11334, + 10667 + ], + "median_ns": 11042, + "p95_ns": 11334, + "throughput_per_second": 90563.30374932078, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 20042, + 15667, + 15666, + 26833, + 10375 + ], + "median_ns": 15667, + "p95_ns": 26833, + "throughput_per_second": 63828.429182357824, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 7103583, + 7080000, + 6699834, + 7274625, + 6469083 + ], + "median_ns": 7080000, + "p95_ns": 7274625, + "throughput_per_second": 141.24293785310735, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 21296848, + "retained_object_delta": 15791843, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 2, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "advertisement", + "size": 50, + "concurrency": 1, + "payload_bytes": null, + "composition": "builtin/plugin/MCP-style names with hidden entries" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "visibility_enabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 484042, + 307417, + 329125, + 302084, + 315291 + ], + "median_ns": 315291, + "p95_ns": 484042, + "throughput_per_second": 158583.6576369132, + "within_run_samples_ns": [] + }, + "visibility_enabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 313167, + 296750, + 298750, + 300417, + 316292 + ], + "median_ns": 300417, + "p95_ns": 316292, + "throughput_per_second": 166435.3215696848, + "within_run_samples_ns": [] + }, + "visibility_disabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 8500, + 7583, + 7125, + 7208, + 7375 + ], + "median_ns": 7375, + "p95_ns": 8500, + "throughput_per_second": 6779661.016949153, + "within_run_samples_ns": [] + }, + "visibility_disabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 5958, + 5500, + 5584, + 5667, + 5750 + ], + "median_ns": 5667, + "p95_ns": 5958, + "throughput_per_second": 8823010.411152285, + "within_run_samples_ns": [] + }, + "repeated_unchanged_projection": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1210458, + 1180333, + 1210125, + 1179333, + 1238416 + ], + "median_ns": 1210125, + "p95_ns": 1238416, + "throughput_per_second": 41318.04565644045, + "within_run_samples_ns": [] + }, + "registry_projection_p95": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 309958, + 320750, + 302542, + 303417, + 332584 + ], + "median_ns": 309958, + "p95_ns": 332584, + "throughput_per_second": 161312.17777892487, + "within_run_samples_ns": [ + [ + 298125, + 298084, + 296625, + 297292, + 296500, + 296666, + 296625, + 296208, + 295959, + 296583, + 295833, + 296458, + 296084, + 296792, + 307792, + 307541, + 313083, + 309958, + 297167, + 299333 + ], + [ + 293708, + 290375, + 320750, + 306000, + 296750, + 292542, + 284667, + 276875, + 282375, + 288167, + 291083, + 298959, + 329083, + 275500, + 274458, + 274083, + 298542, + 287084, + 288709, + 292750 + ], + [ + 296542, + 296958, + 298000, + 295792, + 299250, + 294084, + 293333, + 296750, + 298000, + 302292, + 293334, + 296959, + 292208, + 291625, + 291334, + 294375, + 290916, + 292542, + 402375, + 302542 + ], + [ + 293209, + 292833, + 295625, + 293292, + 299583, + 292334, + 291708, + 315209, + 303417, + 295084, + 292042, + 292583, + 292583, + 293709, + 295125, + 291709, + 291459, + 294250, + 297750, + 299500 + ], + [ + 304333, + 309458, + 304292, + 308792, + 308458, + 308334, + 307875, + 308916, + 314791, + 310416, + 304583, + 317916, + 311042, + 309667, + 304000, + 308625, + 304041, + 348542, + 332584, + 320250 + ] + ] + }, + "rebuild_after_mcp_publication": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 65833, + 56375, + 60750, + 54208, + 57417 + ], + "median_ns": 57417, + "p95_ns": 65833, + "throughput_per_second": 870822.2303498964, + "within_run_samples_ns": [] + } + }, + "registry_hash": "91398880bf75436e99f298a4b5b639ab91a13d6bb2a211a3fa510015f449fa95", + "allocation_peak_bytes": 6602143, + "retained_object_delta": 5905681, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 0, + "operation_count": 250, + "category_counts": { + "builtin": 17, + "plugin": 17, + "mcp": 16 + }, + "projection_counts": { + "enabled_hidden": 45, + "enabled_unhidden": 50, + "disabled_hidden": 45, + "disabled_unhidden": 50, + "rebuild": 50, + "repeated": 50 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "advertisement", + "size": 500, + "concurrency": 1, + "payload_bytes": null, + "composition": "builtin/plugin/MCP-style names with hidden entries" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "visibility_enabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2868375, + 2810458, + 2815584, + 2861208, + 3086000 + ], + "median_ns": 2861208, + "p95_ns": 3086000, + "throughput_per_second": 174751.3637596428, + "within_run_samples_ns": [] + }, + "visibility_enabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 3127750, + 3031792, + 3042875, + 3163916, + 3291291 + ], + "median_ns": 3127750, + "p95_ns": 3291291, + "throughput_per_second": 159859.32379506034, + "within_run_samples_ns": [] + }, + "visibility_disabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 46417, + 46417, + 47417, + 67750, + 51084 + ], + "median_ns": 47417, + "p95_ns": 67750, + "throughput_per_second": 10544741.337494992, + "within_run_samples_ns": [] + }, + "visibility_disabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 44708, + 44500, + 46000, + 44292, + 45500 + ], + "median_ns": 44708, + "p95_ns": 46000, + "throughput_per_second": 11183680.773016015, + "within_run_samples_ns": [] + }, + "repeated_unchanged_projection": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 12327458, + 12218250, + 12597500, + 12687834, + 13001708 + ], + "median_ns": 12597500, + "p95_ns": 13001708, + "throughput_per_second": 39690.41476483429, + "within_run_samples_ns": [] + }, + "registry_projection_p95": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 3320125, + 3142459, + 3247792, + 3199125, + 3311208 + ], + "median_ns": 3247792, + "p95_ns": 3320125, + "throughput_per_second": 153950.74561425115, + "within_run_samples_ns": [ + [ + 3214458, + 3168209, + 3149042, + 3143208, + 3057250, + 3149458, + 3128334, + 3120250, + 3142292, + 3126875, + 3351542, + 3257042, + 3181083, + 3254458, + 3196000, + 3175458, + 3320125, + 3116917, + 3085750, + 3066041 + ], + [ + 3094209, + 3050375, + 3142459, + 3070959, + 3025333, + 3025708, + 3063042, + 3016334, + 3027333, + 3162250, + 3013666, + 3027458, + 3128209, + 3141083, + 3032959, + 3030709, + 3076208, + 3031708, + 3024625, + 3032417 + ], + [ + 3007125, + 3018250, + 2997667, + 3004458, + 2980375, + 3007750, + 3149833, + 3283292, + 3194875, + 3100583, + 3093459, + 3073375, + 3070958, + 3073458, + 3077209, + 3108083, + 3114208, + 3129500, + 3247792, + 3174250 + ], + [ + 3019084, + 2999667, + 3264958, + 3199125, + 3030833, + 3071292, + 3032500, + 2983708, + 2992417, + 2969709, + 2994542, + 2991916, + 3106666, + 3007583, + 3121000, + 3135125, + 3070917, + 3046250, + 3081292, + 3036500 + ], + [ + 3316292, + 3208667, + 3145542, + 3182708, + 3142500, + 3185417, + 3111500, + 3180916, + 3144375, + 3215000, + 3311208, + 3246583, + 3193083, + 3258417, + 3132959, + 3153667, + 3227250, + 3221208, + 3145292, + 3251041 + ] + ] + }, + "rebuild_after_mcp_publication": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 408833, + 407042, + 397416, + 395000, + 409083 + ], + "median_ns": 407042, + "p95_ns": 409083, + "throughput_per_second": 1228374.4674996682, + "within_run_samples_ns": [] + } + }, + "registry_hash": "fd65db87d53a9d0420eae828c8caa4d54b5ead8fb090f7ae2878371ca84108d4", + "allocation_peak_bytes": 11559508, + "retained_object_delta": 6714451, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 0, + "operation_count": 2500, + "category_counts": { + "builtin": 167, + "plugin": 167, + "mcp": 166 + }, + "projection_counts": { + "enabled_hidden": 450, + "enabled_unhidden": 500, + "disabled_hidden": 450, + "disabled_unhidden": 500, + "rebuild": 500, + "repeated": 500 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "advertisement", + "size": 5000, + "concurrency": 1, + "payload_bytes": null, + "composition": "builtin/plugin/MCP-style names with hidden entries" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "visibility_enabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 30871042, + 30260417, + 29952083, + 29601708, + 29612125 + ], + "median_ns": 29952083, + "p95_ns": 30871042, + "throughput_per_second": 166933.29809482698, + "within_run_samples_ns": [] + }, + "visibility_enabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 33888458, + 32779792, + 33147333, + 32837750, + 32798584 + ], + "median_ns": 32837750, + "p95_ns": 33888458, + "throughput_per_second": 152263.78177554795, + "within_run_samples_ns": [] + }, + "visibility_disabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 967125, + 457084, + 711792, + 572625, + 521292 + ], + "median_ns": 572625, + "p95_ns": 967125, + "throughput_per_second": 8731717.965509715, + "within_run_samples_ns": [] + }, + "visibility_disabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 550708, + 448000, + 478417, + 464125, + 426792 + ], + "median_ns": 464125, + "p95_ns": 550708, + "throughput_per_second": 10772959.870724482, + "within_run_samples_ns": [] + }, + "repeated_unchanged_projection": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 130056500, + 130043458, + 131202291, + 128543708, + 129858625 + ], + "median_ns": 130043458, + "p95_ns": 131202291, + "throughput_per_second": 38448.685361781136, + "within_run_samples_ns": [] + }, + "registry_projection_p95": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 33275250, + 33333500, + 33587583, + 33553125, + 33606208 + ], + "median_ns": 33553125, + "p95_ns": 33606208, + "throughput_per_second": 149017.416410543, + "within_run_samples_ns": [ + [ + 32988875, + 32850208, + 32693375, + 32664333, + 32774125, + 32473417, + 32711250, + 33275250, + 32535958, + 33020417, + 32746250, + 32408792, + 32790000, + 32527791, + 32917250, + 33172417, + 33131833, + 33084500, + 32468000, + 33334459 + ], + [ + 32508750, + 32359416, + 32986917, + 33488875, + 33333500, + 33240375, + 33022042, + 33157625, + 32442167, + 32768125, + 32704000, + 32659292, + 32762125, + 32416083, + 32546417, + 32810416, + 32963584, + 33044583, + 32529334, + 33173791 + ], + [ + 32442666, + 32945500, + 32671750, + 32160459, + 32434875, + 32178750, + 31741458, + 31984542, + 31907000, + 31904917, + 32719042, + 32147208, + 32270958, + 33099208, + 33572292, + 33587583, + 33759375, + 33028125, + 33125625, + 32909250 + ], + [ + 32973417, + 33553125, + 34017583, + 32860667, + 33108250, + 33304333, + 32886500, + 32640750, + 32848959, + 32855167, + 32613125, + 32364708, + 32447000, + 32572167, + 32319959, + 32332667, + 32419917, + 32422875, + 31829958, + 32414458 + ], + [ + 32245334, + 32425625, + 32150000, + 32725500, + 31921708, + 32517500, + 32479958, + 32227125, + 32185875, + 32292167, + 32197042, + 32022125, + 32291584, + 32686541, + 33218000, + 34262292, + 32938542, + 33401625, + 33606208, + 32779417 + ] + ] + }, + "rebuild_after_mcp_publication": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 3646667, + 3536250, + 3685875, + 3465417, + 3544916 + ], + "median_ns": 3544916, + "p95_ns": 3685875, + "throughput_per_second": 1410470.6571326372, + "within_run_samples_ns": [] + } + }, + "registry_hash": "875959264842f6837daa22baebc202ca0055c523e46ad47dcd72b690337a549d", + "allocation_peak_bytes": 59043851, + "retained_object_delta": 14795475, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 0, + "operation_count": 25000, + "category_counts": { + "builtin": 1667, + "plugin": 1667, + "mcp": 1666 + }, + "projection_counts": { + "enabled_hidden": 4500, + "enabled_unhidden": 5000, + "disabled_hidden": 4500, + "disabled_unhidden": 5000, + "rebuild": 5000, + "repeated": 5000 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "mcp", + "size": 1, + "concurrency": 1, + "payload_bytes": null, + "composition": "deterministic fake server inventory publication" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "startup_to_ready": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1717102666, + 1725329250, + 1714311375, + 1716734875, + 1892837083 + ], + "median_ns": 1717102666, + "p95_ns": 1892837083, + "throughput_per_second": 0.5823763597837195, + "within_run_samples_ns": [] + }, + "mcp_lifecycle": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 6859833, + 6890500, + 6855917, + 6982208, + 8468083 + ], + "median_ns": 6890500, + "p95_ns": 8468083, + "throughput_per_second": 145.12734924896597, + "within_run_samples_ns": [] + }, + "time_to_first_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 6834042, + 6864208, + 6831375, + 6957083, + 8438833 + ], + "median_ns": 6864208, + "p95_ns": 8438833, + "throughput_per_second": 145.68323104428072, + "within_run_samples_ns": [] + }, + "time_to_settled_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 6859833, + 6890500, + 6855917, + 6982208, + 8468083 + ], + "median_ns": 6890500, + "p95_ns": 8468083, + "throughput_per_second": 145.12734924896597, + "within_run_samples_ns": [] + }, + "cleanup": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 149083, + 128750, + 131667, + 136625, + 146417 + ], + "median_ns": 136625, + "p95_ns": 149083, + "throughput_per_second": 7319.304666056724, + "within_run_samples_ns": [] + } + }, + "registry_hash": "dbb4989c3be2949f3beb5de69201d9a11de843d0cdb7622ed4cdee85e624c7af", + "allocation_peak_bytes": 6498192, + "retained_object_delta": 5777691, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 5, + "category_counts": { + "mcp": 1 + }, + "projection_counts": { + "visible": 1, + "visible_at_first_publication": 1 + }, + "lifecycle_status": "settled" + }, + { + "fixture": { + "kind": "mcp", + "size": 10, + "concurrency": 10, + "payload_bytes": null, + "composition": "deterministic fake server inventory publication" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "startup_to_ready": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1779089000, + 1784954208, + 1829121792, + 1821090416, + 1850754291 + ], + "median_ns": 1821090416, + "p95_ns": 1850754291, + "throughput_per_second": 5.491215544346701, + "within_run_samples_ns": [] + }, + "mcp_lifecycle": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 66038291, + 66391208, + 66181875, + 65971875, + 65271250 + ], + "median_ns": 66038291, + "p95_ns": 66391208, + "throughput_per_second": 151.42729844417082, + "within_run_samples_ns": [] + }, + "time_to_first_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 66000750, + 66352000, + 66149917, + 65945542, + 65235125 + ], + "median_ns": 66000750, + "p95_ns": 66352000, + "throughput_per_second": 151.5134297716314, + "within_run_samples_ns": [] + }, + "time_to_settled_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 66038291, + 66391208, + 66181875, + 65971875, + 65271250 + ], + "median_ns": 66038291, + "p95_ns": 66391208, + "throughput_per_second": 151.42729844417082, + "within_run_samples_ns": [] + }, + "cleanup": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 319208, + 338292, + 286833, + 286917, + 285958 + ], + "median_ns": 286917, + "p95_ns": 338292, + "throughput_per_second": 34853.28509638676, + "within_run_samples_ns": [] + } + }, + "registry_hash": "6ec6fba74b1a57a8542826141c146955b0860dfed1633019e5b384cd43c83a64", + "allocation_peak_bytes": 16888805, + "retained_object_delta": 11764932, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 50, + "category_counts": { + "mcp": 10 + }, + "projection_counts": { + "visible": 10, + "visible_at_first_publication": 10 + }, + "lifecycle_status": "settled" + }, + { + "fixture": { + "kind": "mcp", + "size": 50, + "concurrency": 50, + "payload_bytes": null, + "composition": "deterministic fake server inventory publication" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "startup_to_ready": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2038332458, + 2035720125, + 2220231916, + 2346351458, + 2365520875 + ], + "median_ns": 2220231916, + "p95_ns": 2365520875, + "throughput_per_second": 22.52016991543869, + "within_run_samples_ns": [] + }, + "mcp_lifecycle": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 314626958, + 311856833, + 391118250, + 389559750, + 386309083 + ], + "median_ns": 386309083, + "p95_ns": 391118250, + "throughput_per_second": 129.43003983160293, + "within_run_samples_ns": [] + }, + "time_to_first_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 314573458, + 311816042, + 391067625, + 389511167, + 386258708 + ], + "median_ns": 386258708, + "p95_ns": 391067625, + "throughput_per_second": 129.44691980898978, + "within_run_samples_ns": [] + }, + "time_to_settled_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 314626958, + 311856833, + 391118250, + 389559750, + 386309083 + ], + "median_ns": 386309083, + "p95_ns": 391118250, + "throughput_per_second": 129.43003983160293, + "within_run_samples_ns": [] + }, + "cleanup": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1294292, + 969542, + 1172333, + 1062959, + 1072084 + ], + "median_ns": 1072084, + "p95_ns": 1294292, + "throughput_per_second": 46638.13656392596, + "within_run_samples_ns": [] + } + }, + "registry_hash": "0e78cd556e01ea5799fec4eb49b30e74f35299ddfebc6a0cd5e9d56358792951", + "allocation_peak_bytes": 13323991, + "retained_object_delta": 13169256, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 250, + "category_counts": { + "mcp": 50 + }, + "projection_counts": { + "visible": 50, + "visible_at_first_publication": 50 + }, + "lifecycle_status": "settled" + } + ], + "decisions": [ + { + "name": "execution_framework_overhead_percent_short_safe_size_1", + "threshold": 10.0, + "values": [ + 99.28446477870698, + 99.52843275025496, + 99.49939541435207, + 99.23096992351853, + 99.53925526240155 + ], + "rerun_values": null, + "crossing_count": 5, + "median": 99.49939541435207, + "primary_state": "crossed", + "rerun_state": null, + "state": "crossed", + "rerun_required": false + }, + { + "name": "mcp_lifecycle_startup_percent_10_servers", + "threshold": 20.0, + "values": [ + 3.7119160986325026, + 3.7194908251674317, + 3.6182322735128185, + 3.6226578548969752, + 3.5267377370084403 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 3.6226578548969752, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "mcp_cleanup_seconds_1_servers", + "threshold": 6.0, + "values": [ + 0.000149083, + 0.00012875, + 0.000131667, + 0.000136625, + 0.000146417 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 0.000136625, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "mcp_cleanup_seconds_10_servers", + "threshold": 6.0, + "values": [ + 0.000319208, + 0.000338292, + 0.000286833, + 0.000286917, + 0.000285958 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 0.000286917, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "mcp_cleanup_seconds_50_servers", + "threshold": 6.0, + "values": [ + 0.001294292, + 0.000969542, + 0.001172333, + 0.001062959, + 0.001072084 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 0.001072084, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "registry_projection_p95_ms_500_tools", + "threshold": 5.0, + "values": [ + 3.320125, + 3.142459, + 3.247792, + 3.199125, + 3.311208 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 3.247792, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "mixed_gate_wait_end_to_end_percent_10_pairs", + "threshold": 25.0, + "values": [ + 89.35606849448976, + 89.74481948843007, + 89.488563846407, + 89.98620636604295, + 89.02158934313276 + ], + "rerun_values": null, + "crossing_count": 5, + "median": 89.488563846407, + "primary_state": "crossed", + "rerun_state": null, + "state": "crossed", + "rerun_required": false + } + ] +} diff --git a/docs/superpowers/reports/2026-07-15-tool-execution-before.json b/docs/superpowers/reports/2026-07-15-tool-execution-before.json new file mode 100644 index 00000000..8b727b82 --- /dev/null +++ b/docs/superpowers/reports/2026-07-15-tool-execution-before.json @@ -0,0 +1,3177 @@ +{ + "schema_version": 2, + "environment": { + "python_version": "3.14.6", + "python_implementation": "CPython", + "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O" + }, + "scenarios": [ + { + "fixture": { + "kind": "execution_safe", + "size": 1, + "concurrency": 1, + "payload_bytes": null, + "composition": "safe" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1526291, + 1487209, + 1484000, + 1463083, + 1452291 + ], + "median_ns": 1484000, + "p95_ns": 1526291, + "throughput_per_second": 673.8544474393531, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 12833, + 8667, + 8584, + 6208, + 5292 + ], + "median_ns": 8584, + "p95_ns": 12833, + "throughput_per_second": 116495.80615097856, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 7542, + 7375, + 7417, + 6292, + 5500 + ], + "median_ns": 7375, + "p95_ns": 7542, + "throughput_per_second": 135593.22033898305, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1518749, + 1479834, + 1476583, + 1456791, + 1446791 + ], + "median_ns": 1476583, + "p95_ns": 1518749, + "throughput_per_second": 677.2392747309159, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 46240, + "retained_object_delta": 23319, + "cancellation": { + "completed": true, + "completion_ns": 7468667, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_safe", + "size": 10, + "concurrency": 10, + "payload_bytes": null, + "composition": "safe" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 13702041, + 13796333, + 14075459, + 14030459, + 14134583 + ], + "median_ns": 14030459, + "p95_ns": 14134583, + "throughput_per_second": 712.7350573491573, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 54999, + 52541, + 53832, + 54377, + 50167 + ], + "median_ns": 53832, + "p95_ns": 54999, + "throughput_per_second": 185763.11487591025, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 53709, + 50877, + 55207, + 55960, + 49542 + ], + "median_ns": 53709, + "p95_ns": 55960, + "throughput_per_second": 186188.53451004488, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 13648332, + 13745456, + 14020252, + 13974499, + 14085041 + ], + "median_ns": 13974499, + "p95_ns": 14085041, + "throughput_per_second": 715.589159940546, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 126298, + "retained_object_delta": 72078, + "cancellation": { + "completed": true, + "completion_ns": 7543625, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 10, + "operation_count": 50, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_safe", + "size": 100, + "concurrency": 100, + "payload_bytes": null, + "composition": "safe" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 138307250, + 140739416, + 140765541, + 142424458, + 140010583 + ], + "median_ns": 140739416, + "p95_ns": 142424458, + "throughput_per_second": 710.5330037748629, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 488915, + 491914, + 485499, + 504250, + 496756 + ], + "median_ns": 491914, + "p95_ns": 504250, + "throughput_per_second": 203287.56652585615, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 487457, + 491338, + 507875, + 491333, + 477665 + ], + "median_ns": 491333, + "p95_ns": 507875, + "throughput_per_second": 203527.95354677987, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 137819793, + 140248078, + 140257666, + 141933125, + 139532918 + ], + "median_ns": 140248078, + "p95_ns": 141933125, + "throughput_per_second": 713.0222490464362, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 995124, + "retained_object_delta": 557756, + "cancellation": { + "completed": true, + "completion_ns": 7543500, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 100, + "operation_count": 500, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_exclusive", + "size": 1, + "concurrency": 1, + "payload_bytes": null, + "composition": "exclusive" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1450417, + 1460166, + 1457084, + 1470375, + 1462792 + ], + "median_ns": 1460166, + "p95_ns": 1470375, + "throughput_per_second": 684.8536399286108, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 4375, + 4250, + 3792, + 3625, + 3875 + ], + "median_ns": 3875, + "p95_ns": 4375, + "throughput_per_second": 258064.51612903227, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 5917, + 5583, + 5250, + 5375, + 5417 + ], + "median_ns": 5417, + "p95_ns": 5917, + "throughput_per_second": 184604.0243677312, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1444500, + 1454583, + 1451834, + 1465000, + 1457375 + ], + "median_ns": 1454583, + "p95_ns": 1465000, + "throughput_per_second": 687.4822543643093, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 35960, + "retained_object_delta": 13013, + "cancellation": { + "completed": true, + "completion_ns": 7508000, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_exclusive", + "size": 10, + "concurrency": 10, + "payload_bytes": null, + "composition": "exclusive" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 14090833, + 13882084, + 13820250, + 13685375, + 14155583 + ], + "median_ns": 13882084, + "p95_ns": 14155583, + "throughput_per_second": 720.3529383628568, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 38164, + 36251, + 36874, + 36915, + 38208 + ], + "median_ns": 36915, + "p95_ns": 38208, + "throughput_per_second": 270892.59108763374, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 50460, + 48085, + 49708, + 48207, + 49957 + ], + "median_ns": 49708, + "p95_ns": 50460, + "throughput_per_second": 201174.86118934577, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 14040373, + 13833999, + 13770542, + 13637168, + 14105626 + ], + "median_ns": 13833999, + "p95_ns": 14105626, + "throughput_per_second": 722.8567820483433, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 116964, + "retained_object_delta": 63262, + "cancellation": { + "completed": true, + "completion_ns": 7186125, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 10, + "operation_count": 50, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_exclusive", + "size": 100, + "concurrency": 100, + "payload_bytes": null, + "composition": "exclusive" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 137456542, + 138724458, + 138688500, + 138603875, + 136136333 + ], + "median_ns": 138603875, + "p95_ns": 138724458, + "throughput_per_second": 721.4805502371416, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 360038, + 356825, + 355999, + 378710, + 348752 + ], + "median_ns": 356825, + "p95_ns": 378710, + "throughput_per_second": 280249.42198556714, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 467083, + 471585, + 471242, + 518256, + 481167 + ], + "median_ns": 471585, + "p95_ns": 518256, + "throughput_per_second": 212050.84979378054, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 136989459, + 138252873, + 138217258, + 138085619, + 135655166 + ], + "median_ns": 138085619, + "p95_ns": 138252873, + "throughput_per_second": 724.1883747503061, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 944555, + "retained_object_delta": 531669, + "cancellation": { + "completed": true, + "completion_ns": 7266583, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 100, + "operation_count": 500, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_mixed", + "size": 1, + "concurrency": 2, + "payload_bytes": null, + "composition": "reader/writer pairs" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2840583, + 2893042, + 2866375, + 2797167, + 2880292 + ], + "median_ns": 2866375, + "p95_ns": 2893042, + "throughput_per_second": 697.7454101434739, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 153167, + 151166, + 150750, + 130124, + 124292 + ], + "median_ns": 150750, + "p95_ns": 153167, + "throughput_per_second": 13266.998341625207, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 214792, + 211917, + 203001, + 179709, + 173500 + ], + "median_ns": 203001, + "p95_ns": 214792, + "throughput_per_second": 9852.16821592012, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2625791, + 2681125, + 2663374, + 2617458, + 2706792 + ], + "median_ns": 2663374, + "p95_ns": 2706792, + "throughput_per_second": 750.9272073692993, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "8c718a5dc30b54eeabc4aba7d1a5910c7d3b727e3547c8f82451d70bb73c7ba6", + "allocation_peak_bytes": 46283, + "retained_object_delta": 18465, + "cancellation": { + "completed": true, + "completion_ns": 7344750, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 2, + "operation_count": 10, + "category_counts": { + "builtin": 2 + }, + "projection_counts": { + "visible": 2 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_mixed", + "size": 10, + "concurrency": 20, + "payload_bytes": null, + "composition": "reader/writer pairs" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 26570959, + 26555667, + 27220458, + 27181917, + 28015500 + ], + "median_ns": 27181917, + "p95_ns": 28015500, + "throughput_per_second": 735.7832782728311, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 23852167, + 23836416, + 24405250, + 24385208, + 25153208 + ], + "median_ns": 24385208, + "p95_ns": 25153208, + "throughput_per_second": 820.1693420043823, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1380083, + 1357879, + 1370043, + 1405250, + 1415079 + ], + "median_ns": 1380083, + "p95_ns": 1415079, + "throughput_per_second": 14491.882009995052, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 25190876, + 25197788, + 25850415, + 25776667, + 26600421 + ], + "median_ns": 25776667, + "p95_ns": 26600421, + "throughput_per_second": 775.895502704054, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "8c718a5dc30b54eeabc4aba7d1a5910c7d3b727e3547c8f82451d70bb73c7ba6", + "allocation_peak_bytes": 207439, + "retained_object_delta": 102052, + "cancellation": { + "completed": true, + "completion_ns": 6978833, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 20, + "operation_count": 100, + "category_counts": { + "builtin": 2 + }, + "projection_counts": { + "visible": 2 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "execution_mixed", + "size": 100, + "concurrency": 200, + "payload_bytes": null, + "composition": "reader/writer pairs" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 285567708, + 296809125, + 286243625, + 282915167, + 285880458 + ], + "median_ns": 285880458, + "p95_ns": 296809125, + "throughput_per_second": 699.5931145458009, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 282590584, + 293620459, + 283111417, + 279883541, + 282825125 + ], + "median_ns": 282825125, + "p95_ns": 293620459, + "throughput_per_second": 707.1507526072869, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 13612582, + 15122739, + 13771673, + 13783700, + 13531714 + ], + "median_ns": 13771673, + "p95_ns": 15122739, + "throughput_per_second": 14522.563816320646, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 271955126, + 281686386, + 272471952, + 269131467, + 272348744 + ], + "median_ns": 272348744, + "p95_ns": 281686386, + "throughput_per_second": 734.3525696597301, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "8c718a5dc30b54eeabc4aba7d1a5910c7d3b727e3547c8f82451d70bb73c7ba6", + "allocation_peak_bytes": 1916270, + "retained_object_delta": 981343, + "cancellation": { + "completed": true, + "completion_ns": 7680750, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 200, + "operation_count": 1000, + "category_counts": { + "builtin": 2 + }, + "projection_counts": { + "visible": 2 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "dedupe", + "size": 1024, + "concurrency": 2, + "payload_bytes": 1024, + "composition": "same-step duplicate payload" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1535708, + 1485791, + 1515084, + 1516584, + 1549041 + ], + "median_ns": 1516584, + "p95_ns": 1549041, + "throughput_per_second": 659.3765989882526, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 7208, + 5625, + 5708, + 5500, + 5250 + ], + "median_ns": 5625, + "p95_ns": 7208, + "throughput_per_second": 177777.77777777778, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 6125, + 5792, + 5375, + 5083, + 5041 + ], + "median_ns": 5375, + "p95_ns": 6125, + "throughput_per_second": 186046.51162790696, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1529583, + 1479999, + 1509709, + 1511501, + 1544000 + ], + "median_ns": 1511501, + "p95_ns": 1544000, + "throughput_per_second": 661.5940048997652, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 45775, + "retained_object_delta": 17012, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 2, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "dedupe", + "size": 102400, + "concurrency": 2, + "payload_bytes": 102400, + "composition": "same-step duplicate payload" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1884208, + 1896708, + 1984791, + 1926083, + 1984959 + ], + "median_ns": 1926083, + "p95_ns": 1984959, + "throughput_per_second": 519.1884254209191, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 5458, + 5333, + 7917, + 6875, + 5583 + ], + "median_ns": 5583, + "p95_ns": 7917, + "throughput_per_second": 179115.17105498837, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 5334, + 5375, + 7292, + 6292, + 5625 + ], + "median_ns": 5625, + "p95_ns": 7292, + "throughput_per_second": 177777.77777777778, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1878874, + 1891333, + 1977499, + 1919791, + 1979334 + ], + "median_ns": 1919791, + "p95_ns": 1979334, + "throughput_per_second": 520.8900343839512, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 869202, + "retained_object_delta": 16475, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 2, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "dedupe", + "size": 1048576, + "concurrency": 2, + "payload_bytes": 1048576, + "composition": "same-step duplicate payload" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "end_to_end": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 5878750, + 5783542, + 6220833, + 5802792, + 6150000 + ], + "median_ns": 5878750, + "p95_ns": 6220833, + "throughput_per_second": 170.1041888156496, + "within_run_samples_ns": [] + }, + "read_write_gate_wait": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 7375, + 6792, + 7750, + 8000, + 7250 + ], + "median_ns": 7375, + "p95_ns": 8000, + "throughput_per_second": 135593.22033898305, + "within_run_samples_ns": [] + }, + "tool_call": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 6125, + 6083, + 7208, + 6750, + 7750 + ], + "median_ns": 6750, + "p95_ns": 7750, + "throughput_per_second": 148148.14814814815, + "within_run_samples_ns": [] + }, + "framework_overhead": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 5872625, + 5777459, + 6213625, + 5796042, + 6142250 + ], + "median_ns": 5872625, + "p95_ns": 6213625, + "throughput_per_second": 170.28160320129413, + "within_run_samples_ns": [] + }, + "lookup_suggestion": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "json_parse_canonicalize": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "deduplication": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "permission_approval": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "pre_hook": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "post_hook_reminder": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + }, + "telemetry_wire": { + "measurement_status": "unmeasured", + "reason": "current Toolset exposes no stable boundary for this subphase", + "samples_ns": [], + "median_ns": null, + "p95_ns": null, + "throughput_per_second": null, + "within_run_samples_ns": [] + } + }, + "registry_hash": "c739117bf452f76c6574d251f8e498ab5fb71099c5a47b4ae7cd27e71bf315a2", + "allocation_peak_bytes": 8674621, + "retained_object_delta": 15859, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 2, + "operation_count": 5, + "category_counts": { + "builtin": 1 + }, + "projection_counts": { + "visible": 1 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "advertisement", + "size": 50, + "concurrency": 1, + "payload_bytes": null, + "composition": "builtin/plugin/MCP-style names with hidden entries" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "visibility_enabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 286500, + 275833, + 287000, + 285667, + 297208 + ], + "median_ns": 286500, + "p95_ns": 297208, + "throughput_per_second": 174520.0698080279, + "within_run_samples_ns": [] + }, + "visibility_enabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 284000, + 284542, + 306584, + 286292, + 317750 + ], + "median_ns": 286292, + "p95_ns": 317750, + "throughput_per_second": 174646.86404090928, + "within_run_samples_ns": [] + }, + "visibility_disabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 6584, + 7375, + 6792, + 6334, + 6917 + ], + "median_ns": 6792, + "p95_ns": 7375, + "throughput_per_second": 7361601.884570083, + "within_run_samples_ns": [] + }, + "visibility_disabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 5291, + 5542, + 5458, + 5167, + 5791 + ], + "median_ns": 5458, + "p95_ns": 5791, + "throughput_per_second": 9160864.785635764, + "within_run_samples_ns": [] + }, + "repeated_unchanged_projection": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1140666, + 1132375, + 1124875, + 1163083, + 1249417 + ], + "median_ns": 1140666, + "p95_ns": 1249417, + "throughput_per_second": 43834.04081475209, + "within_run_samples_ns": [] + }, + "registry_projection_p95": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 298625, + 286417, + 297584, + 297417, + 318417 + ], + "median_ns": 297584, + "p95_ns": 318417, + "throughput_per_second": 168019.78601000053, + "within_run_samples_ns": [ + [ + 291083, + 286625, + 287500, + 287750, + 287583, + 295167, + 285125, + 287709, + 288500, + 291625, + 295834, + 298625, + 297917, + 322917, + 289250, + 282708, + 276875, + 277416, + 279917, + 276625 + ], + [ + 284250, + 286417, + 284166, + 282958, + 283750, + 284750, + 321459, + 278250, + 272500, + 271208, + 271500, + 271209, + 270500, + 270125, + 270750, + 270125, + 270709, + 270625, + 273709, + 271000 + ], + [ + 290583, + 293834, + 287167, + 291959, + 291000, + 289416, + 289375, + 295750, + 291833, + 297584, + 290000, + 294167, + 294291, + 326000, + 286542, + 279375, + 272833, + 277958, + 272584, + 279667 + ], + [ + 289959, + 287792, + 291417, + 291000, + 286750, + 287875, + 286875, + 289500, + 289792, + 291000, + 297417, + 317625, + 285500, + 274334, + 272167, + 274167, + 281334, + 275208, + 272708, + 277084 + ], + [ + 334166, + 303750, + 308708, + 306667, + 307792, + 306500, + 308583, + 305833, + 308833, + 303583, + 315250, + 304208, + 309125, + 318417, + 312458, + 315042, + 308375, + 304125, + 309667, + 304292 + ] + ] + }, + "rebuild_after_mcp_publication": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 56458, + 45959, + 49833, + 50959, + 53084 + ], + "median_ns": 50959, + "p95_ns": 56458, + "throughput_per_second": 981180.9493906866, + "within_run_samples_ns": [] + } + }, + "registry_hash": "91398880bf75436e99f298a4b5b639ab91a13d6bb2a211a3fa510015f449fa95", + "allocation_peak_bytes": 6574978, + "retained_object_delta": 5835954, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 0, + "operation_count": 250, + "category_counts": { + "builtin": 17, + "plugin": 17, + "mcp": 16 + }, + "projection_counts": { + "enabled_hidden": 45, + "enabled_unhidden": 50, + "disabled_hidden": 45, + "disabled_unhidden": 50, + "rebuild": 50, + "repeated": 50 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "advertisement", + "size": 500, + "concurrency": 1, + "payload_bytes": null, + "composition": "builtin/plugin/MCP-style names with hidden entries" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "visibility_enabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2559333, + 2638042, + 2725750, + 2790500, + 2968750 + ], + "median_ns": 2725750, + "p95_ns": 2968750, + "throughput_per_second": 183435.7516279923, + "within_run_samples_ns": [] + }, + "visibility_enabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2780708, + 2669375, + 3019667, + 3002917, + 3225000 + ], + "median_ns": 3002917, + "p95_ns": 3225000, + "throughput_per_second": 166504.76853006592, + "within_run_samples_ns": [] + }, + "visibility_disabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 44791, + 42834, + 44708, + 43458, + 67334 + ], + "median_ns": 44708, + "p95_ns": 67334, + "throughput_per_second": 11183680.773016015, + "within_run_samples_ns": [] + }, + "visibility_disabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 43458, + 41834, + 45125, + 44250, + 46041 + ], + "median_ns": 44250, + "p95_ns": 46041, + "throughput_per_second": 11299435.028248588, + "within_run_samples_ns": [] + }, + "repeated_unchanged_projection": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 11304375, + 11213833, + 12185166, + 12161541, + 12150125 + ], + "median_ns": 12150125, + "p95_ns": 12185166, + "throughput_per_second": 41151.84000164607, + "within_run_samples_ns": [] + }, + "registry_projection_p95": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2932625, + 2949875, + 3128791, + 3172083, + 3138791 + ], + "median_ns": 3128791, + "p95_ns": 3172083, + "throughput_per_second": 159806.13598031955, + "within_run_samples_ns": [ + [ + 2825584, + 2791625, + 2752375, + 2819917, + 2854208, + 2725666, + 2791625, + 2925291, + 2847500, + 2850750, + 2932625, + 2938541, + 2717750, + 2829417, + 2860875, + 2770500, + 2773250, + 2831416, + 2894875, + 2687709 + ], + [ + 2876209, + 2961375, + 2798542, + 2849125, + 2936916, + 2856917, + 2758417, + 2890834, + 2898958, + 2738291, + 2842000, + 2854209, + 2836666, + 2801000, + 2778000, + 2862000, + 2849708, + 2862958, + 2934041, + 2949875 + ], + [ + 3111000, + 3151708, + 3081417, + 3081541, + 3074792, + 3068125, + 3079750, + 3038500, + 3039375, + 3064500, + 3061583, + 3055416, + 3071166, + 3052834, + 3036333, + 3035208, + 3107041, + 3128791, + 3099208, + 3108792 + ], + [ + 3204125, + 2990375, + 3119375, + 3124708, + 3027500, + 3039917, + 3089625, + 3073500, + 3022542, + 3094833, + 3172083, + 3111000, + 3146292, + 3024708, + 3075750, + 3069333, + 2993125, + 2976875, + 3004625, + 2964208 + ], + [ + 3095167, + 3138791, + 3047125, + 2982167, + 3023417, + 3120791, + 3046042, + 3155500, + 3104042, + 2925000, + 2948041, + 3041750, + 2993917, + 2961250, + 2958000, + 3052416, + 2949541, + 3077042, + 3091208, + 2953792 + ] + ] + }, + "rebuild_after_mcp_publication": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 415791, + 381458, + 393750, + 446667, + 408083 + ], + "median_ns": 408083, + "p95_ns": 446667, + "throughput_per_second": 1225240.9436315652, + "within_run_samples_ns": [] + } + }, + "registry_hash": "fd65db87d53a9d0420eae828c8caa4d54b5ead8fb090f7ae2878371ca84108d4", + "allocation_peak_bytes": 11239230, + "retained_object_delta": 6176287, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 0, + "operation_count": 2500, + "category_counts": { + "builtin": 167, + "plugin": 167, + "mcp": 166 + }, + "projection_counts": { + "enabled_hidden": 450, + "enabled_unhidden": 500, + "disabled_hidden": 450, + "disabled_unhidden": 500, + "rebuild": 500, + "repeated": 500 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "advertisement", + "size": 5000, + "concurrency": 1, + "payload_bytes": null, + "composition": "builtin/plugin/MCP-style names with hidden entries" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "visibility_enabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 28031958, + 28205292, + 28847500, + 31373375, + 30476458 + ], + "median_ns": 28847500, + "p95_ns": 31373375, + "throughput_per_second": 173325.2448219083, + "within_run_samples_ns": [] + }, + "visibility_enabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 31255459, + 31582208, + 31714250, + 34058875, + 34510750 + ], + "median_ns": 31714250, + "p95_ns": 34510750, + "throughput_per_second": 157657.83520026488, + "within_run_samples_ns": [] + }, + "visibility_disabled_hidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 577709, + 699542, + 548709, + 692708, + 1086666 + ], + "median_ns": 692708, + "p95_ns": 1086666, + "throughput_per_second": 7218048.586128643, + "within_run_samples_ns": [] + }, + "visibility_disabled_unhidden": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 459709, + 469083, + 419375, + 472875, + 734875 + ], + "median_ns": 469083, + "p95_ns": 734875, + "throughput_per_second": 10659094.445972249, + "within_run_samples_ns": [] + }, + "repeated_unchanged_projection": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 122283541, + 124810875, + 123635291, + 133679041, + 132310250 + ], + "median_ns": 124810875, + "p95_ns": 133679041, + "throughput_per_second": 40060.61170551044, + "within_run_samples_ns": [] + }, + "registry_projection_p95": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 32233042, + 31767416, + 31563583, + 35000500, + 33366416 + ], + "median_ns": 32233042, + "p95_ns": 35000500, + "throughput_per_second": 155120.3265270464, + "within_run_samples_ns": [ + [ + 30843917, + 30271541, + 30318708, + 30829625, + 29914541, + 30608042, + 30319458, + 30001833, + 30525500, + 30343667, + 30147709, + 30573458, + 30414334, + 30025125, + 30410542, + 30456083, + 32717333, + 32233042, + 31673416, + 32097292 + ], + [ + 31257833, + 30528167, + 30722208, + 31149459, + 30660208, + 30664125, + 31104833, + 30459875, + 31105291, + 31899833, + 31224292, + 31028791, + 31130625, + 30975958, + 30809916, + 30737209, + 31767416, + 31146667, + 31064750, + 30551125 + ], + [ + 30815167, + 30942084, + 30354208, + 31027542, + 30880166, + 30347417, + 31563583, + 30431167, + 31167667, + 30490583, + 30705291, + 30633291, + 30701208, + 30656417, + 31840000, + 30154833, + 30870417, + 30687375, + 30471583, + 30955541 + ], + [ + 34628209, + 33479500, + 32939333, + 32195041, + 32645541, + 32821916, + 32606333, + 32748791, + 32218917, + 42639542, + 33055333, + 35000500, + 34429167, + 32881917, + 33168209, + 32750084, + 33168125, + 33324000, + 33051959, + 33617958 + ], + [ + 33060167, + 33101375, + 33930917, + 33366416, + 32892708, + 32829833, + 32728542, + 32595917, + 32966833, + 32818042, + 32752584, + 32812084, + 32882083, + 32513917, + 32825084, + 33093334, + 33157959, + 33246167, + 32876583, + 32956500 + ] + ] + }, + "rebuild_after_mcp_publication": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 3643250, + 3744334, + 3701750, + 5440625, + 4034083 + ], + "median_ns": 3744334, + "p95_ns": 5440625, + "throughput_per_second": 1335350.9596099067, + "within_run_samples_ns": [] + } + }, + "registry_hash": "875959264842f6837daa22baebc202ca0055c523e46ad47dcd72b690337a549d", + "allocation_peak_bytes": 47176192, + "retained_object_delta": 9646821, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 0, + "operation_count": 25000, + "category_counts": { + "builtin": 1667, + "plugin": 1667, + "mcp": 1666 + }, + "projection_counts": { + "enabled_hidden": 4500, + "enabled_unhidden": 5000, + "disabled_hidden": 4500, + "disabled_unhidden": 5000, + "rebuild": 5000, + "repeated": 5000 + }, + "lifecycle_status": "completed" + }, + { + "fixture": { + "kind": "mcp", + "size": 1, + "concurrency": 1, + "payload_bytes": null, + "composition": "deterministic fake server inventory publication" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "startup_to_ready": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1808621542, + 1827940167, + 1893547417, + 1845301125, + 1822315500 + ], + "median_ns": 1827940167, + "p95_ns": 1893547417, + "throughput_per_second": 0.5470638580261582, + "within_run_samples_ns": [] + }, + "mcp_lifecycle": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 7716125, + 7398000, + 7398042, + 7672166, + 7213500 + ], + "median_ns": 7398042, + "p95_ns": 7716125, + "throughput_per_second": 135.17090062478692, + "within_run_samples_ns": [] + }, + "time_to_first_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 7690375, + 7373542, + 7376084, + 7643416, + 7189375 + ], + "median_ns": 7376084, + "p95_ns": 7690375, + "throughput_per_second": 135.57329336271115, + "within_run_samples_ns": [] + }, + "time_to_settled_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 7716125, + 7398000, + 7398042, + 7672166, + 7213500 + ], + "median_ns": 7398042, + "p95_ns": 7716125, + "throughput_per_second": 135.17090062478692, + "within_run_samples_ns": [] + }, + "cleanup": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 157833, + 120917, + 117125, + 135333, + 154667 + ], + "median_ns": 135333, + "p95_ns": 157833, + "throughput_per_second": 7389.180761528969, + "within_run_samples_ns": [] + } + }, + "registry_hash": "dbb4989c3be2949f3beb5de69201d9a11de843d0cdb7622ed4cdee85e624c7af", + "allocation_peak_bytes": 6495823, + "retained_object_delta": 5786678, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 5, + "category_counts": { + "mcp": 1 + }, + "projection_counts": { + "visible": 1, + "visible_at_first_publication": 1 + }, + "lifecycle_status": "settled" + }, + { + "fixture": { + "kind": "mcp", + "size": 10, + "concurrency": 10, + "payload_bytes": null, + "composition": "deterministic fake server inventory publication" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "startup_to_ready": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1862035667, + 1887324000, + 1942169166, + 1894862875, + 1919955125 + ], + "median_ns": 1894862875, + "p95_ns": 1942169166, + "throughput_per_second": 5.277426737277757, + "within_run_samples_ns": [] + }, + "mcp_lifecycle": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 70314375, + 65988167, + 66929125, + 67292584, + 67562708 + ], + "median_ns": 67292584, + "p95_ns": 70314375, + "throughput_per_second": 148.60478533563224, + "within_run_samples_ns": [] + }, + "time_to_first_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 70279458, + 65949792, + 66888625, + 67257209, + 67526333 + ], + "median_ns": 67257209, + "p95_ns": 70279458, + "throughput_per_second": 148.68294638869122, + "within_run_samples_ns": [] + }, + "time_to_settled_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 70314375, + 65988167, + 66929125, + 67292584, + 67562708 + ], + "median_ns": 67292584, + "p95_ns": 70314375, + "throughput_per_second": 148.60478533563224, + "within_run_samples_ns": [] + }, + "cleanup": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 311416, + 311000, + 343875, + 306042, + 306417 + ], + "median_ns": 311000, + "p95_ns": 343875, + "throughput_per_second": 32154.340836012863, + "within_run_samples_ns": [] + } + }, + "registry_hash": "6ec6fba74b1a57a8542826141c146955b0860dfed1633019e5b384cd43c83a64", + "allocation_peak_bytes": 11020106, + "retained_object_delta": 5958601, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 50, + "category_counts": { + "mcp": 10 + }, + "projection_counts": { + "visible": 10, + "visible_at_first_publication": 10 + }, + "lifecycle_status": "settled" + }, + { + "fixture": { + "kind": "mcp", + "size": 50, + "concurrency": 50, + "payload_bytes": null, + "composition": "deterministic fake server inventory publication" + }, + "warmups": 1, + "iterations": 5, + "phases": { + "startup_to_ready": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 2139653292, + 2167554084, + 2118934625, + 2214617833, + 2404288583 + ], + "median_ns": 2167554084, + "p95_ns": 2404288583, + "throughput_per_second": 23.067475164324435, + "within_run_samples_ns": [] + }, + "mcp_lifecycle": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 328712250, + 326005500, + 330230333, + 404207167, + 406366208 + ], + "median_ns": 330230333, + "p95_ns": 406366208, + "throughput_per_second": 151.4094709161681, + "within_run_samples_ns": [] + }, + "time_to_first_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 328659375, + 325956875, + 330177291, + 404158334, + 406320500 + ], + "median_ns": 330177291, + "p95_ns": 406320500, + "throughput_per_second": 151.43379439744692, + "within_run_samples_ns": [] + }, + "time_to_settled_inventory": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 328712250, + 326005500, + 330230333, + 404207167, + 406366208 + ], + "median_ns": 330230333, + "p95_ns": 406366208, + "throughput_per_second": 151.4094709161681, + "within_run_samples_ns": [] + }, + "cleanup": { + "measurement_status": "measured", + "reason": null, + "samples_ns": [ + 1016958, + 1001417, + 1047084, + 1064459, + 1146041 + ], + "median_ns": 1047084, + "p95_ns": 1146041, + "throughput_per_second": 47751.66080276272, + "within_run_samples_ns": [] + } + }, + "registry_hash": "0e78cd556e01ea5799fec4eb49b30e74f35299ddfebc6a0cd5e9d56358792951", + "allocation_peak_bytes": 12145143, + "retained_object_delta": 6651320, + "cancellation": { + "completed": true, + "completion_ns": 0, + "queued_reader_completed": true, + "queued_writer_completed": true, + "recovery_completed": true + }, + "leaks": { + "tasks": 0, + "processes": 0, + "sessions": 0 + }, + "task_count_peak": 1, + "operation_count": 250, + "category_counts": { + "mcp": 50 + }, + "projection_counts": { + "visible": 50, + "visible_at_first_publication": 50 + }, + "lifecycle_status": "settled" + } + ], + "decisions": [ + { + "name": "execution_framework_overhead_percent_short_safe_size_1", + "threshold": 10.0, + "values": [ + 99.50586094001733, + 99.50410466854356, + 99.50020215633423, + 99.56994920999014, + 99.62128802010065 + ], + "rerun_values": null, + "crossing_count": 5, + "median": 99.50586094001733, + "primary_state": "crossed", + "rerun_state": null, + "state": "crossed", + "rerun_required": false + }, + { + "name": "mcp_lifecycle_startup_percent_10_servers", + "threshold": 20.0, + "values": [ + 3.776209889324317, + 3.4963878486152877, + 3.4461017182063536, + 3.551316820221094, + 3.5189732885032923 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 3.5189732885032923, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "mcp_cleanup_seconds_1_servers", + "threshold": 6.0, + "values": [ + 0.000157833, + 0.000120917, + 0.000117125, + 0.000135333, + 0.000154667 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 0.000135333, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "mcp_cleanup_seconds_10_servers", + "threshold": 6.0, + "values": [ + 0.000311416, + 0.000311, + 0.000343875, + 0.000306042, + 0.000306417 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 0.000311, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "mcp_cleanup_seconds_50_servers", + "threshold": 6.0, + "values": [ + 0.001016958, + 0.001001417, + 0.001047084, + 0.001064459, + 0.001146041 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 0.001047084, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "registry_projection_p95_ms_500_tools", + "threshold": 5.0, + "values": [ + 2.932625, + 2.949875, + 3.128791, + 3.172083, + 3.138791 + ], + "rerun_values": null, + "crossing_count": 0, + "median": 3.128791, + "primary_state": "uncrossed", + "rerun_state": null, + "state": "uncrossed", + "rerun_required": false + }, + { + "name": "mixed_gate_wait_end_to_end_percent_10_pairs", + "threshold": 25.0, + "values": [ + 89.76780627300656, + 89.760185650769, + 89.65774932956676, + 89.71114141802434, + 89.78318430868626 + ], + "rerun_values": null, + "crossing_count": 5, + "median": 89.760185650769, + "primary_state": "crossed", + "rerun_state": null, + "state": "crossed", + "rerun_required": false + } + ] +} diff --git a/docs/superpowers/reports/2026-07-15-tool-execution-engine-characterization.md b/docs/superpowers/reports/2026-07-15-tool-execution-engine-characterization.md new file mode 100644 index 00000000..3315c22b --- /dev/null +++ b/docs/superpowers/reports/2026-07-15-tool-execution-engine-characterization.md @@ -0,0 +1,104 @@ +# Tool-execution engine before/after characterization + +## Decision + +**GO for the behavior-preserving extraction, with an explicit local overhead finding.** + +The private execution engine preserved every deterministic safety outcome, every threshold decision, +and the `PythinkerToolset` registry/MCP facade. It did not improve local timing. Median execution +fixtures were 3.6–14.5% slower, deduplication fixtures were 19.4–20.7% slower, and registry projection +was 3.8–4.2% slower in this sequential local run. These are absolute increases of roughly 0.05–26 ms +for execution fixtures and 0.30–1.22 ms for deduplication fixtures. No performance improvement is +claimed. + +The extraction ships for the approved ownership and cancellation guarantees: terminal batch +construction, ordered result supervision, bounded cancellation, fail-closed poison while late tasks +drain, and one authoritative batch summary. The benchmark's existing decision states did not change; +all leak and cancellation checks remained green. The measured overhead remains a residual risk to +watch in future characterization. + +The 2026-07-10 report's NO-GO applied to a narrower gate-only `_ToolExecutionPipeline` experiment that +failed to deepen the module. This change moves the full execution state machine and adds the approved +batch/cancellation contract; it does not reinterpret the earlier timing result as a performance win. + +## Compared revisions and invocation + +- Before: `10aedf26` (`main` after PR #206) +- After: `719322d4` (Task 4 head; Task 5 changes are report-only) +- Python: CPython 3.14.6 +- Platform: macOS 26.5.2, arm64 +- Warm-ups: 1 per fixture +- Measured runs: 5 per fixture +- Command, run once in each worktree: + + ```bash + uv run python scripts/benchmark_toolset.py --scenario all --runs 5 --output .json + ``` + +- Before machine record: [`2026-07-15-tool-execution-before.json`](./2026-07-15-tool-execution-before.json) +- After machine record: [`2026-07-15-tool-execution-after.json`](./2026-07-15-tool-execution-after.json) +- Directionality: local engineering evidence only, not universal product telemetry. + +The before run completed first in the base worktree; the after run then used the same machine, Python, +fixture matrix, and five-run protocol. No sample was discarded or selectively rerun. + +## Median timing comparison + +| Fixture | Before | After | Delta | +| --- | ---: | ---: | ---: | +| Safe execution, size 1 | 1.484 ms | 1.689 ms | +13.8% | +| Safe execution, size 10 | 14.030 ms | 15.267 ms | +8.8% | +| Safe execution, size 100 | 140.739 ms | 156.571 ms | +11.2% | +| Exclusive execution, size 1 | 1.460 ms | 1.513 ms | +3.6% | +| Exclusive execution, size 10 | 13.882 ms | 14.658 ms | +5.6% | +| Exclusive execution, size 100 | 138.604 ms | 150.126 ms | +8.3% | +| Mixed execution, 1 pair | 2.866 ms | 3.282 ms | +14.5% | +| Mixed execution, 10 pairs | 27.182 ms | 30.005 ms | +10.4% | +| Mixed execution, 100 pairs | 285.880 ms | 311.995 ms | +9.1% | +| Deduplication, 1 KiB | 1.517 ms | 1.816 ms | +19.7% | +| Deduplication, 100 KiB | 1.926 ms | 2.299 ms | +19.4% | +| Deduplication, 1 MiB | 5.879 ms | 7.096 ms | +20.7% | +| Registry projection p95, 50 tools | 0.298 ms | 0.310 ms | +4.2% | +| Registry projection p95, 500 tools | 3.129 ms | 3.248 ms | +3.8% | +| Registry projection p95, 5,000 tools | 32.233 ms | 33.553 ms | +4.1% | +| MCP startup-to-ready, 1 server | 1827.940 ms | 1717.103 ms | -6.1% | +| MCP startup-to-ready, 10 servers | 1894.863 ms | 1821.090 ms | -3.9% | +| MCP startup-to-ready, 50 servers | 2167.554 ms | 2220.232 ms | +2.4% | + +The machine records contain all raw samples, phase timings, within-run projections, cancellation +measurements, hashes, environment fields, and operation counts. + +## Deterministic decision comparison + +| Decision | Threshold | Before median/state | After median/state | +| --- | ---: | --- | --- | +| Execution framework overhead, short safe size 1 | 10% | 99.5059%, crossed | 99.4994%, crossed | +| MCP lifecycle/startup, 10 servers | 20% | 3.5190%, uncrossed | 3.6227%, uncrossed | +| MCP cleanup, 1 server | 6 s | 0.000135 s, uncrossed | 0.000137 s, uncrossed | +| MCP cleanup, 10 servers | 6 s | 0.000311 s, uncrossed | 0.000287 s, uncrossed | +| MCP cleanup, 50 servers | 6 s | 0.001047 s, uncrossed | 0.001072 s, uncrossed | +| Registry projection p95, 500 tools | 5 ms | 3.1288 ms, uncrossed | 3.2478 ms, uncrossed | +| Mixed gate wait/end-to-end, 10 pairs | 25% | 89.7602%, crossed | 89.4886%, crossed | + +Every decision had either zero or five crossings; neither report permitted an inconclusive rerun. + +## Safety and fault outcomes + +- All 18 before scenarios and all 18 after scenarios reported zero leaked tasks, processes, and + sessions. +- Every execution fixture reported completed cancellation, queued-reader completion, queued-writer + completion, and successful later-call recovery before and after. +- Cancellation medians remained bounded at 6.98–8.47 ms after extraction across the harness fixtures. +- Registry hashes remained stable within every fixture. +- Focused cancellation tests additionally cover a tool that ignores cancellation, timeout poisoning, + blocked new batches, late drain recovery, repeated caller cancellation, completed-result snapshots, + callback deactivation, and absence of unhandled task warnings. +- Full Task 4 validation passed 7,060 root tests and 65 e2e tests. + +## Residual risk + +The harness exercises the compatibility `handle()` path and deterministic local no-op tools; it is +not a production workload and does not isolate each added coroutine/frame. The consistent timing +increase is nevertheless treated as real directional evidence, not dismissed as noise. Future +changes to execution scheduling should rerun the same all/5 comparison and should prefer reducing the +measured overhead without weakening task ownership, cancellation bounds, or facade compatibility. diff --git a/docs/superpowers/specs/2026-07-15-tool-execution-cancellation-state-rollback-design.md b/docs/superpowers/specs/2026-07-15-tool-execution-cancellation-state-rollback-design.md new file mode 100644 index 00000000..12ebaf6f --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-tool-execution-cancellation-state-rollback-design.md @@ -0,0 +1,96 @@ +# Tool Execution Cancellation State Rollback Design + +## Goal + +Keep cross-step duplicate detection and consecutive-call protection consistent with the tool +results that Pythinker actually accepts into conversation state. A cancelled or failed batch must +not make an uncompleted call look previously completed when the model retries it. + +## Confirmed root cause + +`_ExecutionBatch._run()` currently calls `ToolExecutionEngine.end_step()` immediately after +dispatch. `end_step()` commits the current call fingerprints into `_seen_call_keys` and advances +the consecutive-call streak before watcher settlement succeeds. If the batch is then cancelled, +`PythinkerSoul` deliberately retains the prior successful batch fingerprints, but the engine keeps +the cancelled call internally. The next retry therefore observes state that conversation history +does not contain. + +## Design + +Batch state is transactional at the existing execution-engine boundary: + +- Dispatch records calls only in the engine's current, uncommitted step state. +- `_ExecutionBatch` waits for every watcher result before calling `end_step()` and publishing a + finalized `ToolBatchSummary`. +- Cancellation or failure calls an internal engine abort operation after owned futures and watchers + settle. Abort clears only current-step calls, task references, and the current dedup flag, then + closes the step so the next batch is initialized from its authoritative `ToolBatchContext`. +- Abort does not modify previously committed fingerprints or the previously committed consecutive + streak. +- Existing completion snapshots, callback suppression, bounded cancellation, timeout poisoning, + and exception propagation remain unchanged. + +This keeps the change inside the non-exported `ToolExecutionEngine` and `_ExecutionBatch`; it adds +no public API, configuration, dependency, or persisted-data change. + +## Failure and edge behavior + +- A normally settled batch commits once and continues to expose a finalized ordered summary. +- Cancellation before any result, between results, or while a callback is pending leaves completed + result snapshots intact but rolls back the batch's dedup state. +- Dispatch or watcher failure rolls back the same state and re-raises the original exception. +- A timed-out cancellation remains poisoned until late work drains; once drained, the aborted call + is not treated as completed. +- Repeated cancellation and zero-timeout behavior retain their existing contracts. + +## Verification + +A regression test first completes call A, starts and cancels blocking call B, then retries B while +the authoritative prior context still contains only A. Before the fix the retry is reported as a +cross-step duplicate with consecutive count 2. After the fix it is not a duplicate and its +consecutive count is 1. + +Focused cancellation/engine tests run during TDD. The final gate is +`make check-pythinker-code && make test-pythinker-code`, plus the affected core package gates and +`git diff --check` before publication. + +## Review-thread closeout + +The sole unresolved GitHub Code Quality thread is a false positive: awaiting a cancelled task +inside `pytest.raises(asyncio.CancelledError)` is the observable assertion, not a no-effect +statement. After the code fix is pushed and the latest CodeRabbit review completes, reply with that +evidence and resolve the thread through GitHub GraphQL `resolveReviewThread`. + +## Whole-branch review expansion + +The final merge-base-to-head review found three additional cancellation-lifecycle gaps introduced +by this PR. They are part of this closeout rather than deferred follow-up work: + +1. `StepResult` owns async result callbacks, but a callback that suppresses `CancelledError` can + make callback settlement wait forever after the batch cancellation deadline expires. +2. `PythinkerSoul` persists completed tool-result lineage only for `CancelledError`. A typed + `ToolCancellationTimeoutError` therefore skips the same context repair even though some calls + may have completed and the rest have unknown completion state. +3. The execution engine retains poisoned batches and late-drain tasks, but toolset cleanup does not + wait for or report those engine-owned tasks. + +The robust closeout uses one bounded ownership deadline across a `StepResult` cancellation attempt. +The remaining budget is passed to batch and callback settlement. Callback futures remain owned and +failure-observed until they actually drain; if the deadline expires, cancellation surfaces a typed +timeout instead of hanging or pretending cleanup succeeded. + +On either ordinary cancellation or cancellation timeout, Soul persists the assistant tool calls, +every known completed result, and one result for each unfinished call. Ordinary cancellation uses +the existing interrupted marker. Timeout uses an explicit unknown-completion marker that states the +operation may still be running and must not be retried automatically. The original cancellation or +typed timeout is re-raised only after the context write settles. + +`ToolExecutionEngine` also exposes an internal async cleanup operation for retained late-drain +tasks. Cleanup waits only to the configured safety bound, reports surviving work with a typed +`ToolCancellationTimeoutError`, and never cancels the drain observer in a way that would detach the +underlying tool supervisor. `PythinkerToolset.cleanup()` performs this engine cleanup and still +closes all MCP resources before propagating a retained engine-cleanup error. + +The public architecture page must describe `handle_batch()` and `ToolExecutionEngine` as the +Pythinker Soul path, while keeping per-call `handle()` documented only as the core compatibility +fallback for non-batch toolsets. diff --git a/packages/pythinker-core/src/pythinker_core/__init__.py b/packages/pythinker-core/src/pythinker_core/__init__.py index 786da3ce..bab036c9 100644 --- a/packages/pythinker-core/src/pythinker_core/__init__.py +++ b/packages/pythinker-core/src/pythinker_core/__init__.py @@ -7,8 +7,10 @@ """ import asyncio -from collections.abc import Callable, Sequence -from dataclasses import dataclass +import inspect +from collections.abc import Awaitable, Callable, Sequence +from dataclasses import dataclass, field +from typing import cast from loguru import logger @@ -19,7 +21,16 @@ TokenUsage, ) from pythinker_core.message import Message, ToolCall -from pythinker_core.tooling import ToolResult, ToolResultFuture, Toolset +from pythinker_core.tooling import ( + BatchToolset, + ToolBatchContext, + ToolBatchHandle, + ToolBatchSummary, + ToolCancellationTimeoutError, + ToolResult, + ToolResultFuture, + Toolset, +) from pythinker_core.utils.aio import Callback # Explicitly import submodules @@ -27,6 +38,8 @@ logger.disable("pythinker_core") +_STEP_CANCELLATION_TIMEOUT_SECONDS = 5.0 + __all__ = [ # submodules "chat_provider", @@ -39,79 +52,104 @@ "GenerateResult", "step", "StepResult", + "BatchToolset", + "ToolBatchContext", + "ToolBatchHandle", + "ToolBatchSummary", + "ToolCancellationTimeoutError", ] -async def step( - chat_provider: ChatProvider, - system_prompt: str, - toolset: Toolset, - history: Sequence[Message], - *, - on_message_part: Callback[[StreamedMessagePart], None] | None = None, - on_tool_result: Callable[[ToolResult], None] | None = None, -) -> "StepResult": - """ - Run one agent "step". In one step, the function generates LLM response based on the given - context for exactly one time. All new message parts will be streamed to `on_message_part` in - real-time if provided. Tool calls will be handled by `toolset`. The generated message will be - returned in a `StepResult`. Depending on the toolset implementation, the tool calls may be - handled asynchronously and the results need to be fetched with `await result.tool_results()`. - - The message history will NOT be modified in this function. - - The token usage will be returned in the `StepResult` if available. - - Raises: - APIConnectionError: If the API connection fails. - APITimeoutError: If the API request times out. - APIStatusError: If the API returns a status code of 4xx or 5xx. - APIEmptyResponseError: If the API returns an empty response. - ChatProviderError: If any other recognized chat provider error occurs. - asyncio.CancelledError: If the step is cancelled. - """ +class _ToolResultCallbackSupervisor: + """Own async result-publication callbacks without making callback errors fatal.""" + + def __init__(self, callback: Callable[[ToolResult], object]) -> None: + self._callback = callback + self._loop = asyncio.get_running_loop() + self._futures: set[asyncio.Future[None]] = set() + + def _report_failure(self, error: Exception) -> None: + self._loop.call_exception_handler( + { + "message": "Tool result callback failed", + "exception_type": type(error).__name__, + } + ) + + def _async_callback_done(self, future: asyncio.Future[None]) -> None: + self._futures.discard(future) + try: + future.result() + except asyncio.CancelledError: + return + except Exception as error: + self._report_failure(error) + + def __call__(self, result: ToolResult) -> None: + try: + outcome = self._callback(result) + except asyncio.CancelledError: + return + except Exception as error: + self._report_failure(error) + return + + if inspect.isawaitable(outcome): + future = asyncio.ensure_future(cast(Awaitable[None], outcome)) + self._futures.add(future) + future.add_done_callback(self._async_callback_done) + + async def settle(self, *, cancel: bool, timeout: float | None = None) -> None: + futures = list(self._futures) + if cancel: + for future in futures: + future.cancel() + if not futures: + return + if timeout is None: + await asyncio.wait(futures) + return + + _, pending = await asyncio.wait(futures, timeout=timeout) + if pending: + raise ToolCancellationTimeoutError( + f"Tool result callback cancellation did not settle within {timeout:g} seconds" + ) + - tool_calls: list[ToolCall] = [] +async def _dispatch_individual_tool_calls( + tool_calls: Sequence[ToolCall], + toolset: Toolset, + on_tool_result: Callable[[ToolResult], None] | None, +) -> dict[str, ToolResultFuture]: tool_result_futures: dict[str, ToolResultFuture] = {} - tool_callbacks_active = True + callbacks_active = True def future_done_callback(future: ToolResultFuture) -> None: - if not tool_callbacks_active: + if not callbacks_active: return - if on_tool_result: + if on_tool_result is not None: try: - result = future.result() - on_tool_result(result) + on_tool_result(future.result()) except asyncio.CancelledError: return - async def on_tool_call(tool_call: ToolCall) -> None: - tool_calls.append(tool_call) - result = toolset.handle(tool_call) - - if isinstance(result, ToolResult): - future = ToolResultFuture() - future.add_done_callback(future_done_callback) - future.set_result(result) - tool_result_futures[tool_call.id] = future - else: - result.add_done_callback(future_done_callback) - tool_result_futures[tool_call.id] = result - try: - result = await generate( - chat_provider, - system_prompt, - toolset.tools, - history, - on_message_part=on_message_part, - on_tool_call=on_tool_call, - ) + for tool_call in tool_calls: + result = toolset.handle(tool_call) + if isinstance(result, ToolResult): + future = ToolResultFuture() + future.add_done_callback(future_done_callback) + future.set_result(result) + tool_result_futures[tool_call.id] = future + else: + result.add_done_callback(future_done_callback) + tool_result_futures[tool_call.id] = result except BaseException: # A later terminal dispatch can fail after earlier work was accepted. Deactivate # publication before touching callbacks/tasks: already-queued callbacks cannot be # retracted by remove_done_callback(). - tool_callbacks_active = False + callbacks_active = False futures = list(tool_result_futures.values()) for future in futures: future.remove_done_callback(future_done_callback) @@ -119,6 +157,65 @@ async def on_tool_call(tool_call: ToolCall) -> None: await asyncio.gather(*futures, return_exceptions=True) raise + return tool_result_futures + + +async def _await_owned_settlement(task: asyncio.Task[None]) -> None: + cancellation: asyncio.CancelledError | None = None + while not task.done(): + try: + await asyncio.shield(task) + except asyncio.CancelledError as error: + cancellation = cancellation or error + + # Retrieve settlement failures before restoring the caller's cancellation. + task.result() + if cancellation is not None: + raise cancellation + + +async def step( + chat_provider: ChatProvider, + system_prompt: str, + toolset: Toolset, + history: Sequence[Message], + *, + on_message_part: Callback[[StreamedMessagePart], None] | None = None, + on_tool_result: Callable[[ToolResult], object] | None = None, + tool_batch_context: ToolBatchContext | None = None, +) -> "StepResult": + """Generate one complete response, then dispatch its terminal tool-call batch. + + The message history is not modified. Batch-capable toolsets receive all calls at once after + successful terminal assembly; legacy toolsets keep per-call ``handle()`` dispatch. + """ + callback_supervisor = ( + _ToolResultCallbackSupervisor(on_tool_result) if on_tool_result is not None else None + ) + result = await generate( + chat_provider, + system_prompt, + toolset.tools, + history, + on_message_part=on_message_part, + ) + tool_calls = list(result.message.tool_calls or ()) + + if isinstance(toolset, BatchToolset): + tool_batch = toolset.handle_batch( + tool_calls, + tool_batch_context or ToolBatchContext(), + on_tool_result=callback_supervisor, + ) + tool_result_futures: dict[str, ToolResultFuture] = {} + else: + tool_batch = None + tool_result_futures = await _dispatch_individual_tool_calls( + tool_calls, + toolset, + callback_supervisor, + ) + return StepResult( result.id, result.message, @@ -126,6 +223,8 @@ async def on_tool_call(tool_call: ToolCall) -> None: tool_calls, tool_result_futures, truncated=result.truncated, + _tool_batch=tool_batch, + _tool_result_callback_supervisor=callback_supervisor, ) @@ -144,25 +243,111 @@ class StepResult: """All the tool calls generated in this step.""" _tool_result_futures: dict[str, ToolResultFuture] - """@private The futures of the results of the spawned tool calls.""" + """@private Legacy futures for per-call toolset dispatch.""" truncated: bool = False """True when the model's response was cut off by the output-token limit.""" - async def tool_results(self) -> list[ToolResult]: - """All the tool results returned by corresponding tool calls.""" - if not self._tool_result_futures: - return [] + _tool_batch: ToolBatchHandle | None = field(default=None, repr=False, compare=False) + """@private Supervising handle for batch-capable toolset dispatch.""" + + _tool_result_callback_supervisor: _ToolResultCallbackSupervisor | None = field( + default=None, + repr=False, + compare=False, + ) + + _cancel_settlement_task: asyncio.Task[None] | None = field( + default=None, + init=False, + repr=False, + compare=False, + ) + async def tool_results(self) -> list[ToolResult]: + """Return results in model call order, settling all owned work.""" try: - results: list[ToolResult] = [] - for tool_call in self.tool_calls: - future = self._tool_result_futures[tool_call.id] - result = await future - results.append(result) + results: list[ToolResult] + if self._tool_batch is not None: + results = await self._tool_batch.results() + else: + results = [] + for tool_call in self.tool_calls: + future = self._tool_result_futures[tool_call.id] + results.append(await future) + await self._cancel_legacy_futures() + + if self._tool_result_callback_supervisor is not None: + await self._tool_result_callback_supervisor.settle(cancel=False) return results - finally: - # one exception should cancel all the futures to avoid hanging tasks - for future in self._tool_result_futures.values(): - future.cancel() - await asyncio.gather(*self._tool_result_futures.values(), return_exceptions=True) + except BaseException as primary_error: + try: + await self.cancel_tool_execution() + except ToolCancellationTimeoutError: + raise + except asyncio.CancelledError: + # Preserve the original control-flow/error after supervised settlement. + pass + raise primary_error + + async def cancel_tool_execution(self) -> None: + """Idempotently cancel and settle all owned tool execution.""" + settlement = self._cancel_settlement_task + if settlement is None: + settlement = asyncio.create_task(self._cancel_and_settle_owned_work()) + object.__setattr__(self, "_cancel_settlement_task", settlement) + await _await_owned_settlement(settlement) + + async def _cancel_and_settle_owned_work(self) -> None: + loop = asyncio.get_running_loop() + deadline = loop.time() + _STEP_CANCELLATION_TIMEOUT_SECONDS + try: + if self._tool_batch is not None: + await self._tool_batch.cancel_and_settle(timeout=max(0.0, deadline - loop.time())) + else: + await self._cancel_legacy_futures() + except BaseException as primary_error: + if self._tool_result_callback_supervisor is not None: + try: + await self._tool_result_callback_supervisor.settle( + cancel=True, + timeout=max(0.0, deadline - loop.time()), + ) + except BaseException as callback_error: + primary_error.add_note( + "Owned tool-result callback cleanup also failed: " + f"{type(callback_error).__name__}: {callback_error}" + ) + raise + + if self._tool_result_callback_supervisor is not None: + await self._tool_result_callback_supervisor.settle( + cancel=True, + timeout=max(0.0, deadline - loop.time()), + ) + + async def _cancel_legacy_futures(self) -> None: + futures = list(self._tool_result_futures.values()) + for future in futures: + future.cancel() + await asyncio.gather(*futures, return_exceptions=True) + + @property + def completed_tool_results(self) -> dict[str, ToolResult]: + """Snapshot of successful results that have already completed.""" + if self._tool_batch is not None: + return dict(self._tool_batch.completed_results) + + completed: dict[str, ToolResult] = {} + for tool_call_id, future in self._tool_result_futures.items(): + if not future.done() or future.cancelled() or future.exception() is not None: + continue + completed[tool_call_id] = future.result() + return completed + + @property + def tool_execution_summary(self) -> ToolBatchSummary: + """Final batch metadata, or the empty summary for legacy dispatch.""" + if self._tool_batch is None: + return ToolBatchSummary() + return self._tool_batch.summary diff --git a/packages/pythinker-core/src/pythinker_core/tooling/__init__.py b/packages/pythinker-core/src/pythinker_core/tooling/__init__.py index b49124de..d7d41404 100644 --- a/packages/pythinker-core/src/pythinker_core/tooling/__init__.py +++ b/packages/pythinker-core/src/pythinker_core/tooling/__init__.py @@ -1,5 +1,7 @@ from abc import ABC, abstractmethod from asyncio import Future +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass from typing import Any, ClassVar, Protocol, Self, cast, override, runtime_checkable import jsonschema @@ -327,6 +329,61 @@ class ToolResult(BaseModel): ToolResultFuture = Future[ToolResult] type HandleResult = ToolResultFuture | ToolResult +type ToolCallFingerprint = tuple[str, str] + + +class ToolCancellationTimeoutError(RuntimeError): + """One or more cancelled tool tasks did not settle within the safety bound.""" + + +@dataclass(frozen=True, slots=True) +class ToolBatchContext: + """Immutable cross-step execution metadata supplied when a batch is created.""" + + turn_id: str = "" + step_no: int = 0 + prior_call_fingerprints: tuple[ToolCallFingerprint, ...] = () + + +@dataclass(frozen=True, slots=True) +class ToolBatchSummary: + """Final normalized call and deduplication state exposed by a batch handle.""" + + current_call_fingerprints: tuple[ToolCallFingerprint, ...] = () + dedup_triggered: bool = False + consecutive_identical_call_count: int = 0 + finalized: bool = True + + +@runtime_checkable +class ToolBatchHandle(Protocol): + """Supervises one exception-atomic terminal batch and its ordered results.""" + + @property + def tool_calls(self) -> Sequence[ToolCall]: ... + + @property + def completed_results(self) -> Mapping[str, ToolResult]: ... + + @property + def summary(self) -> ToolBatchSummary: ... + + async def results(self) -> list[ToolResult]: ... + + async def cancel_and_settle(self, *, timeout: float | None = None) -> None: ... + + +@runtime_checkable +class BatchToolset(Protocol): + """Optional additive Toolset protocol for terminal batch dispatch.""" + + def handle_batch( + self, + tool_calls: Sequence[ToolCall], + context: ToolBatchContext, + *, + on_tool_result: Callable[[ToolResult], None] | None = None, + ) -> ToolBatchHandle: ... @runtime_checkable diff --git a/packages/pythinker-core/tests/test_batch_toolset.py b/packages/pythinker-core/tests/test_batch_toolset.py new file mode 100644 index 00000000..6f788516 --- /dev/null +++ b/packages/pythinker-core/tests/test_batch_toolset.py @@ -0,0 +1,666 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable, Mapping, Sequence + +import pytest + +from pythinker_core import StepResult, step +from pythinker_core.chat_provider import StreamedMessagePart +from pythinker_core.chat_provider.mock import MockChatProvider +from pythinker_core.message import Message, TextPart, ToolCall +from pythinker_core.tooling import ( + BatchToolset, + Tool, + ToolBatchContext, + ToolBatchHandle, + ToolBatchSummary, + ToolCancellationTimeoutError, + ToolOk, + ToolResult, + ToolResultFuture, + Toolset, +) +from pythinker_core.tooling.simple import SimpleToolset + + +def _tool_call(call_id: str, name: str) -> ToolCall: + return ToolCall( + id=call_id, + function=ToolCall.FunctionBody(name=name, arguments="{}"), + ) + + +def _tool_result(call: ToolCall) -> ToolResult: + return ToolResult(tool_call_id=call.id, return_value=ToolOk(output=call.function.name)) + + +def _message_parts(*parts: StreamedMessagePart) -> list[StreamedMessagePart]: + return list(parts) + + +class CompletedBatch: + def __init__( + self, + calls: list[ToolCall], + results: list[ToolResult], + on_tool_result: Callable[[ToolResult], None] | None, + ) -> None: + self._calls = calls + self._results = results + self._on_tool_result = on_tool_result + self._notified = False + self._completed = {result.tool_call_id: result for result in reversed(results)} + self._summary = ToolBatchSummary( + current_call_fingerprints=tuple( + (call.function.name, call.function.arguments or "{}") for call in calls + ), + dedup_triggered=False, + consecutive_identical_call_count=1 if calls else 0, + ) + self.cancelled = False + self.cancel_timeout: float | None = None + self.cancel_error: BaseException | None = None + + @property + def tool_calls(self) -> list[ToolCall]: + return list(self._calls) + + @property + def completed_results(self) -> dict[str, ToolResult]: + return dict(self._completed) + + @property + def summary(self) -> ToolBatchSummary: + return self._summary + + async def results(self) -> list[ToolResult]: + if not self._notified and self._on_tool_result is not None: + self._notified = True + for result in reversed(self._results): + self._on_tool_result(result) + return list(self._results) + + async def cancel_and_settle(self, *, timeout: float | None = None) -> None: + self.cancelled = True + self.cancel_timeout = timeout + if self.cancel_error is not None: + raise self.cancel_error + + +class RecordingBatchToolset: + def __init__(self, trace: list[str]) -> None: + self.trace = trace + self.batch_calls = 0 + self.received_context: ToolBatchContext | None = None + self.handle_count = 0 + self.batch: CompletedBatch | None = None + + @property + def tools(self) -> list[Tool]: + return [] + + def handle(self, tool_call: ToolCall) -> ToolResult: + self.handle_count += 1 + raise AssertionError(f"batch-capable toolset used handle for {tool_call.id}") + + def handle_batch( + self, + tool_calls: Sequence[ToolCall], + context: ToolBatchContext, + *, + on_tool_result: Callable[[ToolResult], None] | None = None, + ) -> ToolBatchHandle: + self.trace.append("batch") + self.batch_calls += 1 + self.received_context = context + calls = list(tool_calls) + self.batch = CompletedBatch(calls, [_tool_result(call) for call in calls], on_tool_result) + return self.batch + + +async def test_step_uses_batch_toolset_once_after_all_message_parts() -> None: + trace: list[str] = [] + toolset = RecordingBatchToolset(trace) + calls = [_tool_call("call-1", "First"), _tool_call("call-2", "Second")] + + result = await step( + MockChatProvider([TextPart(text="ready"), *calls], finish_reason="tool_calls"), + "", + toolset, + [], + on_message_part=lambda part: trace.append(f"part:{type(part).__name__}"), + tool_batch_context=ToolBatchContext(turn_id="turn-1", step_no=2), + ) + + assert trace == ["part:TextPart", "part:ToolCall", "part:ToolCall", "batch"] + assert toolset.batch_calls == 1 + assert toolset.handle_count == 0 + assert toolset.received_context == ToolBatchContext(turn_id="turn-1", step_no=2) + assert await result.tool_results() == [_tool_result(call) for call in calls] + + +async def test_batch_callbacks_can_complete_out_of_order_but_results_stay_model_ordered() -> None: + toolset = RecordingBatchToolset([]) + calls = [_tool_call("call-1", "First"), _tool_call("call-2", "Second")] + callbacks: list[str] = [] + + result = await step( + MockChatProvider(_message_parts(*calls), finish_reason="tool_calls"), + "", + toolset, + [], + on_tool_result=lambda item: callbacks.append(item.tool_call_id), + ) + + ordered = await result.tool_results() + await asyncio.sleep(0) + assert callbacks == ["call-2", "call-1"] + assert [item.tool_call_id for item in ordered] == ["call-1", "call-2"] + + +async def test_step_exposes_batch_summary() -> None: + toolset = RecordingBatchToolset([]) + calls = [_tool_call("call-1", "First"), _tool_call("call-2", "Second")] + + result = await step( + MockChatProvider(_message_parts(*calls), finish_reason="tool_calls"), + "", + toolset, + [], + tool_batch_context=ToolBatchContext( + turn_id="turn-1", + step_no=1, + prior_call_fingerprints=(("Earlier", "{}"),), + ), + ) + + assert result.tool_execution_summary == ToolBatchSummary( + current_call_fingerprints=(("First", "{}"), ("Second", "{}")), + dedup_triggered=False, + consecutive_identical_call_count=1, + ) + + +async def test_empty_tool_call_response_returns_empty_summary() -> None: + toolset = RecordingBatchToolset([]) + + result = await step(MockChatProvider([TextPart(text="done")]), "", toolset, []) + + assert toolset.batch_calls == 1 + assert result.tool_calls == [] + assert await result.tool_results() == [] + assert result.tool_execution_summary == ToolBatchSummary(consecutive_identical_call_count=0) + + +class WaitingBatch: + def __init__(self, calls: Sequence[ToolCall]) -> None: + self._calls = list(calls) + self.results_started = asyncio.Event() + self.cancel_started = asyncio.Event() + self.settle_release = asyncio.Event() + self.settle_finished = asyncio.Event() + self._never = asyncio.Event() + self.cancel_count = 0 + + @property + def tool_calls(self) -> Sequence[ToolCall]: + return self._calls + + @property + def completed_results(self) -> Mapping[str, ToolResult]: + return {} + + @property + def summary(self) -> ToolBatchSummary: + return ToolBatchSummary() + + async def results(self) -> list[ToolResult]: + self.results_started.set() + await self._never.wait() + return [] + + async def cancel_and_settle(self, *, timeout: float | None = None) -> None: + del timeout + self.cancel_count += 1 + self.cancel_started.set() + await self.settle_release.wait() + self.settle_finished.set() + + +class WaitingBatchToolset(RecordingBatchToolset): + def __init__(self) -> None: + super().__init__([]) + self.waiting: WaitingBatch | None = None + + def handle_batch( + self, + tool_calls: Sequence[ToolCall], + context: ToolBatchContext, + *, + on_tool_result: Callable[[ToolResult], None] | None = None, + ) -> WaitingBatch: + del context, on_tool_result + self.batch_calls += 1 + self.waiting = WaitingBatch(tool_calls) + return self.waiting + + +async def test_tool_results_cancellation_delegates_to_batch_handle() -> None: + toolset = WaitingBatchToolset() + result = await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + toolset, + [], + ) + assert toolset.waiting is not None + waiter = asyncio.create_task(result.tool_results()) + await toolset.waiting.results_started.wait() + + waiter.cancel() + await toolset.waiting.cancel_started.wait() + toolset.waiting.settle_release.set() + with pytest.raises(asyncio.CancelledError): + await waiter + + assert toolset.waiting.cancel_count == 1 + assert toolset.waiting.settle_finished.is_set() + + +async def test_cancel_tool_execution_owns_batch_before_tool_results_starts() -> None: + toolset = WaitingBatchToolset() + result = await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + toolset, + [], + ) + assert toolset.waiting is not None + cancel = asyncio.create_task(result.cancel_tool_execution()) + await toolset.waiting.cancel_started.wait() + toolset.waiting.settle_release.set() + await cancel + + assert not toolset.waiting.results_started.is_set() + assert toolset.waiting.settle_finished.is_set() + + +async def test_repeated_cancellation_during_settlement_never_detaches_cleanup() -> None: + toolset = WaitingBatchToolset() + result = await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + toolset, + [], + ) + assert toolset.waiting is not None + cancel = asyncio.create_task(result.cancel_tool_execution()) + await toolset.waiting.cancel_started.wait() + + cancel.cancel() + await asyncio.sleep(0) + cancel.cancel() + await asyncio.sleep(0) + assert not toolset.waiting.settle_finished.is_set() + toolset.waiting.settle_release.set() + + with pytest.raises(asyncio.CancelledError): + await cancel + assert toolset.waiting.settle_finished.is_set() + assert toolset.waiting.cancel_count == 1 + + +async def test_direct_step_result_constructor_keeps_future_map_contract() -> None: + call = _tool_call("call-1", "First") + expected = _tool_result(call) + future: ToolResultFuture = asyncio.get_running_loop().create_future() + future.set_result(expected) + result = StepResult( + None, + Message(role="assistant", content=[]), + None, + [call], + {call.id: future}, + ) + + assert await result.tool_results() == [expected] + assert result.tool_execution_summary == ToolBatchSummary() + + +async def test_step_completed_results_snapshot_covers_batch_and_legacy_modes() -> None: + batch_toolset = RecordingBatchToolset([]) + batch_result = await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + batch_toolset, + [], + ) + assert batch_result.completed_tool_results == { + "call-1": _tool_result(_tool_call("call-1", "First")) + } + + done_call = _tool_call("done", "Done") + pending_call = _tool_call("pending", "Pending") + done: ToolResultFuture = asyncio.get_running_loop().create_future() + done.set_result(_tool_result(done_call)) + pending: ToolResultFuture = asyncio.get_running_loop().create_future() + legacy = StepResult( + None, + Message(role="assistant", content=[]), + None, + [done_call, pending_call], + {"done": done, "pending": pending}, + ) + assert legacy.completed_tool_results == {"done": _tool_result(done_call)} + await legacy.cancel_tool_execution() + + +class InlineCallbackToolset(RecordingBatchToolset): + def handle_batch( + self, + tool_calls: Sequence[ToolCall], + context: ToolBatchContext, + *, + on_tool_result: Callable[[ToolResult], None] | None = None, + ) -> ToolBatchHandle: + batch = super().handle_batch(tool_calls, context, on_tool_result=None) + if on_tool_result is not None: + on_tool_result(_tool_result(list(tool_calls)[0])) + return batch + + +async def test_batch_immediate_callback_exception_is_reported_but_nonfatal() -> None: + reports: list[dict[str, object]] = [] + loop = asyncio.get_running_loop() + previous = loop.get_exception_handler() + loop.set_exception_handler(lambda _loop, context: reports.append(context)) + try: + result = await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + InlineCallbackToolset([]), + [], + on_tool_result=lambda _result: (_ for _ in ()).throw( + RuntimeError("private callback detail") + ), + ) + assert await result.tool_results() == [_tool_result(_tool_call("call-1", "First"))] + finally: + loop.set_exception_handler(previous) + + assert reports == [ + { + "message": "Tool result callback failed", + "exception_type": "RuntimeError", + } + ] + + +async def test_tool_results_waits_for_owned_async_callback() -> None: + callback_started = asyncio.Event() + callback_release = asyncio.Event() + + async def blocking_callback(_result: ToolResult) -> None: + callback_started.set() + await callback_release.wait() + + result = await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + RecordingBatchToolset([]), + [], + on_tool_result=blocking_callback, + ) + results_task = asyncio.create_task(result.tool_results()) + await callback_started.wait() + assert not results_task.done() + + callback_release.set() + assert await results_task == [_tool_result(_tool_call("call-1", "First"))] + + +async def test_tool_results_cancellation_settles_owned_async_callback() -> None: + callback_started = asyncio.Event() + callback_cancelled = asyncio.Event() + + async def blocking_callback(_result: ToolResult) -> None: + callback_started.set() + try: + await asyncio.Event().wait() + finally: + callback_cancelled.set() + + result = await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + RecordingBatchToolset([]), + [], + on_tool_result=blocking_callback, + ) + results_task = asyncio.create_task(result.tool_results()) + await callback_started.wait() + results_task.cancel() + + with pytest.raises(asyncio.CancelledError): + await results_task + assert callback_cancelled.is_set() + + +@pytest.mark.parametrize( + "batch_error", + [None, ToolCancellationTimeoutError("batch cancellation failed first")], +) +async def test_tool_results_cancellation_bounds_resistant_async_callback( + monkeypatch: pytest.MonkeyPatch, + batch_error: ToolCancellationTimeoutError | None, +) -> None: + monkeypatch.setattr("pythinker_core._STEP_CANCELLATION_TIMEOUT_SECONDS", 0.01) + callback_started = asyncio.Event() + callback_cancelled = asyncio.Event() + callback_release = asyncio.Event() + callback_finished = asyncio.Event() + + async def resistant_callback(_result: ToolResult) -> None: + callback_started.set() + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + callback_cancelled.set() + await callback_release.wait() + finally: + callback_finished.set() + + toolset = RecordingBatchToolset([]) + result = await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + toolset, + [], + on_tool_result=resistant_callback, + ) + assert toolset.batch is not None + toolset.batch.cancel_error = batch_error + results_task = asyncio.create_task(result.tool_results()) + await callback_started.wait() + results_task.cancel() + + try: + done, pending = await asyncio.wait({results_task}, timeout=0.1) + assert done == {results_task} + assert pending == set() + if batch_error is None: + with pytest.raises(ToolCancellationTimeoutError, match="callback cancellation"): + await results_task + else: + with pytest.raises( + ToolCancellationTimeoutError, match="batch cancellation failed first" + ) as caught: + await results_task + assert len(caught.value.__notes__) == 1 + assert caught.value.__notes__[0].startswith( + "Owned tool-result callback cleanup also failed: " + "ToolCancellationTimeoutError: Tool result callback cancellation did not settle" + ) + assert callback_cancelled.is_set() + assert toolset.batch.cancel_timeout is not None + assert 0 <= toolset.batch.cancel_timeout <= 0.01 + finally: + callback_release.set() + await asyncio.wait_for(callback_finished.wait(), timeout=1) + await asyncio.gather(results_task, return_exceptions=True) + + +async def test_batch_async_callback_exception_is_reported_but_nonfatal() -> None: + reports: list[dict[str, object]] = [] + reported = asyncio.Event() + loop = asyncio.get_running_loop() + previous = loop.get_exception_handler() + + def capture_report(_loop: asyncio.AbstractEventLoop, context: dict[str, object]) -> None: + reports.append(context) + reported.set() + + loop.set_exception_handler(capture_report) + + async def failing_callback(_result: ToolResult) -> None: + await asyncio.sleep(0) + raise RuntimeError("private async callback detail") + + try: + result = await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + RecordingBatchToolset([]), + [], + on_tool_result=failing_callback, + ) + assert await result.tool_results() == [_tool_result(_tool_call("call-1", "First"))] + await asyncio.wait_for(reported.wait(), timeout=1) + finally: + loop.set_exception_handler(previous) + + assert reports == [ + { + "message": "Tool result callback failed", + "exception_type": "RuntimeError", + } + ] + + +class ConstructionFailureToolset(RecordingBatchToolset): + def __init__(self) -> None: + super().__init__([]) + + def handle_batch( + self, + tool_calls: Sequence[ToolCall], + context: ToolBatchContext, + *, + on_tool_result: Callable[[ToolResult], None] | None = None, + ) -> ToolBatchHandle: + del tool_calls, context, on_tool_result + raise RuntimeError("construction failed") + + +async def test_batch_construction_failure_propagates_without_callbacks() -> None: + toolset = ConstructionFailureToolset() + callbacks: list[ToolResult] = [] + + with pytest.raises(RuntimeError, match="construction failed"): + await step( + MockChatProvider([_tool_call("call-1", "First")], finish_reason="tool_calls"), + "", + toolset, + [], + on_tool_result=callbacks.append, + ) + + assert callbacks == [] + + +class PendingThenFailingToolset: + def __init__(self) -> None: + self.handle_count = 0 + self.first_future: ToolResultFuture | None = None + + @property + def tools(self) -> list[Tool]: + return [] + + def handle(self, tool_call: ToolCall) -> ToolResult | ToolResultFuture: + self.handle_count += 1 + if self.handle_count == 1: + self.first_future = asyncio.get_running_loop().create_future() + return self.first_future + raise RuntimeError("second dispatch failed") + + +async def test_legacy_second_handle_cancellation_preserves_pr1_rollback() -> None: + toolset = PendingThenFailingToolset() + callbacks: list[ToolResult] = [] + + with pytest.raises(RuntimeError, match="second dispatch failed"): + await step( + MockChatProvider( + [_tool_call("call-1", "First"), _tool_call("call-2", "Second")], + finish_reason="tool_calls", + ), + "", + toolset, + [], + on_tool_result=callbacks.append, + ) + + assert toolset.first_future is not None + assert toolset.first_future.cancelled() + await asyncio.sleep(0) + assert callbacks == [] + + +class ImmediateThenCancelledToolset: + def __init__(self) -> None: + self.handle_count = 0 + + @property + def tools(self) -> list[Tool]: + return [] + + def handle(self, tool_call: ToolCall) -> ToolResult: + self.handle_count += 1 + if self.handle_count == 1: + return _tool_result(tool_call) + raise asyncio.CancelledError() + + +async def test_legacy_dispatch_abort_preserves_pr1_queued_callback_guard() -> None: + callbacks: list[ToolResult] = [] + with pytest.raises(asyncio.CancelledError): + await step( + MockChatProvider( + [_tool_call("call-1", "First"), _tool_call("call-2", "Second")], + finish_reason="tool_calls", + ), + "", + ImmediateThenCancelledToolset(), + [], + on_tool_result=callbacks.append, + ) + + await asyncio.sleep(0) + assert callbacks == [] + + +def test_batch_protocol_is_optional_and_runtime_checkable() -> None: + batch = RecordingBatchToolset([]) + simple = SimpleToolset() + + assert isinstance(batch, BatchToolset) + assert isinstance(batch, Toolset) + assert isinstance(simple, Toolset) + assert not isinstance(simple, BatchToolset) + assert isinstance( + CompletedBatch([], [], None), + ToolBatchHandle, + ) + assert issubclass(ToolCancellationTimeoutError, RuntimeError) diff --git a/src/pythinker_code/soul/pythinkersoul.py b/src/pythinker_code/soul/pythinkersoul.py index 945c057b..abd04741 100644 --- a/src/pythinker_code/soul/pythinkersoul.py +++ b/src/pythinker_code/soul/pythinkersoul.py @@ -25,6 +25,7 @@ TokenUsage, ) from pythinker_core.message import Message, ToolCall +from pythinker_core.tooling import ToolBatchContext, ToolCancellationTimeoutError from pythinker_core.tooling.error import ToolRuntimeError from tenacity import RetryCallState, retry_if_exception, stop_after_attempt, wait_exponential_jitter @@ -627,8 +628,8 @@ def __init__( self._steer_queue: asyncio.Queue[str | list[ContentPart]] = asyncio.Queue() self._prompt_queue_lock = asyncio.Lock() - # Tool calls made in the previous step, fed to the toolset's dedup - # tracking at the start of each step (see PythinkerToolset.begin_step). + # Normalized calls from the previous batch summary, carried into the next + # ToolBatchContext for cross-step deduplication. self._last_tool_calls: list[tuple[str, str]] = [] self._current_turn_id: str = "" self._plan_mode: bool = self._runtime.session.state.plan_mode @@ -2182,26 +2183,13 @@ async def _append_notification(view: NotificationView) -> None: await self._persist_assembled_history(prepared_request) effective_history = prepared_request.assembled.provider_history - # Capture tool results as they stream in. If the batch is interrupted - # mid-flight, already-completed calls must keep their real output rather - # than being overwritten with a synthetic "interrupted" marker; only the - # still-pending calls get the marker (see the CancelledError handler). - completed_tool_results: dict[str, ToolResult] = {} - def _on_tool_result(tool_result: ToolResult) -> None: - completed_tool_results[tool_result.tool_call_id] = tool_result wire_send(tool_result) async def _run_step_once() -> StepResult: - # Reset per-step dedup state. Inside the retry wrapper on purpose: a - # retried step must not await tool tasks cancelled by the failed attempt. - if isinstance(self._agent.toolset, PythinkerToolset): - self._agent.toolset.begin_step( - self._last_tool_calls, - step_no=self._current_step_no, - turn_id=self._current_turn_id, - ) - # run an LLM step (may be interrupted) + # Run an LLM step (may be interrupted). The terminal batch receives all + # execution state atomically; retries that fail before batch construction + # cannot leave per-step tool state behind. from pythinker_code.telemetry import metrics as _m from pythinker_code.telemetry import otel as _otel @@ -2232,6 +2220,11 @@ async def _run_step_once() -> StepResult: effective_history, on_message_part=wire_send, on_tool_result=_on_tool_result, + tool_batch_context=ToolBatchContext( + turn_id=self._current_turn_id, + step_no=self._current_step_no, + prior_call_fingerprints=tuple(self._last_tool_calls), + ), ) finally: reset_step_permission_profile(profile_token) @@ -2368,18 +2361,25 @@ async def _pythinker_core_step_with_retry() -> StepResult: with deliberation_scope(deliberation_context_id, deliberation_generation): try: results = await result.tool_results() - except asyncio.CancelledError: - # Interrupted mid-tool: persist the assistant message plus a result + except (asyncio.CancelledError, ToolCancellationTimeoutError) as interruption: + # Interrupted or timed out mid-tool: persist the assistant message plus a result # for every tool_call so the next turn does not see unanswered - # tool_calls (which providers reject). Keep the real output of calls - # that already completed (streamed via on_tool_result); only the - # still-pending calls get a synthetic interruption marker. Shield the - # write from the same cancellation so it completes, then re-raise. + # tool_calls (which providers reject). Keep successful outputs from + # the StepResult's authoritative completion snapshot; only still-pending + # calls get a truthful synthetic marker. Shield the write from the + # same cancellation so it completes, then re-raise the original error. + completed_tool_results = result.completed_tool_results + pending_message = ( + "Tool call completion is unknown because cancellation did not settle; " + "the operation may still be running and must not be retried automatically." + if isinstance(interruption, ToolCancellationTimeoutError) + else "Tool call interrupted by user." + ) interrupted = [ completed_tool_results.get(tc.id) or ToolResult( tool_call_id=tc.id, - return_value=ToolRuntimeError(message="Tool call interrupted by user."), + return_value=ToolRuntimeError(message=pending_message), ) for tc in result.tool_calls ] @@ -2393,9 +2393,8 @@ async def _pythinker_core_step_with_retry() -> StepResult: raise logger.debug("Got tool results: {results}", results=results) - # Update dedup tracking for the next step - if isinstance(self._agent.toolset, PythinkerToolset): - self._last_tool_calls = self._agent.toolset.end_step() + batch_summary = result.tool_execution_summary + self._last_tool_calls = list(batch_summary.current_call_fingerprints) # If a tool (EnterPlanMode/ExitPlanMode) changed plan mode during execution, # send a corrected StatusUpdate so the client sees the up-to-date state. @@ -2523,8 +2522,8 @@ async def _pythinker_core_step_with_retry() -> StepResult: # success on a call that never made progress, which the all-error # check above can't see. repeat_threshold = self._loop_control.max_consecutive_identical_calls - if repeat_threshold and isinstance(self._agent.toolset, PythinkerToolset): - repeat_count = self._agent.toolset.consecutive_repeat_count + if repeat_threshold: + repeat_count = batch_summary.consecutive_identical_call_count if repeat_count >= repeat_threshold: from pythinker_code.telemetry import track diff --git a/src/pythinker_code/soul/tool_execution.py b/src/pythinker_code/soul/tool_execution.py new file mode 100644 index 00000000..fcd88560 --- /dev/null +++ b/src/pythinker_code/soul/tool_execution.py @@ -0,0 +1,945 @@ +from __future__ import annotations + +import asyncio +import contextlib +import copy +import difflib +import hashlib +import json +import math +import time +from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping, Sequence +from contextvars import ContextVar +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, cast + +from pythinker_core.tooling import ( + CallableTool, + CallableTool2, + HandleResult, + ToolBatchContext, + ToolBatchHandle, + ToolBatchSummary, + ToolCancellationTimeoutError, + ToolError, + ToolResultFuture, +) +from pythinker_core.tooling.error import ToolNotFoundError, ToolParseError, ToolRuntimeError +from pythinker_core.utils.typing import JsonType + +from pythinker_code.hooks.engine import HookEngine +from pythinker_code.telemetry.names import sanitize_telemetry_tool_name +from pythinker_code.utils.logging import logger +from pythinker_code.wire.types import ( + ContentPart, + TextPart, + ToolCall, + ToolExecutionStarted, + ToolResult, + ToolReturnValue, + ToolUseSkipped, +) + +if TYPE_CHECKING: + from pythinker_code.soul.agent import Runtime + + +type ToolType = CallableTool | CallableTool2[Any] +type ToolCallKey = tuple[str, str] + + +current_tool_call = ContextVar[ToolCall | None]("current_tool_call", default=None) +_current_tool_execution_started_ids: ContextVar[set[str] | None] = ContextVar( + "current_tool_execution_started_ids", default=None +) +_current_session_id: ContextVar[str] = ContextVar("_current_session_id", default="") + + +def set_session_id(sid: str) -> None: + _current_session_id.set(sid) + + +def get_session_id() -> str: + return _current_session_id.get() + + +def _get_session_id() -> str: + return _current_session_id.get() + + +def get_current_tool_call_or_none() -> ToolCall | None: + """ + Get the current tool call or None. + Expect to be not None when called from a `__call__` method of a tool. + """ + return current_tool_call.get() + + +def emit_current_tool_execution_started() -> None: + """Emit ToolExecutionStarted once for the current tool call, if wire is active.""" + tool_call = get_current_tool_call_or_none() + if tool_call is None: + return + + started_ids = _current_tool_execution_started_ids.get() + if started_ids is None: + started_ids = set[str]() + _current_tool_execution_started_ids.set(started_ids) + if tool_call.id in started_ids: + return + started_ids.add(tool_call.id) + + try: + from pythinker_code.soul import get_wire_or_none + + if wire := get_wire_or_none(): + wire.soul_side.send(ToolExecutionStarted(tool_call_id=tool_call.id)) + except Exception as exc: # noqa: BLE001 - lifecycle events must not break tool execution + logger.debug( + "Failed to emit tool execution start: {tool_name} (call_id={call_id}): {error}", + tool_name=tool_call.function.name, + call_id=tool_call.id, + error=exc, + ) + + +def _emit_tool_use_skipped( + *, + tool_call_id: str, + tool_name: str, + reason: Literal["dedup", "policy", "interrupt", "concurrent_inflight"], + resumed: bool = False, +) -> None: + try: + from pythinker_code.soul import get_wire_or_none + + if wire := get_wire_or_none(): + wire.soul_side.send( + ToolUseSkipped( + tool_call_id=tool_call_id, + tool_name=tool_name, + reason=reason, + resumed=resumed, + ) + ) + except Exception as exc: # noqa: BLE001 - observability must not break tool execution + logger.debug( + "Failed to emit tool skipped event: {tool_name} (call_id={call_id}): {error}", + tool_name=tool_name, + call_id=tool_call_id, + error=exc, + ) + + +def tool_defers_execution_started(tool: ToolType) -> bool: + return bool(getattr(tool, "emits_tool_execution_started_after_approval", False)) + + +_REMINDER_TEXT_1 = ( + "\n\n\n" + "You are repeating the exact same tool call with identical parameters." + " Please carefully analyze the previous result. If the task is not yet complete," + " try a different method or parameters instead of repeating the same call." + "\n" +) + +TOOL_USE_SKIPPED_REASONS = frozenset({"dedup", "policy", "interrupt", "concurrent_inflight"}) + + +def _make_reminder_text_2(tool_name: str, repeat_count: int, canonical_args: str) -> str: + # Echo only a bounded preview of the arguments: large-payload tools + # (WriteFile, MultiEdit) would otherwise re-inject the whole body into + # context on every repeat — defeating the reminder by inflating tokens. + # Exact identity is preserved by the args_hash in the dedup telemetry. + args_limit = 256 + if len(canonical_args) > args_limit: + dropped = len(canonical_args) - args_limit + args_preview = f"{canonical_args[:args_limit]}... [truncated {dropped} chars]" + else: + args_preview = canonical_args + return ( + "\n\n\n" + "You have repeatedly called the same tool with identical parameters many times.\n" + "Repeated tool call detected:\n" + f"- tool: {tool_name}\n" + f"- repeated_times: {repeat_count}\n" + f"- arguments: {args_preview}\n" + "The previous repeated calls did not make progress. Do not call this exact same tool " + "with the exact same arguments again.\n" + "Carefully inspect the latest tool result and choose a different next action, " + "different parameters, or finish the task if enough evidence has been gathered." + "\n" + ) + + +def _sort_json_value(value: object) -> object: + if isinstance(value, list): + return [_sort_json_value(item) for item in cast("list[object]", value)] + if isinstance(value, dict): + value_dict = cast("dict[str, object]", value) + return {key: _sort_json_value(value_dict[key]) for key in sorted(value_dict)} + return value + + +def _canonical_tool_arguments(arguments: Any) -> str: + try: + return json.dumps( + _sort_json_value(arguments), + ensure_ascii=False, + separators=(",", ":"), + ) + except (TypeError, ValueError): + return str(arguments) + + +def _canonical_tool_arguments_text(arguments: str) -> str: + try: + return _canonical_tool_arguments(json.loads(arguments, strict=False)) + except json.JSONDecodeError: + return arguments + + +def _normalize_call_key(tool_name: str, arguments: str) -> ToolCallKey: + return (tool_name, _canonical_tool_arguments_text(arguments)) + + +def _append_reminder_to_return_value(return_value: Any, reminder_text: str) -> Any: + """Append dedup reminder text to a ToolReturnValue output.""" + if not isinstance(return_value, ToolReturnValue): + return return_value + + output = return_value.output + + if isinstance(output, str): + new_output: str | list[ContentPart] = output + reminder_text + else: + new_output = list(output) + if new_output and isinstance(new_output[-1], TextPart): + new_output[-1] = TextPart(text=new_output[-1].text + reminder_text) + else: + new_output.append(TextPart(text=reminder_text)) + + return return_value.model_copy(update={"output": new_output}) + + +def _emit_tool_use_skipped_if_opted_in( + tool: ToolType, + *, + tool_call_id: str, + tool_name: str, + reason: Literal["dedup", "policy", "interrupt", "concurrent_inflight"], + resumed: bool = False, +) -> None: + if not getattr(tool, "emits_tool_use_skipped", False): + return + _emit_tool_use_skipped( + tool_call_id=tool_call_id, + tool_name=tool_name, + reason=reason, + resumed=resumed, + ) + + +TOOL_CANCELLATION_TIMEOUT_SECONDS = 5.0 + +_DEFAULT_MAX_CONCURRENT_READERS = 10 +"""Cap on concurrent parallel-safe tool calls. A turn that fans out many readers +(e.g. dozens of FetchURL) overlaps freely up to this bound rather than opening an +unbounded number of sockets/file handles at once.""" + + +class ReadWriteGate: + """Async reader-writer gate for same-step parallel tool calls. + + Parallel-safe tools (readers) overlap freely up to ``max_concurrent_readers``; + a mutating tool (writer) waits for in-flight readers to drain and excludes + everything while it runs. Writers hold the lock while draining, which also + blocks new readers behind a queued writer — dispatch order stays deterministic + and writers cannot starve. Unflagged/plugin-style tools default to the + exclusive writer path unless they explicitly declare ``supports_parallel=True``. + """ + + def __init__(self, max_concurrent_readers: int = _DEFAULT_MAX_CONCURRENT_READERS) -> None: + self._writer_lock = asyncio.Lock() + self._active_readers = 0 + self._readers_drained = asyncio.Event() + self._readers_drained.set() + self._reader_slots = asyncio.Semaphore(max_concurrent_readers) + + @contextlib.asynccontextmanager + async def shared(self) -> AsyncGenerator[None]: + # Only tools that opted into ``supports_parallel=True`` should enter this + # shared path; unflagged/plugin adapters stay exclusive by default. + # Cap concurrent readers. Acquire the slot BEFORE the writer lock / counter + # bump: a reader still queued here has not incremented _active_readers, so it + # never holds _readers_drained open, and writers (which never touch the + # semaphore) cannot be starved — keeping the cap deadlock-safe. + await self._reader_slots.acquire() + try: + async with self._writer_lock: + self._active_readers += 1 + self._readers_drained.clear() + try: + yield + finally: + self._active_readers -= 1 + if self._active_readers == 0: + self._readers_drained.set() + finally: + self._reader_slots.release() + + @contextlib.asynccontextmanager + async def exclusive(self) -> AsyncGenerator[None]: + async with self._writer_lock: + await self._readers_drained.wait() + yield + + +@dataclass(frozen=True, slots=True) +class _PreparedToolCall: + tool_call: ToolCall + tool: ToolType | None = None + arguments: JsonType | None = None + canonical_args: str = "" + immediate_result: ToolResult | None = None + + +class ToolExecutionEngine: + """Private execution state machine behind :class:`PythinkerToolset`. + + Registry, visibility, and MCP ownership stay in the facade. This engine owns call + preparation, dedup state, concurrency scheduling, lifecycle hooks, and batch supervision. + """ + + def __init__( + self, + runtime: Runtime | None, + resolve_tool: Callable[[str], ToolType | None], + available_tool_names: Callable[[], list[str]], + get_hook_engine: Callable[[], HookEngine], + execute_tool: Callable[[ToolType, JsonType], Awaitable[ToolReturnValue]], + ) -> None: + self._runtime = runtime + self._resolve_tool = resolve_tool + self._available_tool_names = available_tool_names + self._get_hook_engine = get_hook_engine + self._execute_tool = execute_tool + self._concurrency_gate = ReadWriteGate() + self._previous_step_calls: list[ToolCallKey] = [] + self._current_step_calls: list[ToolCallKey] = [] + self._current_step_tasks: dict[ToolCallKey, asyncio.Task[ToolResult]] = {} + self._seen_call_keys: set[ToolCallKey] = set() + self._consecutive_key: ToolCallKey | None = None + self._consecutive_count = 0 + self._step_closed = True + self._step_started = False + self._dedup_triggered = False + self._step_no = 0 + self._turn_id = "" + self._poisoned_batches: set[_ExecutionBatch] = set() + self._late_drain_tasks: set[asyncio.Task[None]] = set() + + @property + def poisoned(self) -> bool: + return bool(self._poisoned_batches) + + def _ensure_healthy(self) -> None: + if self.poisoned: + raise ToolCancellationTimeoutError( + "Tool execution is unavailable while timed-out cancellation finishes" + ) + + def register_cancellation_timeout(self, batch: _ExecutionBatch) -> None: + if batch in self._poisoned_batches: + return + self._poisoned_batches.add(batch) + + async def drain_late_batch() -> None: + try: + await batch.wait_until_drained() + finally: + self._poisoned_batches.discard(batch) + logger.info("Timed-out tool cancellation drained; tool execution recovered") + + drain_task = asyncio.create_task(drain_late_batch()) + self._late_drain_tasks.add(drain_task) + drain_task.add_done_callback(self._late_drain_tasks.discard) + + async def cleanup(self, *, timeout: float | None = None) -> None: + """Wait a bounded interval for retained late-cancellation observers.""" + effective_timeout = TOOL_CANCELLATION_TIMEOUT_SECONDS if timeout is None else timeout + if not math.isfinite(effective_timeout) or effective_timeout < 0: + raise ValueError("tool cleanup timeout must be finite and non-negative") + + drain_tasks = list(self._late_drain_tasks) + if not drain_tasks: + return + + done, pending = await asyncio.wait(drain_tasks, timeout=effective_timeout) + for task in done: + task.result() + if pending: + raise ToolCancellationTimeoutError( + f"Tool execution cleanup did not settle within {effective_timeout:g} seconds" + ) + + def begin_step( + self, + previous_calls: Sequence[ToolCallKey], + *, + step_no: int = 0, + turn_id: str = "", + ) -> None: + self._previous_step_calls = [ + _normalize_call_key(tool_name, arguments) for tool_name, arguments in previous_calls + ] + self._current_step_calls = [] + self._current_step_tasks = {} + self._step_closed = False + self._step_started = True + self._dedup_triggered = False + self._step_no = step_no + self._turn_id = turn_id + if not self._previous_step_calls: + self._seen_call_keys = set() + self._consecutive_key = None + self._consecutive_count = 0 + else: + self._seen_call_keys.update(self._previous_step_calls) + if self._consecutive_key is None and self._consecutive_count == 0: + self._advance_consecutive_streak(self._previous_step_calls) + + def end_step(self) -> list[ToolCallKey]: + if not self._step_closed: + self._advance_consecutive_streak(self._current_step_calls) + self._seen_call_keys.update(self._current_step_calls) + self._step_closed = True + return list(self._current_step_calls) + + def abort_step(self) -> None: + """Discard uncommitted state for the current execution step.""" + if self._step_closed: + return + self._current_step_calls = [] + self._current_step_tasks = {} + self._dedup_triggered = False + self._step_closed = True + + def _advance_consecutive_streak(self, calls: Sequence[ToolCallKey]) -> None: + for call_key in calls: + if call_key == self._consecutive_key: + self._consecutive_count += 1 + else: + self._consecutive_key = call_key + self._consecutive_count = 1 + + def _projected_streak_for_call(self, call_index: int) -> int: + consecutive_key = self._consecutive_key + consecutive_count = self._consecutive_count + for call_key in self._current_step_calls[: call_index + 1]: + if call_key == consecutive_key: + consecutive_count += 1 + else: + consecutive_key = call_key + consecutive_count = 1 + return consecutive_count + + @property + def dedup_triggered(self) -> bool: + return self._dedup_triggered + + @property + def consecutive_repeat_count(self) -> int: + return self._consecutive_count + + @property + def summary(self) -> ToolBatchSummary: + return ToolBatchSummary( + current_call_fingerprints=tuple(self._current_step_calls), + dedup_triggered=self._dedup_triggered, + consecutive_identical_call_count=self._consecutive_count, + finalized=self._step_closed, + ) + + async def gated_call(self, tool: ToolType, arguments: JsonType) -> ToolReturnValue: + if getattr(tool, "supports_parallel", False): + async with self._concurrency_gate.shared(): + return await tool.call(arguments) + async with self._concurrency_gate.exclusive(): + return await tool.call(arguments) + + def prepare(self, tool_call: ToolCall) -> _PreparedToolCall: + tool_name = tool_call.function.name + tool = self._resolve_tool(tool_name) + if tool is None: + matches = difflib.get_close_matches( + tool_name, + self._available_tool_names(), + n=1, + cutoff=0.6, + ) + return _PreparedToolCall( + tool_call=tool_call, + immediate_result=ToolResult( + tool_call_id=tool_call.id, + return_value=ToolNotFoundError( + tool_name, + suggestion=matches[0] if matches else None, + ), + ), + ) + + if tool_name == "ToolSearch" and self._runtime is not None: + from pythinker_code.llm import supports_deferred_tool_search + + if not supports_deferred_tool_search(self._runtime.llm): + return _PreparedToolCall( + tool_call=tool_call, + immediate_result=ToolResult( + tool_call_id=tool_call.id, + return_value=ToolNotFoundError(tool_name), + ), + ) + + try: + arguments: JsonType = json.loads(tool_call.function.arguments or "{}", strict=False) + except json.JSONDecodeError as error: + logger.warning( + "Tool call JSON parse error: {tool_name} (call_id={call_id}): {error}", + tool_name=tool_name, + call_id=tool_call.id, + error=error, + ) + return _PreparedToolCall( + tool_call=tool_call, + immediate_result=ToolResult( + tool_call_id=tool_call.id, + return_value=ToolParseError(str(error)), + ), + ) + + return _PreparedToolCall( + tool_call=tool_call, + tool=tool, + arguments=arguments, + canonical_args=_canonical_tool_arguments(arguments), + ) + + def prepare_batch(self, tool_calls: Sequence[ToolCall]) -> tuple[_PreparedToolCall, ...]: + return tuple(self.prepare(tool_call) for tool_call in tool_calls) + + def handle(self, tool_call: ToolCall) -> HandleResult: + self._ensure_healthy() + if not self._step_started or self._step_closed: + self.begin_step(()) + return self.dispatch(self.prepare(tool_call)) + + def handle_batch( + self, + tool_calls: Sequence[ToolCall], + context: ToolBatchContext, + *, + on_tool_result: Callable[[ToolResult], None] | None = None, + ) -> ToolBatchHandle: + self._ensure_healthy() + prepared = self.prepare_batch(tool_calls) + if not self._step_started or self._step_closed: + self.begin_step( + context.prior_call_fingerprints, + step_no=context.step_no, + turn_id=context.turn_id, + ) + return _ExecutionBatch(self, prepared, on_tool_result) + + def dispatch(self, prepared: _PreparedToolCall) -> HandleResult: + tool_call = prepared.tool_call + if prepared.immediate_result is not None: + return prepared.immediate_result + if prepared.tool is None or prepared.arguments is None: + raise RuntimeError("prepared tool call is missing execution data") + + token = current_tool_call.set(tool_call) + try: + tool = prepared.tool + arguments = prepared.arguments + canonical_args = prepared.canonical_args + call_key = (tool_call.function.name, canonical_args) + call_index = len(self._current_step_calls) + self._current_step_calls.append(call_key) + + if call_key in self._current_step_tasks: + from pythinker_code.telemetry import track + + _emit_tool_use_skipped_if_opted_in( + tool, + tool_call_id=tool_call.id, + tool_name=tool_call.function.name, + reason="dedup", + resumed=True, + ) + track( + "tool_call_dedup_detected", + turn_id=self._turn_id, + step_no=self._step_no, + tool_name=tool_call.function.name, + dup_type="same_step", + args_hash=hashlib.sha256(canonical_args.encode("utf-8")).hexdigest()[:8], + ) + original_task = self._current_step_tasks[call_key] + + async def await_duplicate() -> ToolResult: + original_result = await original_task + return ToolResult( + tool_call_id=tool_call.id, + return_value=original_result.return_value, + ) + + return asyncio.create_task(await_duplicate()) + + is_cross_step_dup = call_key in self._seen_call_keys + reminder_text: str | None = None + if is_cross_step_dup: + from pythinker_code.telemetry import track + + track( + "tool_call_dedup_detected", + turn_id=self._turn_id, + step_no=self._step_no, + tool_name=tool_call.function.name, + dup_type="cross_step", + args_hash=hashlib.sha256(canonical_args.encode("utf-8")).hexdigest()[:8], + ) + self._dedup_triggered = True + repeat_count = self._projected_streak_for_call(call_index) + if repeat_count == 3: + reminder_text = _REMINDER_TEXT_1 + elif repeat_count in (5, 8): + reminder_text = _make_reminder_text_2( + tool_call.function.name, + repeat_count, + canonical_args, + ) + if reminder_text is not None: + _emit_tool_use_skipped_if_opted_in( + tool, + tool_call_id=tool_call.id, + tool_name=tool_call.function.name, + reason="dedup", + resumed=False, + ) + + async def call_tool() -> ToolResult: + started_ids_token = _current_tool_execution_started_ids.set(set[str]()) + try: + return await call_with_lifecycle() + finally: + _current_tool_execution_started_ids.reset(started_ids_token) + + async def call_with_lifecycle() -> ToolResult: + tool_input_dict = copy.deepcopy(arguments) if isinstance(arguments, dict) else {} + + if self._runtime is not None: + from pythinker_code.soul.permission import check_tool_call_allowed + + if error := check_tool_call_allowed( + self._runtime, + tool_call.function.name, + tool_input_dict, + tool=tool, + ): + _emit_tool_use_skipped_if_opted_in( + tool, + tool_call_id=tool_call.id, + tool_name=tool_call.function.name, + reason="policy", + ) + return ToolResult(tool_call_id=tool_call.id, return_value=error) + + from pythinker_code.hooks import events + + hook_engine = self._get_hook_engine() + hook_results = await hook_engine.trigger( + "PreToolUse", + matcher_value=tool_call.function.name, + input_data=events.pre_tool_use( + session_id=_get_session_id(), + cwd=str(Path.cwd()), + tool_name=tool_call.function.name, + tool_input=copy.deepcopy(tool_input_dict), + tool_call_id=tool_call.id, + ), + ) + for hook_result in hook_results: + if hook_result.action == "block": + _emit_tool_use_skipped_if_opted_in( + tool, + tool_call_id=tool_call.id, + tool_name=tool_call.function.name, + reason="policy", + ) + return ToolResult( + tool_call_id=tool_call.id, + return_value=ToolError( + message=hook_result.reason or "Blocked by PreToolUse hook", + brief="Hook blocked", + ), + ) + + from pythinker_code.telemetry import metrics, otel + + if not tool_defers_execution_started(tool): + emit_current_tool_execution_started() + + started_at = time.monotonic() + telemetry_tool_name = sanitize_telemetry_tool_name(tool_call.function.name) + span_context = otel.start_span( + "pythinker.tool", + { + "tool.name": telemetry_tool_name, + "tool.call_id": tool_call.id, + "gen_ai.operation.name": "execute_tool", + "gen_ai.tool.name": telemetry_tool_name, + }, + ) + span = span_context.__enter__() + try: + return_value = await self._execute_tool(tool, copy.deepcopy(arguments)) + except Exception as error: + elapsed = time.monotonic() - started_at + span.set_attribute("tool.success", False) + span.set_attribute("tool.error_type", type(error).__name__) + span.set_attribute("tool.duration_ms", int(elapsed * 1000)) + span_context.__exit__(type(error), error, error.__traceback__) + metrics.record_tool_call( + tool_name=telemetry_tool_name, + duration_seconds=elapsed, + success=False, + error_type=type(error).__name__, + ) + metrics.record_error(kind="tool_error", error_type=type(error).__name__) + logger.exception( + "Tool execution failed: {tool_name} (call_id={call_id})", + tool_name=tool_call.function.name, + call_id=tool_call.id, + ) + hook_engine.fire_and_forget_trigger( + "PostToolUseFailure", + matcher_value=tool_call.function.name, + input_data=events.post_tool_use_failure( + session_id=_get_session_id(), + cwd=str(Path.cwd()), + tool_name=tool_call.function.name, + tool_input=copy.deepcopy(tool_input_dict), + error=str(error), + tool_call_id=tool_call.id, + ), + ) + from pythinker_code.telemetry import track + + error_type = type(error).__name__ + track("tool_error", tool_name=telemetry_tool_name, error_type=error_type) + track( + "tool_call", + tool_name=telemetry_tool_name, + success=False, + duration_ms=int(elapsed * 1000), + error_type=error_type, + dup_type="cross_step" if is_cross_step_dup else "normal", + ) + return ToolResult( + tool_call_id=tool_call.id, + return_value=ToolRuntimeError(str(error)), + ) + except BaseException as error: + span_context.__exit__(type(error), error, error.__traceback__) + raise + + elapsed = time.monotonic() - started_at + succeeded = not isinstance(return_value, ToolError) + span.set_attribute("tool.success", succeeded) + if isinstance(return_value, ToolError): + span.set_attribute("tool.error_brief", return_value.brief or "") + span.set_attribute("tool.duration_ms", int(elapsed * 1000)) + span_context.__exit__(None, None, None) + metrics.record_tool_call( + tool_name=telemetry_tool_name, + duration_seconds=elapsed, + success=succeeded, + ) + logger.info( + "Tool {tool_name} completed in {elapsed:.1f}s (call_id={call_id})", + tool_name=tool_call.function.name, + elapsed=elapsed, + call_id=tool_call.id, + ) + from pythinker_code.telemetry import track + + track( + "tool_call", + tool_name=telemetry_tool_name, + success=succeeded, + duration_ms=int(elapsed * 1000), + dup_type="cross_step" if is_cross_step_dup else "normal", + ) + hook_engine.fire_and_forget_trigger( + "PostToolUse", + matcher_value=tool_call.function.name, + input_data=events.post_tool_use( + session_id=_get_session_id(), + cwd=str(Path.cwd()), + tool_name=tool_call.function.name, + tool_input=copy.deepcopy(tool_input_dict), + tool_output=str(return_value)[:2000], + tool_call_id=tool_call.id, + ), + ) + if reminder_text is not None: + return ToolResult( + tool_call_id=tool_call.id, + return_value=_append_reminder_to_return_value( + return_value, + reminder_text, + ), + ) + return ToolResult(tool_call_id=tool_call.id, return_value=return_value) + + task = asyncio.create_task(call_tool()) + self._current_step_tasks[call_key] = task + return task + finally: + current_tool_call.reset(token) + + +class _ExecutionBatch: + def __init__( + self, + engine: ToolExecutionEngine, + prepared_calls: Sequence[_PreparedToolCall], + on_tool_result: Callable[[ToolResult], None] | None, + ) -> None: + self._engine = engine + self._prepared_calls = tuple(prepared_calls) + self._on_tool_result = on_tool_result + self._completed_results: dict[str, ToolResult] = {} + self._source_futures: list[ToolResultFuture] = [] + self._watcher_tasks: list[asyncio.Task[ToolResult]] = [] + self._summary = ToolBatchSummary(finalized=False) + self._callbacks_active = True + self._settlement_task: asyncio.Task[None] | None = None + self._supervisor = asyncio.create_task(self._run()) + self._supervisor.add_done_callback(self._consume_supervisor_failure) + + @property + def tool_calls(self) -> Sequence[ToolCall]: + return tuple(prepared.tool_call for prepared in self._prepared_calls) + + @property + def completed_results(self) -> Mapping[str, ToolResult]: + return dict(self._completed_results) + + @property + def summary(self) -> ToolBatchSummary: + return self._summary + + @staticmethod + def _consume_supervisor_failure(supervisor: asyncio.Task[list[ToolResult]]) -> None: + try: + supervisor.exception() + except asyncio.CancelledError: + return + + async def _watch(self, future: ToolResultFuture) -> ToolResult: + result = await future + self._completed_results[result.tool_call_id] = result + if self._callbacks_active and self._on_tool_result is not None: + self._on_tool_result(result) + return result + + async def _run(self) -> list[ToolResult]: + try: + for prepared in self._prepared_calls: + handled = self._engine.dispatch(prepared) + if isinstance(handled, ToolResult): + future = ToolResultFuture() + future.set_result(handled) + else: + future = handled + self._source_futures.append(future) + + self._watcher_tasks = [ + asyncio.create_task(self._watch(future)) for future in self._source_futures + ] + results = list(await asyncio.gather(*self._watcher_tasks)) + self._engine.end_step() + self._summary = self._engine.summary + return results + except BaseException: + self._callbacks_active = False + for index, future in enumerate(self._source_futures): + if future.done() and not future.cancelled() and future.exception() is None: + result = future.result() + self._completed_results[result.tool_call_id] = result + elif not future.done() and index < len(self._watcher_tasks): + self._watcher_tasks[index].cancel() + for future in self._source_futures: + future.cancel() + await asyncio.gather(*self._watcher_tasks, return_exceptions=True) + await asyncio.gather(*self._source_futures, return_exceptions=True) + self._engine.abort_step() + raise + + async def results(self) -> list[ToolResult]: + return await asyncio.shield(self._supervisor) + + async def _wait_for_supervisor(self) -> None: + try: + await asyncio.shield(self._supervisor) + except asyncio.CancelledError: + if not self._supervisor.done(): + raise + await asyncio.gather(self._supervisor, return_exceptions=True) + except Exception: + await asyncio.gather(self._supervisor, return_exceptions=True) + + async def wait_until_drained(self) -> None: + await asyncio.gather(self._supervisor, return_exceptions=True) + + async def _bounded_settlement(self, timeout: float) -> None: + self._callbacks_active = False + if not self._supervisor.done() and self._supervisor.cancelling() == 0: + self._supervisor.cancel() + if self._supervisor.done(): + await self._wait_for_supervisor() + return + try: + await asyncio.wait_for(self._wait_for_supervisor(), timeout=timeout) + except TimeoutError as error: + self._engine.register_cancellation_timeout(self) + logger.error( + "Tool cancellation timed out after {timeout:g}s; pausing new tool batches until " + "late work drains", + timeout=timeout, + ) + raise ToolCancellationTimeoutError( + f"Tool execution cancellation did not settle within {timeout:g} seconds" + ) from error + + async def cancel_and_settle(self, *, timeout: float | None = None) -> None: + effective_timeout = TOOL_CANCELLATION_TIMEOUT_SECONDS if timeout is None else timeout + if not math.isfinite(effective_timeout) or effective_timeout < 0: + raise ValueError("tool cancellation timeout must be finite and non-negative") + + settlement = self._settlement_task + if settlement is None: + settlement = asyncio.create_task(self._bounded_settlement(effective_timeout)) + settlement.add_done_callback(self._consume_settlement_failure) + self._settlement_task = settlement + await asyncio.shield(settlement) + + @staticmethod + def _consume_settlement_failure(settlement: asyncio.Task[None]) -> None: + try: + settlement.exception() + except asyncio.CancelledError: + return diff --git a/src/pythinker_code/soul/toolset.py b/src/pythinker_code/soul/toolset.py index bfb8bd77..6d05b633 100644 --- a/src/pythinker_code/soul/toolset.py +++ b/src/pythinker_code/soul/toolset.py @@ -2,16 +2,11 @@ import asyncio import contextlib -import copy import difflib -import hashlib import importlib import inspect -import json import re -import time -from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable -from contextvars import ContextVar +from collections.abc import Awaitable, Callable, Iterable, Sequence from dataclasses import dataclass from dataclasses import replace as dataclass_replace from datetime import timedelta @@ -20,25 +15,30 @@ from pythinker_core.tooling import ( CallableTool, - CallableTool2, HandleResult, Tool, + ToolBatchContext, + ToolBatchHandle, + ToolCancellationTimeoutError, ToolError, ToolOk, Toolset, ) -from pythinker_core.tooling.error import ( - ToolNotFoundError, - ToolParseError, - ToolRuntimeError, -) from pythinker_core.tooling.mcp import convert_mcp_content from pythinker_core.utils.typing import JsonType from pythinker_host.path import HostPath from pythinker_code.exception import InvalidToolError, MCPRuntimeError from pythinker_code.hooks.engine import HookEngine -from pythinker_code.telemetry.names import sanitize_telemetry_tool_name +from pythinker_code.soul import tool_execution as _tool_execution +from pythinker_code.soul.tool_execution import ( + ReadWriteGate, + ToolCallKey, + ToolExecutionEngine, + ToolType, + get_current_tool_call_or_none, + tool_defers_execution_started, +) from pythinker_code.tools import SkipThisTool from pythinker_code.utils.logging import logger from pythinker_code.wire.types import ( @@ -50,10 +50,8 @@ TextPart, ToolCall, ToolCallRequest, - ToolExecutionStarted, ToolResult, ToolReturnValue, - ToolUseSkipped, VideoURLPart, ) @@ -66,99 +64,20 @@ from pythinker_code.soul.agent import Runtime -current_tool_call = ContextVar[ToolCall | None]("current_tool_call", default=None) -_current_tool_execution_started_ids: ContextVar[set[str] | None] = ContextVar( - "current_tool_execution_started_ids", default=None -) +current_tool_call = _tool_execution.current_tool_call +emit_current_tool_execution_started = _tool_execution.emit_current_tool_execution_started +get_session_id = _tool_execution.get_session_id +set_session_id = _tool_execution.set_session_id +_ReadWriteGate = ReadWriteGate +_tool_defers_execution_started = tool_defers_execution_started # Per-server timeout for closing MCP clients during teardown, so one hung client # cannot block cleanup of the rest (mcpext-3). _MCP_CLOSE_TIMEOUT_S = 5.0 -_current_session_id: ContextVar[str] = ContextVar("_current_session_id", default="") _MCP_LOG_NAME_RE = re.compile(r"[^A-Za-z0-9_.-]+") -def set_session_id(sid: str) -> None: - _current_session_id.set(sid) - - -def get_session_id() -> str: - return _current_session_id.get() - - -def _get_session_id() -> str: - return _current_session_id.get() - - -def get_current_tool_call_or_none() -> ToolCall | None: - """ - Get the current tool call or None. - Expect to be not None when called from a `__call__` method of a tool. - """ - return current_tool_call.get() - - -def emit_current_tool_execution_started() -> None: - """Emit ToolExecutionStarted once for the current tool call, if wire is active.""" - tool_call = get_current_tool_call_or_none() - if tool_call is None: - return - - started_ids = _current_tool_execution_started_ids.get() - if started_ids is None: - started_ids = set[str]() - _current_tool_execution_started_ids.set(started_ids) - if tool_call.id in started_ids: - return - started_ids.add(tool_call.id) - - try: - from pythinker_code.soul import get_wire_or_none - - if wire := get_wire_or_none(): - wire.soul_side.send(ToolExecutionStarted(tool_call_id=tool_call.id)) - except Exception as exc: # noqa: BLE001 - lifecycle events must not break tool execution - logger.debug( - "Failed to emit tool execution start: {tool_name} (call_id={call_id}): {error}", - tool_name=tool_call.function.name, - call_id=tool_call.id, - error=exc, - ) - - -def _emit_tool_use_skipped( - *, - tool_call_id: str, - tool_name: str, - reason: Literal["dedup", "policy", "interrupt", "concurrent_inflight"], - resumed: bool = False, -) -> None: - try: - from pythinker_code.soul import get_wire_or_none - - if wire := get_wire_or_none(): - wire.soul_side.send( - ToolUseSkipped( - tool_call_id=tool_call_id, - tool_name=tool_name, - reason=reason, - resumed=resumed, - ) - ) - except Exception as exc: # noqa: BLE001 - observability must not break tool execution - logger.debug( - "Failed to emit tool skipped event: {tool_name} (call_id={call_id}): {error}", - tool_name=tool_name, - call_id=tool_call_id, - error=exc, - ) - - -def _tool_defers_execution_started(tool: ToolType) -> bool: - return bool(getattr(tool, "emits_tool_execution_started_after_approval", False)) - - def _is_external_side_effect_tool(tool: ToolType) -> bool: """Return True for tool adapters whose side effects are not statically classified. @@ -367,174 +286,12 @@ def allows(self, tool_name: str) -> bool: return self.enabled is None or tool_name in self.enabled -type ToolType = CallableTool | CallableTool2[Any] -type ToolCallKey = tuple[str, str] - - if TYPE_CHECKING: def type_check(pythinker_toolset: PythinkerToolset): _: Toolset = pythinker_toolset -_REMINDER_TEXT_1 = ( - "\n\n\n" - "You are repeating the exact same tool call with identical parameters." - " Please carefully analyze the previous result. If the task is not yet complete," - " try a different method or parameters instead of repeating the same call." - "\n" -) - -TOOL_USE_SKIPPED_REASONS = frozenset({"dedup", "policy", "interrupt", "concurrent_inflight"}) - - -def _make_reminder_text_2(tool_name: str, repeat_count: int, canonical_args: str) -> str: - # Echo only a bounded preview of the arguments: large-payload tools - # (WriteFile, MultiEdit) would otherwise re-inject the whole body into - # context on every repeat — defeating the reminder by inflating tokens. - # Exact identity is preserved by the args_hash in the dedup telemetry. - args_limit = 256 - if len(canonical_args) > args_limit: - dropped = len(canonical_args) - args_limit - args_preview = f"{canonical_args[:args_limit]}... [truncated {dropped} chars]" - else: - args_preview = canonical_args - return ( - "\n\n\n" - "You have repeatedly called the same tool with identical parameters many times.\n" - "Repeated tool call detected:\n" - f"- tool: {tool_name}\n" - f"- repeated_times: {repeat_count}\n" - f"- arguments: {args_preview}\n" - "The previous repeated calls did not make progress. Do not call this exact same tool " - "with the exact same arguments again.\n" - "Carefully inspect the latest tool result and choose a different next action, " - "different parameters, or finish the task if enough evidence has been gathered." - "\n" - ) - - -def _sort_json_value(value: object) -> object: - if isinstance(value, list): - return [_sort_json_value(item) for item in cast("list[object]", value)] - if isinstance(value, dict): - value_dict = cast("dict[str, object]", value) - return {key: _sort_json_value(value_dict[key]) for key in sorted(value_dict)} - return value - - -def _canonical_tool_arguments(arguments: Any) -> str: - try: - return json.dumps( - _sort_json_value(arguments), - ensure_ascii=False, - separators=(",", ":"), - ) - except (TypeError, ValueError): - return str(arguments) - - -def _canonical_tool_arguments_text(arguments: str) -> str: - try: - return _canonical_tool_arguments(json.loads(arguments, strict=False)) - except json.JSONDecodeError: - return arguments - - -def _normalize_call_key(tool_name: str, arguments: str) -> ToolCallKey: - return (tool_name, _canonical_tool_arguments_text(arguments)) - - -def _append_reminder_to_return_value(return_value: Any, reminder_text: str) -> Any: - """Append dedup reminder text to a ToolReturnValue output.""" - if not isinstance(return_value, ToolReturnValue): - return return_value - - output = return_value.output - - if isinstance(output, str): - new_output: str | list[ContentPart] = output + reminder_text - else: - new_output = list(output) - if new_output and isinstance(new_output[-1], TextPart): - new_output[-1] = TextPart(text=new_output[-1].text + reminder_text) - else: - new_output.append(TextPart(text=reminder_text)) - - return return_value.model_copy(update={"output": new_output}) - - -def _emit_tool_use_skipped_if_opted_in( - tool: ToolType, - *, - tool_call_id: str, - tool_name: str, - reason: Literal["dedup", "policy", "interrupt", "concurrent_inflight"], - resumed: bool = False, -) -> None: - if not getattr(tool, "emits_tool_use_skipped", False): - return - _emit_tool_use_skipped( - tool_call_id=tool_call_id, - tool_name=tool_name, - reason=reason, - resumed=resumed, - ) - - -_DEFAULT_MAX_CONCURRENT_READERS = 10 -"""Cap on concurrent parallel-safe tool calls. A turn that fans out many readers -(e.g. dozens of FetchURL) overlaps freely up to this bound rather than opening an -unbounded number of sockets/file handles at once.""" - - -class _ReadWriteGate: - """Async reader-writer gate for same-step parallel tool calls. - - Parallel-safe tools (readers) overlap freely up to ``max_concurrent_readers``; - a mutating tool (writer) waits for in-flight readers to drain and excludes - everything while it runs. Writers hold the lock while draining, which also - blocks new readers behind a queued writer — dispatch order stays deterministic - and writers cannot starve. Unflagged/plugin-style tools default to the - exclusive writer path unless they explicitly declare ``supports_parallel=True``. - """ - - def __init__(self, max_concurrent_readers: int = _DEFAULT_MAX_CONCURRENT_READERS) -> None: - self._writer_lock = asyncio.Lock() - self._active_readers = 0 - self._readers_drained = asyncio.Event() - self._readers_drained.set() - self._reader_slots = asyncio.Semaphore(max_concurrent_readers) - - @contextlib.asynccontextmanager - async def shared(self) -> AsyncGenerator[None]: - # Only tools that opted into ``supports_parallel=True`` should enter this - # shared path; unflagged/plugin adapters stay exclusive by default. - # Cap concurrent readers. Acquire the slot BEFORE the writer lock / counter - # bump: a reader still queued here has not incremented _active_readers, so it - # never holds _readers_drained open, and writers (which never touch the - # semaphore) cannot be starved — keeping the cap deadlock-safe. - await self._reader_slots.acquire() - try: - async with self._writer_lock: - self._active_readers += 1 - self._readers_drained.clear() - try: - yield - finally: - self._active_readers -= 1 - if self._active_readers == 0: - self._readers_drained.set() - finally: - self._reader_slots.release() - - @contextlib.asynccontextmanager - async def exclusive(self) -> AsyncGenerator[None]: - async with self._writer_lock: - await self._readers_drained.wait() - yield - - class PythinkerToolset: def __init__(self, runtime: Runtime | None = None) -> None: self._runtime = runtime @@ -544,19 +301,14 @@ def __init__(self, runtime: Runtime | None = None) -> None: self._mcp_loading_task: asyncio.Task[None] | None = None self._deferred_mcp_load: tuple[list[MCPConfig], Runtime] | None = None self._hook_engine: HookEngine = HookEngine() - self._concurrency_gate = _ReadWriteGate() - - # Deduplication state - self._previous_step_calls: list[ToolCallKey] = [] - self._current_step_calls: list[ToolCallKey] = [] - self._current_step_tasks: dict[ToolCallKey, asyncio.Task[ToolResult]] = {} - self._seen_call_keys: set[ToolCallKey] = set() - self._consecutive_key: ToolCallKey | None = None - self._consecutive_count: int = 0 - self._step_closed: bool = False - self._dedup_triggered: bool = False - self._step_no: int = 0 - self._turn_id: str = "" + + self._execution = ToolExecutionEngine( + runtime, + lambda name: self._tool_dict.get(name), + lambda: list(self._tool_dict), + lambda: self._hook_engine, + lambda tool, arguments: self._gated_call(tool, arguments), + ) def set_hook_engine(self, engine: HookEngine) -> None: self._hook_engine = engine @@ -756,18 +508,13 @@ def _is_tool_visible(self, tool: ToolType) -> bool: return True - async def _gated_call(self, tool: ToolType, arguments: JsonType) -> ToolReturnValue: - """Execute under the same-step concurrency policy. + @property + def _concurrency_gate(self) -> _ReadWriteGate: + """Compatibility seam for local characterization probes.""" + return self._execution._concurrency_gate # pyright: ignore[reportPrivateUsage] - Tools declaring ``supports_parallel`` share the gate; everything - else (including unflagged plugin/MCP tools — the safe default) - runs exclusively so same-step mutations stay ordered. - """ - if getattr(tool, "supports_parallel", False): - async with self._concurrency_gate.shared(): - return await tool.call(arguments) - async with self._concurrency_gate.exclusive(): - return await tool.call(arguments) + async def _gated_call(self, tool: ToolType, arguments: JsonType) -> ToolReturnValue: + return await self._execution.gated_call(tool, arguments) def begin_step( self, @@ -776,364 +523,35 @@ def begin_step( step_no: int = 0, turn_id: str = "", ) -> None: - """Called before each step to set up deduplication state.""" - self._previous_step_calls = [ - _normalize_call_key(tool_name, arguments) for tool_name, arguments in previous_calls - ] - self._current_step_calls = [] - self._current_step_tasks = {} - self._step_closed = False - self._dedup_triggered = False - self._step_no = step_no - self._turn_id = turn_id - if not self._previous_step_calls: - self._seen_call_keys = set() - self._consecutive_key = None - self._consecutive_count = 0 - else: - self._seen_call_keys.update(self._previous_step_calls) - if self._consecutive_key is None and self._consecutive_count == 0: - self._advance_consecutive_streak(self._previous_step_calls) + """Prepare execution state for one legacy caller step.""" + self._execution.begin_step(previous_calls, step_no=step_no, turn_id=turn_id) def end_step(self) -> list[ToolCallKey]: - """Called after each step to capture the calls made in this step.""" - if not self._step_closed: - self._advance_consecutive_streak(self._current_step_calls) - self._seen_call_keys.update(self._current_step_calls) - self._step_closed = True - return list(self._current_step_calls) - - def _advance_consecutive_streak(self, calls: list[ToolCallKey]) -> None: - for call_key in calls: - if call_key == self._consecutive_key: - self._consecutive_count += 1 - else: - self._consecutive_key = call_key - self._consecutive_count = 1 - - def _projected_streak_for_call(self, call_index: int) -> int: - consecutive_key = self._consecutive_key - consecutive_count = self._consecutive_count - for call_key in self._current_step_calls[: call_index + 1]: - if call_key == consecutive_key: - consecutive_count += 1 - else: - consecutive_key = call_key - consecutive_count = 1 - return consecutive_count + """Finalize and return the current step's normalized call fingerprints.""" + return self._execution.end_step() @property def dedup_triggered(self) -> bool: - """Whether a cross-step duplicate was blocked in the current step.""" - return self._dedup_triggered + return self._execution.dedup_triggered @property def consecutive_repeat_count(self) -> int: - """Length of the current streak of identical-argument tool calls. - - Tracked independently of each call's reported success/failure, so it - still catches a degenerate loop even if a tool falsely reports success - on a call that made no progress. - """ - return self._consecutive_count + return self._execution.consecutive_repeat_count def handle(self, tool_call: ToolCall) -> HandleResult: - token = current_tool_call.set(tool_call) - try: - if tool_call.function.name not in self._tool_dict: - available = list(self._tool_dict.keys()) - matches = difflib.get_close_matches( - tool_call.function.name, available, n=1, cutoff=0.6 - ) - return ToolResult( - tool_call_id=tool_call.id, - return_value=ToolNotFoundError( - tool_call.function.name, - suggestion=matches[0] if matches else None, - ), - ) - - tool = self._tool_dict[tool_call.function.name] - - if tool_call.function.name == "ToolSearch" and self._runtime is not None: - from pythinker_code.llm import supports_deferred_tool_search - - if not supports_deferred_tool_search(self._runtime.llm): - return ToolResult( - tool_call_id=tool_call.id, - return_value=ToolNotFoundError(tool_call.function.name), - ) - - try: - arguments: JsonType = json.loads(tool_call.function.arguments or "{}", strict=False) - except json.JSONDecodeError as e: - logger.warning( - "Tool call JSON parse error: {tool_name} (call_id={call_id}): {error}", - tool_name=tool_call.function.name, - call_id=tool_call.id, - error=e, - ) - return ToolResult(tool_call_id=tool_call.id, return_value=ToolParseError(str(e))) - - canonical_args = _canonical_tool_arguments(arguments) - call_key = (tool_call.function.name, canonical_args) - call_index = len(self._current_step_calls) - self._current_step_calls.append(call_key) - - # Same-step dedup: wait for the original task and copy its result. - if call_key in self._current_step_tasks: - from pythinker_code.telemetry import track - - _emit_tool_use_skipped_if_opted_in( - tool, - tool_call_id=tool_call.id, - tool_name=tool_call.function.name, - reason="dedup", - resumed=True, - ) - track( - "tool_call_dedup_detected", - turn_id=self._turn_id, - step_no=self._step_no, - tool_name=tool_call.function.name, - dup_type="same_step", - args_hash=hashlib.sha256(canonical_args.encode("utf-8")).hexdigest()[:8], - ) - original_task = self._current_step_tasks[call_key] - - async def _await_dup() -> ToolResult: - original_result = await original_task - return ToolResult( - tool_call_id=tool_call.id, - return_value=original_result.return_value, - ) + """Compatibility path for third-party per-call core dispatch.""" + return self._execution.handle(tool_call) - return asyncio.create_task(_await_dup()) - - is_cross_step_dup = call_key in self._seen_call_keys - reminder_text: str | None = None - if is_cross_step_dup: - from pythinker_code.telemetry import track - - track( - "tool_call_dedup_detected", - turn_id=self._turn_id, - step_no=self._step_no, - tool_name=tool_call.function.name, - dup_type="cross_step", - args_hash=hashlib.sha256(canonical_args.encode("utf-8")).hexdigest()[:8], - ) - self._dedup_triggered = True - repeat_count = self._projected_streak_for_call(call_index) - if repeat_count == 3: - reminder_text = _REMINDER_TEXT_1 - elif repeat_count in (5, 8): - reminder_text = _make_reminder_text_2( - tool_call.function.name, repeat_count, canonical_args - ) - if reminder_text is not None: - _emit_tool_use_skipped_if_opted_in( - tool, - tool_call_id=tool_call.id, - tool_name=tool_call.function.name, - reason="dedup", - resumed=False, - ) - - async def _call(): - started_ids_token = _current_tool_execution_started_ids.set(set[str]()) - try: - return await _call_with_lifecycle() - finally: - _current_tool_execution_started_ids.reset(started_ids_token) - - async def _call_with_lifecycle(): - tool_input_dict = copy.deepcopy(arguments) if isinstance(arguments, dict) else {} - - if self._runtime is not None: - from pythinker_code.soul.permission import check_tool_call_allowed - - if err := check_tool_call_allowed( - self._runtime, - tool_call.function.name, - tool_input_dict, - tool=tool, - ): - _emit_tool_use_skipped_if_opted_in( - tool, - tool_call_id=tool_call.id, - tool_name=tool_call.function.name, - reason="policy", - ) - return ToolResult(tool_call_id=tool_call.id, return_value=err) - - # --- PreToolUse --- - from pythinker_code.hooks import events - - results = await self._hook_engine.trigger( - "PreToolUse", - matcher_value=tool_call.function.name, - input_data=events.pre_tool_use( - session_id=_get_session_id(), - cwd=str(Path.cwd()), - tool_name=tool_call.function.name, - tool_input=copy.deepcopy(tool_input_dict), - tool_call_id=tool_call.id, - ), - ) - for result in results: - if result.action == "block": - _emit_tool_use_skipped_if_opted_in( - tool, - tool_call_id=tool_call.id, - tool_name=tool_call.function.name, - reason="policy", - ) - return ToolResult( - tool_call_id=tool_call.id, - return_value=ToolError( - message=result.reason or "Blocked by PreToolUse hook", - brief="Hook blocked", - ), - ) - - # --- Execute tool --- - from pythinker_code.telemetry import metrics as _m - from pythinker_code.telemetry import otel as _otel - - if not _tool_defers_execution_started(tool): - emit_current_tool_execution_started() - - t0 = time.monotonic() - telemetry_tool_name = sanitize_telemetry_tool_name(tool_call.function.name) - _tool_span_cm = _otel.start_span( - "pythinker.tool", - { - "tool.name": telemetry_tool_name, - "tool.call_id": tool_call.id, - # GenAI semconv so GenAI-aware backends recognize the tool layer. - "gen_ai.operation.name": "execute_tool", - "gen_ai.tool.name": telemetry_tool_name, - }, - ) - _tool_span = _tool_span_cm.__enter__() - try: - ret = await self._gated_call(tool, copy.deepcopy(arguments)) - except Exception as e: - tool_elapsed = time.monotonic() - t0 - _tool_span.set_attribute("tool.success", False) - _tool_span.set_attribute("tool.error_type", type(e).__name__) - _tool_span.set_attribute("tool.duration_ms", int(tool_elapsed * 1000)) - _tool_span_cm.__exit__(type(e), e, e.__traceback__) - _m.record_tool_call( - tool_name=telemetry_tool_name, - duration_seconds=tool_elapsed, - success=False, - error_type=type(e).__name__, - ) - _m.record_error(kind="tool_error", error_type=type(e).__name__) - logger.exception( - "Tool execution failed: {tool_name} (call_id={call_id})", - tool_name=tool_call.function.name, - call_id=tool_call.id, - ) - # --- PostToolUseFailure (fire-and-forget) --- - self._hook_engine.fire_and_forget_trigger( - "PostToolUseFailure", - matcher_value=tool_call.function.name, - input_data=events.post_tool_use_failure( - session_id=_get_session_id(), - cwd=str(Path.cwd()), - tool_name=tool_call.function.name, - tool_input=copy.deepcopy(tool_input_dict), - error=str(e), - tool_call_id=tool_call.id, - ), - ) - from pythinker_code.telemetry import track - - _error_type = type(e).__name__ - track( - "tool_error", - tool_name=telemetry_tool_name, - error_type=_error_type, - ) - track( - "tool_call", - tool_name=telemetry_tool_name, - success=False, - duration_ms=int(tool_elapsed * 1000), - error_type=_error_type, - dup_type="cross_step" if is_cross_step_dup else "normal", - ) - return ToolResult( - tool_call_id=tool_call.id, - return_value=ToolRuntimeError(str(e)), - ) - except BaseException as e: - # CancelledError/KeyboardInterrupt during the tool call: close the - # span in this task so its OTel context token detaches now, not - # later under GC in a different asyncio context. - _tool_span_cm.__exit__(type(e), e, e.__traceback__) - raise - - tool_elapsed = time.monotonic() - t0 - _tool_succeeded = not isinstance(ret, ToolError) - _tool_span.set_attribute("tool.success", _tool_succeeded) - if isinstance(ret, ToolError): - _tool_span.set_attribute("tool.error_brief", ret.brief or "") - _tool_span.set_attribute("tool.duration_ms", int(tool_elapsed * 1000)) - _tool_span_cm.__exit__(None, None, None) - _m.record_tool_call( - tool_name=telemetry_tool_name, - duration_seconds=tool_elapsed, - success=_tool_succeeded, - ) - logger.info( - "Tool {tool_name} completed in {elapsed:.1f}s (call_id={call_id})", - tool_name=tool_call.function.name, - elapsed=tool_elapsed, - call_id=tool_call.id, - ) - from pythinker_code.telemetry import track as _track_tool_call - - _track_tool_call( - "tool_call", - tool_name=telemetry_tool_name, - success=not isinstance(ret, ToolError), - duration_ms=int(tool_elapsed * 1000), - dup_type="cross_step" if is_cross_step_dup else "normal", - ) - - # --- PostToolUse (fire-and-forget) --- - self._hook_engine.fire_and_forget_trigger( - "PostToolUse", - matcher_value=tool_call.function.name, - input_data=events.post_tool_use( - session_id=_get_session_id(), - cwd=str(Path.cwd()), - tool_name=tool_call.function.name, - tool_input=copy.deepcopy(tool_input_dict), - tool_output=str(ret)[:2000], - tool_call_id=tool_call.id, - ), - ) - - # Append the dedup reminder inline (no-op on errors) so the - # returned task is the tool task itself: cancelling it cancels - # the tool, rather than orphaning it behind a wrapper task. - if reminder_text is not None: - return ToolResult( - tool_call_id=tool_call.id, - return_value=_append_reminder_to_return_value(ret, reminder_text), - ) - return ToolResult(tool_call_id=tool_call.id, return_value=ret) - - task = asyncio.create_task(_call()) - self._current_step_tasks[call_key] = task - return task - finally: - current_tool_call.reset(token) + def handle_batch( + self, + tool_calls: Sequence[ToolCall], + context: ToolBatchContext, + *, + on_tool_result: Callable[[ToolResult], None] | None = None, + ) -> ToolBatchHandle: + """Create one supervised execution batch after terminal response assembly.""" + calls = tuple(tool_calls) + return self._execution.handle_batch(calls, context, on_tool_result=on_tool_result) def register_external_tool( self, @@ -1650,6 +1068,12 @@ async def cleanup(self) -> None: with contextlib.suppress(asyncio.CancelledError): await self._mcp_loading_task + execution_error: asyncio.CancelledError | ToolCancellationTimeoutError | None = None + try: + await self._execution.cleanup() + except (asyncio.CancelledError, ToolCancellationTimeoutError) as error: + execution_error = error + # Close every MCP client concurrently with a per-server timeout, so one # hung or slow client cannot block teardown of the rest (mcpext-3). async def _close(info: MCPServerInfo) -> None: @@ -1660,6 +1084,8 @@ async def _close(info: MCPServerInfo) -> None: logger.debug("MCP client close failed/timed out: {error}", error=exc) await asyncio.gather(*(_close(info) for info in self._mcp_servers.values())) + if execution_error is not None: + raise execution_error @dataclass(slots=True) diff --git a/tasks/lessons.md b/tasks/lessons.md index 7df5d11e..df8e49f7 100644 --- a/tasks/lessons.md +++ b/tasks/lessons.md @@ -128,6 +128,15 @@ Format: trigger → rule. ## Verification gates +- **When teardown awaits supervised internal work before closing external resources**, preserve a + caller `CancelledError` but do not let it skip the remaining resource closures; capture the + cancellation, complete the teardown sequence, then re-raise it. + +- **When adding an internal method that a sibling class must call under strict Pyright**, do not + assume a leading underscore is harmless merely because both classes share a module. Use a + documented method on the non-exported internal type and preserve `reportPrivateUsage`; never add + a suppression just to retain protected-member spelling. + - **When a repo-required skill is absent from the advertised Codex skill roots**, check the project-documented legacy skill roots (especially `~/.claude/skills/`) before reporting it as unavailable; an incomplete root search is not evidence that the skill is missing. diff --git a/tasks/todo.md b/tasks/todo.md index 029dacf6..876d13d6 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -2,6 +2,59 @@ ## Active +### PR #207 cancellation-state review fix (2026-07-15) + +- [x] Execute `docs/superpowers/plans/2026-07-15-tool-execution-cancellation-state-rollback.md` + with a failing regression test before production edits. +- [x] Keep cancelled/failed batch fingerprints out of committed dedup and consecutive-call state. +- [x] Preserve completed-result snapshots, callback suppression, bounded cancellation, timeout + poisoning, and successful finalized summaries. +- [x] Run focused tests, core and CLI package gates, and `git diff --check`. +- [x] Bound cancellation-resistant async result callbacks under the StepResult ownership deadline. +- [x] Persist completed results and explicit completion-unknown markers when cancellation times out. +- [x] Connect engine late-drain ownership to bounded toolset/runtime cleanup. +- [x] Preserve caller cancellation until after MCP teardown completes. +- [x] Update the public architecture flow from per-call Soul dispatch to the batch engine path. +- [x] Complete task-scoped and whole-branch reviews with no open Critical/Important findings. +- [ ] Push the PR head, confirm the latest CodeRabbit review succeeds, reply to the remaining false + positive with evidence, and resolve it through GitHub GraphQL. + +Acceptance: after successful call A and cancelled call B, retrying B with A as the authoritative +prior context is not a cross-step duplicate and starts a fresh consecutive streak at 1. All PR +review threads are resolved only after the tested fix is present on GitHub. + +#### Review: PR #207 cancellation-state review fix + +- Root cause: `_ExecutionBatch._run()` finalized the engine step before watcher settlement. A + cancelled or failed batch therefore committed fingerprints that `PythinkerSoul` correctly kept + out of authoritative conversation state, causing a later retry to appear duplicated. +- Behavior: successful watcher settlement now commits the step exactly once. Cancellation or + failure preserves previously committed fingerprints and completed-result snapshots while + discarding only the current uncommitted step state before re-raising the original exception. +- TDD evidence: the new regression first failed because the retry reported + `dedup_triggered is True` and consecutive count 2; after the fix it passed with no duplicate, + consecutive count 1, and two real blocking-tool invocations. The final callback and + engine/Soul cancellation regressions passed 92 focused tests across the core and CLI packages. +- Package evidence: `make test-pythinker-core` reported 433 passed. `make check-pythinker-core` + passed Ruff, formatting, and Pyright with 0 errors; its repository-configured non-blocking `ty` + step retained 62 existing provider/third-party diagnostics outside the changed files. + `make check-pythinker-code` passed Ruff, formatting, Pyright with 0 errors, and blocking `ty`. + The final cancellation/MCP set reported 57 passed. The main CLI suite reported 7,073 passed, 9 + skipped, and 1 expected xfail; separate `tests_e2e` reported 65 passed and 4 skipped. The + VitePress documentation build and `git diff --check` also passed. +- Review: the task-scoped review approved the transactional rollback and the follow-up Pyright + correction. A leading-underscore sibling call initially triggered strict `reportPrivateUsage`; + the final `abort_step()` seam is documented and remains internal through the non-exported engine, + without a suppression. The first whole-branch review correctly blocked publication on three + Important cancellation-lifecycle gaps: callback settlement was unbounded, cancellation timeout + skipped context lineage repair, and engine late-drain work was absent from runtime cleanup. The + implementation now bounds all owned cancellation under one deadline, preserves truthful timeout + lineage, joins retained engine work during cleanup, and documents the batch path. The latest-head + CodeRabbit review then found that caller cancellation during engine cleanup could skip MCP + teardown. A real cancellation regression reproduced the leak before the fix and now proves MCP + closure precedes re-raising `CancelledError`. Fresh local re-review has no remaining + Critical/Important issue; final GitHub re-review and closeout remain pending. + ### TUI thinking Markdown and activity motion (2026-07-11) - [x] Execute `docs/superpowers/plans/2026-07-11-tui-thinking-markdown-and-activity-motion.md` diff --git a/tests/core/test_pythinkersoul_stuck_loop.py b/tests/core/test_pythinkersoul_stuck_loop.py index aaa729ab..d88dfdb4 100644 --- a/tests/core/test_pythinkersoul_stuck_loop.py +++ b/tests/core/test_pythinkersoul_stuck_loop.py @@ -9,7 +9,7 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncIterator, Sequence +from collections.abc import AsyncIterator, Callable, Sequence from pathlib import Path from typing import Self from unittest.mock import patch @@ -18,7 +18,15 @@ from pydantic import BaseModel from pythinker_core.chat_provider import StreamedMessagePart, ThinkingEffort, TokenUsage from pythinker_core.message import Message, TextPart, ToolCall -from pythinker_core.tooling import CallableTool2, ToolError, ToolOk, ToolReturnValue +from pythinker_core.tooling import ( + CallableTool2, + ToolBatchContext, + ToolBatchHandle, + ToolError, + ToolOk, + ToolResult, + ToolReturnValue, +) from pythinker_core.tooling.simple import SimpleToolset from pythinker_code.llm import LLM @@ -165,6 +173,41 @@ def _make_soul( return context, soul +class _SummaryOnlyToolset(PythinkerToolset): + """Fail if the soul reaches through the batch result into facade execution state.""" + + def __init__(self) -> None: + super().__init__() + self.contexts: list[ToolBatchContext] = [] + + def begin_step( + self, + previous_calls: list[tuple[str, str]], + *, + step_no: int = 0, + turn_id: str = "", + ) -> None: + del previous_calls, step_no, turn_id + raise AssertionError("PythinkerSoul must pass ToolBatchContext to core") + + def end_step(self) -> list[tuple[str, str]]: + raise AssertionError("PythinkerSoul must read StepResult.tool_execution_summary") + + @property + def consecutive_repeat_count(self) -> int: + raise AssertionError("PythinkerSoul must read StepResult.tool_execution_summary") + + def handle_batch( + self, + tool_calls: Sequence[ToolCall], + context: ToolBatchContext, + *, + on_tool_result: Callable[[ToolResult], None] | None = None, + ) -> ToolBatchHandle: + self.contexts.append(context) + return super().handle_batch(tool_calls, context, on_tool_result=on_tool_result) + + def _make_soul_with_pythinker_toolset( runtime: Runtime, provider: _ScriptedToolCallProvider, tmp_path: Path ) -> tuple[Context, PythinkerSoul]: @@ -501,6 +544,39 @@ async def test_consecutive_identical_calls_yield_stuck_outcome( assert "identical" in context.history[-1].extract_text(" ").lower() +@pytest.mark.asyncio +async def test_soul_routes_execution_state_only_through_batch_context_and_summary( + runtime: Runtime, + tmp_path: Path, +) -> None: + runtime.config.loop_control.max_consecutive_identical_calls = 3 + provider = _ScriptedToolCallProvider(["Ok", "Ok", None]) + llm = LLM(chat_provider=provider, max_context_size=100_000, capabilities=set()) + runtime = _rebuild_runtime_with_llm(runtime, llm) + toolset = _SummaryOnlyToolset() + toolset.add(_OkTool()) + soul = PythinkerSoul( + Agent( + name="Batch Context Test Agent", + system_prompt="Batch context test prompt.", + toolset=toolset, + runtime=runtime, + ), + context=Context(file_backend=tmp_path / "batch-context.jsonl"), + ) + + await run_soul(soul, "go", _drain_ui_messages, asyncio.Event()) + + assert [context.step_no for context in toolset.contexts] == [1, 2, 3] + assert len({context.turn_id for context in toolset.contexts}) == 1 + assert toolset.contexts[0].turn_id + assert [context.prior_call_fingerprints for context in toolset.contexts] == [ + (), + (("Ok", "{}"),), + (("Ok", "{}"),), + ] + + @pytest.mark.asyncio async def test_max_consecutive_identical_calls_zero_disables_backstop( runtime: Runtime, tmp_path: Path diff --git a/tests/core/test_pythinkersoul_turn_balance.py b/tests/core/test_pythinkersoul_turn_balance.py index a31724a6..47cd9cdd 100644 --- a/tests/core/test_pythinkersoul_turn_balance.py +++ b/tests/core/test_pythinkersoul_turn_balance.py @@ -3,12 +3,21 @@ import asyncio from pathlib import Path from types import SimpleNamespace +from typing import ClassVar from unittest.mock import AsyncMock import pytest +from pydantic import BaseModel from pythinker_core import StepResult +from pythinker_core.chat_provider.mock import MockChatProvider from pythinker_core.message import Message, ToolCall -from pythinker_core.tooling import ToolResult +from pythinker_core.tooling import ( + CallableTool2, + ToolCancellationTimeoutError, + ToolOk, + ToolResult, + ToolReturnValue, +) from pythinker_core.tooling.empty import EmptyToolset import pythinker_code.soul.pythinkersoul as pythinkersoul_module @@ -16,6 +25,7 @@ from pythinker_code.soul.approval import Approval from pythinker_code.soul.context import Context from pythinker_code.soul.pythinkersoul import PythinkerSoul, TurnOutcome +from pythinker_code.soul.toolset import PythinkerToolset from pythinker_code.wire.types import StepBegin, StepInterrupted, TextPart, TurnBegin, TurnEnd @@ -45,6 +55,52 @@ def __await__(self): return super().__await__() +class _NoParams(BaseModel): + pass + + +class _CancellationIgnoringTool(CallableTool2[_NoParams]): + name = "Stubborn" + description = "Wait until released, including after cancellation" + params = _NoParams + supports_parallel: ClassVar[bool] = True + + def __init__(self) -> None: + super().__init__() + self.started = asyncio.Event() + self.cancel_seen = asyncio.Event() + self.release = asyncio.Event() + self.finished = asyncio.Event() + + async def __call__(self, params: _NoParams) -> ToolReturnValue: + del params + self.started.set() + try: + await self.release.wait() + except asyncio.CancelledError: + self.cancel_seen.set() + await self.release.wait() + finally: + self.finished.set() + return ToolOk(output="late output") + + +class _ImmediateTool(CallableTool2[_NoParams]): + name = "Immediate" + description = "Complete immediately" + params = _NoParams + supports_parallel: ClassVar[bool] = True + + def __init__(self) -> None: + super().__init__() + self.finished = asyncio.Event() + + async def __call__(self, params: _NoParams) -> ToolReturnValue: + del params + self.finished.set() + return ToolOk(output="real output") + + @pytest.mark.asyncio async def test_run_emits_turn_end_when_step_interrupts( runtime: Runtime, @@ -270,6 +326,120 @@ async def fake_pythinker_core_step(chat_provider, system_prompt, toolset, histor ) +async def test_step_interruption_uses_completed_result_snapshot( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + soul = _make_soul(runtime, tmp_path) + done_call = ToolCall( + id="call-done", + function=ToolCall.FunctionBody(name="Noop", arguments="{}"), + ) + pending_call = ToolCall( + id="call-pending", + function=ToolCall.FunctionBody(name="Noop", arguments="{}"), + ) + done_future = asyncio.get_running_loop().create_future() + done_future.set_result( + ToolResult(tool_call_id=done_call.id, return_value=ToolOk(output="real output")) + ) + pending_future = _EnteredFuture() + + async def fake_pythinker_core_step(chat_provider, system_prompt, toolset, history, **kwargs): + return StepResult( + id="step-partial", + message=Message(role="assistant", content=[TextPart(text="I'll use tools.")]), + usage=None, + tool_calls=[done_call, pending_call], + _tool_result_futures={ + done_call.id: done_future, + pending_call.id: pending_future, + }, + ) + + monkeypatch.setattr(pythinkersoul_module.pythinker_core, "step", fake_pythinker_core_step) + monkeypatch.setattr(pythinkersoul_module, "wire_send", lambda _msg: None) + + step_task = asyncio.create_task(soul._step()) + await pending_future.entered.wait() + step_task.cancel() + with pytest.raises(asyncio.CancelledError): + await step_task + + tool_messages = { + message.tool_call_id: message for message in soul.context.history if message.role == "tool" + } + assert "real output" in tool_messages[done_call.id].extract_text(" ") + assert "interrupted by user" in tool_messages[pending_call.id].extract_text(" ").lower() + + +async def test_step_timeout_persists_completed_and_completion_unknown_results( + runtime: Runtime, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("pythinker_core._STEP_CANCELLATION_TIMEOUT_SECONDS", 0.01) + monkeypatch.setattr( + "pythinker_code.soul.tool_execution.TOOL_CANCELLATION_TIMEOUT_SECONDS", 0.01 + ) + done_call = ToolCall( + id="call-done", + function=ToolCall.FunctionBody(name="Immediate", arguments="{}"), + ) + pending_call = ToolCall( + id="call-pending", + function=ToolCall.FunctionBody(name="Stubborn", arguments="{}"), + ) + assert runtime.llm is not None + runtime.llm.chat_provider = MockChatProvider( + [done_call, pending_call], + finish_reason="tool_calls", + ) + immediate = _ImmediateTool() + stubborn = _CancellationIgnoringTool() + toolset = PythinkerToolset() + toolset.add(immediate) + toolset.add(stubborn) + soul = PythinkerSoul( + Agent( + name="Timeout lineage agent", + system_prompt="Test prompt.", + toolset=toolset, + runtime=runtime, + ), + context=Context(file_backend=tmp_path / "history.jsonl"), + ) + monkeypatch.setattr(pythinkersoul_module, "wire_send", lambda _message: None) + + step_task = asyncio.create_task(soul._step()) + await stubborn.started.wait() + await immediate.finished.wait() + await asyncio.sleep(0) + step_task.cancel() + + try: + with pytest.raises(ToolCancellationTimeoutError): + await step_task + + history = list(soul.context.history) + assistant_messages = [message for message in history if message.role == "assistant"] + tool_messages = { + message.tool_call_id: message for message in history if message.role == "tool" + } + assert len(assistant_messages) == 1 + assert set(tool_messages) == {done_call.id, pending_call.id} + assert "real output" in tool_messages[done_call.id].extract_text(" ") + pending_text = tool_messages[pending_call.id].extract_text(" ").lower() + assert "completion is unknown" in pending_text + assert "may still be running" in pending_text + assert "must not be retried automatically" in pending_text + assert soul._last_tool_calls == [] # pyright: ignore[reportPrivateUsage] + finally: + stubborn.release.set() + await asyncio.wait_for(stubborn.finished.wait(), timeout=1) + + async def test_step_persists_markers_when_cancelled_twice( runtime: Runtime, tmp_path: Path, diff --git a/tests/core/test_session_logging.py b/tests/core/test_session_logging.py index d5ca2eac..ad5077f5 100644 --- a/tests/core/test_session_logging.py +++ b/tests/core/test_session_logging.py @@ -205,12 +205,20 @@ async def call(self, arguments): function=ToolCall.FunctionBody(name="FailingTool", arguments="{}"), ) - with patch("pythinker_code.soul.toolset.logger") as mock_logger: + from loguru import logger as loguru_logger + + records: list[str] = [] + loguru_logger.enable("pythinker_code") + sink_id = loguru_logger.add(lambda message: records.append(str(message)), level="ERROR") + try: result = toolset.handle(tool_call) if isinstance(result, asyncio.Task): await result - mock_logger.exception.assert_called() - assert "FailingTool" in str(mock_logger.exception.call_args) + finally: + loguru_logger.remove(sink_id) + loguru_logger.disable("pythinker_code") + + assert any("FailingTool" in record for record in records) async def test_toolset_json_parse_error_logged(self): """When tool call arguments are invalid JSON, toolset should log a WARNING.""" @@ -230,10 +238,18 @@ class DummyTool: function=ToolCall.FunctionBody(name="DummyTool", arguments="{invalid json}"), ) - with patch("pythinker_code.soul.toolset.logger") as mock_logger: + from loguru import logger as loguru_logger + + records: list[str] = [] + loguru_logger.enable("pythinker_code") + sink_id = loguru_logger.add(lambda message: records.append(str(message)), level="WARNING") + try: toolset.handle(tool_call) - mock_logger.warning.assert_called() - assert "DummyTool" in str(mock_logger.warning.call_args) + finally: + loguru_logger.remove(sink_id) + loguru_logger.disable("pythinker_code") + + assert any("DummyTool" in record for record in records) class TestFileToolLogging: diff --git a/tests/core/test_tool_execution_cancellation.py b/tests/core/test_tool_execution_cancellation.py new file mode 100644 index 00000000..c3e435ea --- /dev/null +++ b/tests/core/test_tool_execution_cancellation.py @@ -0,0 +1,439 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from typing import Any, ClassVar, cast + +import pytest +from pydantic import BaseModel +from pythinker_core import step +from pythinker_core.chat_provider.mock import MockChatProvider +from pythinker_core.tooling import ( + CallableTool2, + ToolBatchContext, + ToolCancellationTimeoutError, + ToolOk, + ToolReturnValue, +) + +from pythinker_code.soul import tool_execution +from pythinker_code.soul.toolset import MCPServerInfo, PythinkerToolset +from pythinker_code.wire.types import ToolCall, ToolResult + + +class NoParams(BaseModel): + pass + + +class CancellationIgnoringTool(CallableTool2[NoParams]): + name: str = "Stubborn" + description: str = "Wait until released, including after cancellation" + params: type[NoParams] = NoParams + supports_parallel: ClassVar[bool] = True + + def __init__(self) -> None: + super().__init__() + self.started = asyncio.Event() + self.cancel_seen = asyncio.Event() + self.release = asyncio.Event() + self.finished = asyncio.Event() + self.invocations = 0 + + async def __call__(self, params: NoParams) -> ToolReturnValue: + del params + self.invocations += 1 + self.started.set() + try: + await self.release.wait() + except asyncio.CancelledError: + self.cancel_seen.set() + await self.release.wait() + finally: + self.finished.set() + return ToolOk(output="late") + + +class CompleteAndCancelCallerTool(CallableTool2[NoParams]): + name: str = "CompleteAndCancel" + description: str = "Complete while cancelling the result collector" + params: type[NoParams] = NoParams + supports_parallel: ClassVar[bool] = True + + def __init__(self) -> None: + super().__init__() + self.cancel_target: Callable[[], bool] | None = None + + async def __call__(self, params: NoParams) -> ToolReturnValue: + del params + assert self.cancel_target is not None + asyncio.get_running_loop().call_soon(self.cancel_target) + return ToolOk(output="completed before cancellation") + + +class ImmediateTool(CallableTool2[NoParams]): + name: str = "Immediate" + description: str = "Complete immediately" + params: type[NoParams] = NoParams + supports_parallel: ClassVar[bool] = True + + def __init__(self) -> None: + super().__init__() + self.invocations = 0 + + async def __call__(self, params: NoParams) -> ToolReturnValue: + del params + self.invocations += 1 + return ToolOk(output="done") + + +class RecordingClient: + def __init__(self) -> None: + self.closed = asyncio.Event() + + async def close(self) -> None: + self.closed.set() + + +def _call(call_id: str, name: str) -> ToolCall: + return ToolCall( + id=call_id, + function=ToolCall.FunctionBody(name=name, arguments="{}"), + ) + + +async def _wait_until_recovered(toolset: PythinkerToolset) -> None: + for _ in range(100): + if not toolset._execution.poisoned: # pyright: ignore[reportPrivateUsage] + return + await asyncio.sleep(0.001) + raise AssertionError("execution engine did not recover after late task settlement") + + +def test_cancellation_timeout_default_is_five_seconds() -> None: + assert tool_execution.TOOL_CANCELLATION_TIMEOUT_SECONDS == 5.0 + + +@pytest.mark.parametrize("timeout", [-1.0, float("nan"), float("inf")]) +async def test_engine_cleanup_rejects_invalid_timeout(timeout: float) -> None: + toolset = PythinkerToolset() + + with pytest.raises(ValueError, match="finite and non-negative"): + await toolset._execution.cleanup(timeout=timeout) # pyright: ignore[reportPrivateUsage] + + +@pytest.mark.parametrize("timeout", [-1.0, float("nan"), float("inf")]) +async def test_invalid_cancellation_timeout_does_not_cancel_work(timeout: float) -> None: + stubborn = CancellationIgnoringTool() + toolset = PythinkerToolset() + toolset.add(stubborn) + batch = toolset.handle_batch([_call("stubborn", "Stubborn")], ToolBatchContext()) + await stubborn.started.wait() + + with pytest.raises(ValueError, match="finite and non-negative"): + await batch.cancel_and_settle(timeout=timeout) + + assert not stubborn.cancel_seen.is_set() + stubborn.release.set() + assert [result.tool_call_id for result in await batch.results()] == ["stubborn"] + + +async def test_zero_timeout_accepts_already_completed_batch() -> None: + immediate = ImmediateTool() + toolset = PythinkerToolset() + toolset.add(immediate) + batch = toolset.handle_batch([_call("done", "Immediate")], ToolBatchContext()) + assert [result.tool_call_id for result in await batch.results()] == ["done"] + + await batch.cancel_and_settle(timeout=0) + + assert toolset._execution.poisoned is False # pyright: ignore[reportPrivateUsage] + + +async def test_cancelled_batch_does_not_commit_cross_step_dedup_state() -> None: + stubborn = CancellationIgnoringTool() + immediate = ImmediateTool() + toolset = PythinkerToolset() + toolset.add(stubborn) + toolset.add(immediate) + + first = toolset.handle_batch( + [_call("first", "Immediate")], + ToolBatchContext(turn_id="turn", step_no=1), + ) + await first.results() + prior = first.summary.current_call_fingerprints + + cancelled = toolset.handle_batch( + [_call("cancelled", "Stubborn")], + ToolBatchContext( + turn_id="turn", + step_no=2, + prior_call_fingerprints=prior, + ), + ) + await stubborn.started.wait() + settlement = asyncio.create_task(cancelled.cancel_and_settle()) + await stubborn.cancel_seen.wait() + stubborn.release.set() + await settlement + + retry = toolset.handle_batch( + [_call("retry", "Stubborn")], + ToolBatchContext( + turn_id="turn", + step_no=2, + prior_call_fingerprints=prior, + ), + ) + assert [result.tool_call_id for result in await retry.results()] == ["retry"] + assert retry.summary.dedup_triggered is False + assert retry.summary.consecutive_identical_call_count == 1 + assert stubborn.invocations == 2 + + +async def test_timeout_poisons_new_batches_until_late_task_is_drained() -> None: + stubborn = CancellationIgnoringTool() + immediate = ImmediateTool() + callbacks: list[str] = [] + toolset = PythinkerToolset() + toolset.add(stubborn) + toolset.add(immediate) + batch = toolset.handle_batch( + [_call("stubborn", "Stubborn")], + ToolBatchContext(turn_id="turn", step_no=1), + on_tool_result=lambda result: callbacks.append(result.tool_call_id), + ) + await stubborn.started.wait() + + with pytest.raises(ToolCancellationTimeoutError, match="did not settle"): + await batch.cancel_and_settle(timeout=0.01) + + assert toolset._execution.poisoned is True # pyright: ignore[reportPrivateUsage] + with pytest.raises(ToolCancellationTimeoutError, match="unavailable"): + toolset.handle_batch( + [_call("blocked", "Immediate")], + ToolBatchContext(turn_id="turn", step_no=2), + ) + assert immediate.invocations == 0 + assert callbacks == [] + + stubborn.release.set() + await stubborn.finished.wait() + await _wait_until_recovered(toolset) + + recovery = toolset.handle_batch( + [_call("recovered", "Immediate")], + ToolBatchContext(turn_id="turn", step_no=2), + ) + assert [result.tool_call_id for result in await recovery.results()] == ["recovered"] + assert immediate.invocations == 1 + assert callbacks == [] + + +async def test_timeout_keeps_previously_completed_snapshot_and_blocks_late_callback() -> None: + stubborn = CancellationIgnoringTool() + immediate = ImmediateTool() + callbacks: list[str] = [] + toolset = PythinkerToolset() + toolset.add(stubborn) + toolset.add(immediate) + batch = toolset.handle_batch( + [_call("done", "Immediate"), _call("stubborn", "Stubborn")], + ToolBatchContext(), + on_tool_result=lambda result: callbacks.append(result.tool_call_id), + ) + await stubborn.started.wait() + for _ in range(100): + if "done" in batch.completed_results: + break + await asyncio.sleep(0.001) + + with pytest.raises(ToolCancellationTimeoutError): + await batch.cancel_and_settle(timeout=0.01) + + assert batch.completed_results == { + "done": ToolResult(tool_call_id="done", return_value=ToolOk(output="done")) + } + assert callbacks == ["done"] + stubborn.release.set() + await stubborn.finished.wait() + await _wait_until_recovered(toolset) + await asyncio.sleep(0) + assert callbacks == ["done"] + + +async def test_toolset_cleanup_waits_for_timed_out_engine_work() -> None: + stubborn = CancellationIgnoringTool() + toolset = PythinkerToolset() + toolset.add(stubborn) + batch = toolset.handle_batch([_call("stubborn", "Stubborn")], ToolBatchContext()) + await stubborn.started.wait() + with pytest.raises(ToolCancellationTimeoutError): + await batch.cancel_and_settle(timeout=0.01) + + cleanup_task = asyncio.create_task(toolset.cleanup()) + try: + await asyncio.sleep(0) + assert not cleanup_task.done() + + stubborn.release.set() + await asyncio.wait_for(cleanup_task, timeout=1) + assert stubborn.finished.is_set() + assert toolset._execution.poisoned is False # pyright: ignore[reportPrivateUsage] + finally: + stubborn.release.set() + await asyncio.wait_for(stubborn.finished.wait(), timeout=1) + await asyncio.gather(cleanup_task, return_exceptions=True) + + +async def test_toolset_cleanup_closes_mcp_before_reporting_engine_timeout( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(tool_execution, "TOOL_CANCELLATION_TIMEOUT_SECONDS", 0.01) + stubborn = CancellationIgnoringTool() + client = RecordingClient() + toolset = PythinkerToolset() + toolset.add(stubborn) + toolset._mcp_servers["recording"] = MCPServerInfo( # pyright: ignore[reportPrivateUsage] + status="connected", + client=cast(Any, client), + tools=[], + resources=[], + prompts=[], + ) + batch = toolset.handle_batch([_call("stubborn", "Stubborn")], ToolBatchContext()) + await stubborn.started.wait() + with pytest.raises(ToolCancellationTimeoutError): + await batch.cancel_and_settle(timeout=0.01) + + try: + with pytest.raises(ToolCancellationTimeoutError, match="cleanup did not settle"): + await toolset.cleanup() + assert client.closed.is_set() + finally: + stubborn.release.set() + await asyncio.wait_for(stubborn.finished.wait(), timeout=1) + await _wait_until_recovered(toolset) + + +async def test_toolset_cleanup_closes_mcp_before_preserving_caller_cancellation() -> None: + stubborn = CancellationIgnoringTool() + client = RecordingClient() + toolset = PythinkerToolset() + toolset.add(stubborn) + toolset._mcp_servers["recording"] = MCPServerInfo( # pyright: ignore[reportPrivateUsage] + status="connected", + client=cast(Any, client), + tools=[], + resources=[], + prompts=[], + ) + batch = toolset.handle_batch([_call("stubborn", "Stubborn")], ToolBatchContext()) + await stubborn.started.wait() + with pytest.raises(ToolCancellationTimeoutError): + await batch.cancel_and_settle(timeout=0.01) + + cleanup_task = asyncio.create_task(toolset.cleanup()) + try: + await asyncio.sleep(0) + assert not cleanup_task.done() + + cleanup_task.cancel() + with pytest.raises(asyncio.CancelledError): + await cleanup_task + assert client.closed.is_set() + finally: + stubborn.release.set() + await asyncio.wait_for(stubborn.finished.wait(), timeout=1) + await _wait_until_recovered(toolset) + + +async def test_completion_racing_cancellation_remains_in_snapshot() -> None: + tool = CompleteAndCancelCallerTool() + toolset = PythinkerToolset() + toolset.add(tool) + result = await step( + MockChatProvider( + [_call("completed", "CompleteAndCancel")], + finish_reason="tool_calls", + ), + "", + toolset, + [], + ) + waiter = asyncio.create_task(result.tool_results()) + tool.cancel_target = waiter.cancel + + with pytest.raises(asyncio.CancelledError): + await waiter + + assert result.completed_tool_results == { + "completed": ToolResult( + tool_call_id="completed", + return_value=ToolOk(output="completed before cancellation"), + ) + } + + +async def test_repeated_caller_cancellation_cannot_detach_core_settlement() -> None: + stubborn = CancellationIgnoringTool() + toolset = PythinkerToolset() + toolset.add(stubborn) + result = await step( + MockChatProvider([_call("stubborn", "Stubborn")], finish_reason="tool_calls"), + "", + toolset, + [], + ) + waiter = asyncio.create_task(result.tool_results()) + await stubborn.started.wait() + + waiter.cancel() + await stubborn.cancel_seen.wait() + waiter.cancel() + await asyncio.sleep(0) + assert not waiter.done() + + stubborn.release.set() + with pytest.raises(asyncio.CancelledError): + await waiter + assert stubborn.finished.is_set() + assert toolset._execution.poisoned is False # pyright: ignore[reportPrivateUsage] + + +async def test_core_surfaces_timeout_then_engine_recovers_without_task_warnings( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("pythinker_core._STEP_CANCELLATION_TIMEOUT_SECONDS", 0.01) + monkeypatch.setattr(tool_execution, "TOOL_CANCELLATION_TIMEOUT_SECONDS", 0.01) + reports: list[dict[str, object]] = [] + loop = asyncio.get_running_loop() + previous_handler = loop.get_exception_handler() + loop.set_exception_handler(lambda _loop, context: reports.append(context)) + stubborn = CancellationIgnoringTool() + toolset = PythinkerToolset() + toolset.add(stubborn) + try: + result = await step( + MockChatProvider([_call("stubborn", "Stubborn")], finish_reason="tool_calls"), + "", + toolset, + [], + ) + waiter = asyncio.create_task(result.tool_results()) + await stubborn.started.wait() + waiter.cancel() + + with pytest.raises(ToolCancellationTimeoutError): + await waiter + assert toolset._execution.poisoned is True # pyright: ignore[reportPrivateUsage] + + stubborn.release.set() + await stubborn.finished.wait() + await _wait_until_recovered(toolset) + await asyncio.sleep(0) + finally: + stubborn.release.set() + loop.set_exception_handler(previous_handler) + + assert reports == [] diff --git a/tests/core/test_tool_execution_engine.py b/tests/core/test_tool_execution_engine.py new file mode 100644 index 00000000..ac4b399f --- /dev/null +++ b/tests/core/test_tool_execution_engine.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable, Sequence +from typing import cast + +import pytest +from pydantic import BaseModel +from pythinker_core.tooling import ( + BatchToolset, + CallableTool2, + ToolBatchContext, + ToolBatchHandle, + ToolOk, + ToolResult, + ToolReturnValue, +) + +from pythinker_code.soul.tool_execution import ToolExecutionEngine +from pythinker_code.soul.toolset import PythinkerToolset +from pythinker_code.wire.types import ToolCall + + +class DelayParams(BaseModel): + label: str + delay: float = 0 + + +class DelayTool(CallableTool2[DelayParams]): + name: str = "Delay" + description: str = "Return a label after an optional delay" + params: type[DelayParams] = DelayParams + supports_parallel = True + + def __init__(self, invocations: list[str]) -> None: + super().__init__() + self._invocations = invocations + + async def __call__(self, params: DelayParams) -> ToolReturnValue: + self._invocations.append(params.label) + await asyncio.sleep(params.delay) + return ToolOk(output=params.label) + + +def _call(call_id: str, *, label: str, delay: float = 0) -> ToolCall: + return ToolCall( + id=call_id, + function=ToolCall.FunctionBody( + name="Delay", + arguments=f'{{"label":"{label}","delay":{delay}}}', + ), + ) + + +def test_facade_retains_registry_while_engine_owns_execution_state() -> None: + toolset = PythinkerToolset() + tool = DelayTool([]) + toolset.add(tool) + + assert isinstance(toolset._execution, ToolExecutionEngine) # pyright: ignore[reportPrivateUsage] + assert toolset.find("Delay") is tool + assert "_tool_dict" in vars(toolset) + assert "_tool_dict" not in vars(toolset._execution) # pyright: ignore[reportPrivateUsage] + assert "_current_step_tasks" not in vars(toolset) + assert "_current_step_tasks" in vars( + toolset._execution # pyright: ignore[reportPrivateUsage] + ) + + +async def test_facade_handle_and_step_state_use_public_execution_contract() -> None: + invocations: list[str] = [] + toolset = PythinkerToolset() + toolset.add(DelayTool(invocations)) + call = _call("call-1", label="one") + toolset.begin_step([("Delay", '{"label":"before","delay":0}')], step_no=3, turn_id="t") + + handled = toolset.handle(call) + assert isinstance(handled, asyncio.Future) + result = await handled + + assert result == ToolResult(tool_call_id=call.id, return_value=ToolOk(output="one")) + assert invocations == ["one"] + assert toolset.end_step() == [("Delay", '{"delay":0,"label":"one"}')] + assert toolset.dedup_triggered is False + assert toolset.consecutive_repeat_count == 1 + + +async def test_batch_handle_reports_completion_order_and_returns_model_order() -> None: + invocations: list[str] = [] + callbacks: list[str] = [] + toolset = PythinkerToolset() + toolset.add(DelayTool(invocations)) + calls = [ + _call("slow", label="slow", delay=0.02), + _call("fast", label="fast", delay=0), + ] + + batch = toolset.handle_batch( + calls, + ToolBatchContext(turn_id="turn", step_no=1), + on_tool_result=lambda result: callbacks.append(result.tool_call_id), + ) + results = await batch.results() + + assert isinstance(toolset, BatchToolset) + assert isinstance(batch, ToolBatchHandle) + assert callbacks == ["fast", "slow"] + assert [result.tool_call_id for result in results] == ["slow", "fast"] + assert set(batch.completed_results) == {"slow", "fast"} + assert batch.summary.current_call_fingerprints == ( + ("Delay", '{"delay":0.02,"label":"slow"}'), + ("Delay", '{"delay":0,"label":"fast"}'), + ) + assert batch.summary.finalized is True + assert invocations == ["slow", "fast"] + + +async def test_batch_same_step_duplicate_runs_original_once() -> None: + invocations: list[str] = [] + toolset = PythinkerToolset() + toolset.add(DelayTool(invocations)) + calls = [_call("one", label="same"), _call("two", label="same")] + + results = await toolset.handle_batch(calls, ToolBatchContext()).results() + + assert invocations == ["same"] + assert [result.tool_call_id for result in results] == ["one", "two"] + assert [result.return_value.output for result in results] == ["same", "same"] + + +class _ExplodingFunction: + @property + def name(self) -> str: + raise RuntimeError("batch preparation failed") + + arguments = "{}" + + +class _ExplodingCall: + id = "explode" + function = _ExplodingFunction() + + +async def test_batch_construction_failure_starts_no_tool_work() -> None: + invocations: list[str] = [] + callbacks: list[ToolResult] = [] + toolset = PythinkerToolset() + toolset.add(DelayTool(invocations)) + calls = cast(Sequence[ToolCall], [_call("valid", label="valid"), _ExplodingCall()]) + + with pytest.raises(RuntimeError, match="batch preparation failed"): + toolset.handle_batch( + calls, + ToolBatchContext(), + on_tool_result=cast(Callable[[ToolResult], None], callbacks.append), + ) + + await asyncio.sleep(0) + assert invocations == [] + assert callbacks == [] + + +async def test_batch_context_seeds_cross_step_dedup_summary() -> None: + invocations: list[str] = [] + toolset = PythinkerToolset() + toolset.add(DelayTool(invocations)) + prior = (("Delay", '{"delay":0,"label":"same"}'),) + + batch = toolset.handle_batch( + [_call("one", label="same")], + ToolBatchContext( + turn_id="turn", + step_no=2, + prior_call_fingerprints=prior, + ), + ) + await batch.results() + + assert batch.summary.dedup_triggered is True + assert batch.summary.consecutive_identical_call_count == 2