Commit fe5e465
feat(tool): consolidate Tool + AsyncTool into one class — RFC + code (#212)
* chore(openspec): draft tool-consolidation RFC
Proposes collapsing `Tool` + `AsyncTool` (and `AbstractTool` +
`AbstractAsyncTool`) into one `Tool` class that supports `@tool_action`
methods of either kind on the same class.
Mirrors the dual-API pattern shipped on cube-harness `MonitoredTool`
(PR #386):
* `execute_action(action)` — sync dispatch
* `async_execute_action(action)` — async dispatch, universal call-site
`async_execute_action` already exists on `AbstractTool` as a non-abstract
method from cube-standard #152. This proposal makes it the canonical
universal call-site and drops the class-level split that hid the dual
surface.
Migration: `AsyncTool` and `AbstractAsyncTool` become aliases of the
unified classes for one release window. The single in-tree `AsyncTool`
subclass (`AsyncBrowserTool`) gets a one-line edit; everything else
keeps working unchanged.
Phase 2 (collapse `Toolbox` + `AsyncToolbox`) is explicitly out of
scope here — `AsyncToolbox.execute_action` is async by contract and
collapsing it would break every `await tb.execute_action(action)`
call site. A future RFC if cost-benefit shifts.
Signed-off-by: Alexandre Lacoste <alex.lacoste.shmu@gmail.com>
Signed-off-by: Cube Harness <cube-harness@example.com>
* docs(openspec): make tool-consolidation proposal pedagogical
Expanded the proposal with:
* "What this is, in one sentence" lede explaining the gist
* Concrete friction story showing why one tool can't have one
async method + N sync helpers today
* "Before vs after" mermaid diagram
* Three concrete code cases authors care about (pure sync, pure
async, mixed) with 4-line examples each
* Dispatch-matrix mermaid showing how the two call sites route
per the action's def keyword
* Sync vs async Agent author UX section + table
* Debug story with actual call-stack snippets for both paths
* Efficiency story with concrete numbers from the parallelism smoke
* Migration story as a "what you have / what you'll have" table
The structure walks the reader through *why* (the friction), *what*
(the diagram + code cases), *how* (the dispatch matrix), and *what
it costs/saves* (debug + efficiency stories) before the
alternatives/risks sections.
Signed-off-by: Alexandre Lacoste <alex.lacoste.shmu@gmail.com>
Signed-off-by: Cube Harness <cube-harness@example.com>
* docs(openspec): tool-consolidation — symmetric bridge, F1 acknowledgement
Pivot from "raise TypeError on sync→async" to "auto-bridge in both
directions", on the back of a discussion with the harness author:
* RAM overhead is noise (± a few hundred KB per active bridge,
pooled threads for async→sync).
* `_arun` already handles sync inners cleanly via `asyncio.to_thread`
(shipped by PR #386); symmetric move is for `execute_action` to
bridge async actions via thread+loop so 90/10 mixed tools "just
work" from any call-site.
* The 99% fast paths stay at zero overhead; bridge cost (~2-5 ms)
is only paid on the rare async-from-sync call.
What changed in the docs:
* **Dispatch matrix** is now 2x2 with both bridges shown. The
"raises TypeError" red box is gone; both call-sites work for both
action kinds.
* New section **"What the implementation looks like"** with the
actual ~50 LOC dispatch + bridge code, including `contextvars`
propagation for OTel/tracing across the bridge thread.
* New subsection **"Thread-affine tools (override hook)"** documents
the override pattern for sync-Playwright-style tools that need
per-instance single-threaded executors. This is the layer-correct
extension point for F1's class of problems.
* Risks updated: bridge overhead becomes the new risk (transparent
but not free); class-definition-time validation regression is
softer; thread affinity stays an author's concern via the
documented override hook.
* New section **"Known harness-side follow-ups (out of scope)"**
names F1 (sync-browser cubes on async episode) and PR #487 (the
install_monitoring task.tool-clobber fix) so reviewers see the
surrounding landscape and the layer separation.
Companion section: cube-harness needs no API change for this RFC;
F1's harness-side fix (per-episode env-executor) is orthogonal.
Signed-off-by: Alexandre Lacoste <alex.lacoste.shmu@gmail.com>
Signed-off-by: Cube Harness <cube-harness@example.com>
* docs(openspec): tool-consolidation — also collapse Toolbox + AsyncToolbox
The original draft framed `Toolbox` / `AsyncToolbox` consolidation as
"Phase 2 — out of scope" on the misread that we'd be collapsing into
a sync-only Toolbox and breaking every `await tb.execute_action(...)`
call site. Reviewer (correctly) pushed back: the same dual-API pattern
collapses Toolbox cleanly.
What changed in the docs:
* **The change in one diagram** — extended to three layers
(AbstractTool, Tool, Toolbox). All three collapse to one class
each, all carrying the same dual API.
* **Toolbox section** (replaces the rejected "Phase 2" section) —
shows the ~10 LOC routing on the unified Toolbox and the
`AsyncToolbox(Toolbox)` deprecation shim that preserves
`await tb.execute_action(...)` for legacy callers with a
DeprecationWarning.
* **Migration table** — adds Toolbox / AsyncToolbox migration rows.
* **Alternatives** — adds the "consolidate Tool only" variant and
explains why it was rejected (so the prior framing is on the
record, not silently rewritten).
* **deltas.md** — adds the Toolbox class snippet + AsyncToolbox shim
block alongside the existing AsyncTool / AbstractAsyncTool aliases.
Signed-off-by: Alexandre Lacoste <alex.lacoste.shmu@gmail.com>
Signed-off-by: Cube Harness <cube-harness@example.com>
* feat(tool): consolidate Tool/AsyncTool + Toolbox/AsyncToolbox
Implementation of the tool-consolidation RFC. Three layers collapse
to one class each carrying the dual API (`execute_action` sync +
`async_execute_action` async). All four cells of the dispatch matrix
work without raising — the rare-path bridges (sync→async via thread+loop,
async→sync via asyncio.to_thread) keep mixed-action tools working from
any call site.
`src/cube/tool.py`:
* `Tool.execute_action` — sync caller. Sync action: direct call.
Async action: bridge via one-shot daemon thread + new event loop
(~2-5 ms). `contextvars.copy_context()` propagates the caller's
context (OTel spans, tracing state) into the worker.
* `Tool.async_execute_action` — async caller. Async action: direct
await. Sync action: `asyncio.to_thread` (pooled, parallel-safe).
* `Tool._bridge_async_to_sync` helper hosts the thread+loop bridge.
Doc-string covers overhead + override hook for thread-affine tools.
* `_ToolActionsMixin.__init_subclass__` no longer enforces all-sync
or all-async — mixing is now explicitly supported.
* `AsyncTool` becomes a deprecated alias subclassing both `Tool`
(for the new dual API) AND `AbstractAsyncTool` (so existing
`isinstance(x, AbstractAsyncTool)` checks keep returning True).
Overrides `execute_action` with async semantics so legacy
`await asynctool.execute_action(a)` callers keep working.
Subclassing emits a `DeprecationWarning`.
* `AbstractAsyncTool` becomes a deprecated shim subclassing
`AbstractTool`. Keeps abstract async `execute_action` so legacy
abstract subclasses keep working. Subclassing emits a
`DeprecationWarning` (skipped for our own `AsyncTool` shim at
module import time to avoid noise).
* `Toolbox` gets `async_execute_action` (routes to leaf's async
dispatch) and `async_reset`/`async_close` (await coroutine returns
from any leaf). Sync `reset`/`close` close (don't await) any
legacy coroutine returns so they don't leak.
* `AsyncToolbox` becomes a deprecated shim subclassing `Toolbox`,
with async `execute_action`/`reset`/`close` preserved. Calling
`execute_action` emits a `DeprecationWarning`.
`src/cube/tools/browser.py`:
* `AsyncBrowserTool(AsyncTool)` → `AsyncBrowserTool(Tool)`. One-line
body change + docstring update. Subclasses with `async def` action
methods keep working — `Tool.async_execute_action` dispatches them
natively.
`tests/test_tool.py`:
* Replaced `test_async_tool_rejects_sync_action_at_class_definition`
(the validation it asserted is intentionally removed) with
`test_async_tool_subclass_accepts_mixed_methods` covering the new
affordance + deprecation warning.
* Added the 4-cell dispatch matrix: (sync caller × {sync, async} action)
× (async caller × {sync, async} action).
* Added `test_sync_caller_async_action_bridge_inside_running_loop` —
the motivating use case (Agent._run inside Episode's asyncio.run).
* Added `test_async_caller_parallel_gather_over_sync_actions` —
real OS-thread parallelism via to_thread inside asyncio.gather.
* Added deprecation-warning tests for `AsyncTool` subclass and
`AsyncToolbox.execute_action`.
* Added `test_asynctool_subclass_isinstance_of_abstract_async_tool` —
backward compat for isinstance checks across the alias.
`scripts/smoke/tool_consolidation.py`:
* 9-check end-to-end smoke covering the 4-cell matrix, real
parallelism (4×100ms sync sleeps via gather → <250ms wall-clock),
bridge-from-inside-running-loop, and both deprecation shims.
Test status:
* `pytest tests/test_tool.py`: 52 passed, 16 deprecation warnings
(all intentional — alias subclassing).
* `scripts/smoke/tool_consolidation.py`: 9/9 cells pass.
* `make lint`: clean.
Signed-off-by: Alexandre Lacoste <alex.lacoste.shmu@gmail.com>
Signed-off-by: Cube Harness <cube-harness@example.com>
* feat(tool): drop AsyncTool/AsyncToolbox/AbstractAsyncTool — no deprecation window
The previous commit shipped these as deprecation shims preserving the
legacy async-execute_action contract for one release window. We're
pre-1.0, so removing them outright is cleaner:
- `Tool` already supports sync OR async `@tool_action` methods on one class.
- `Toolbox.async_execute_action` is the canonical async call-site.
- `AbstractTool.async_execute_action` is the canonical async base.
Drops ~280 LOC of shim machinery (classes, deprecation tests, smoke
cells, docstring history). Paired cube-harness PR migrates the only
remaining consumer (`MonitoredTool`, `as_async`, `Agent.run` isinstance
checks).
Net diff: +153 / -581 over the previous shimmed commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Alexandre Lacoste <alex.lacoste.shmu@gmail.com>
---------
Signed-off-by: Alexandre Lacoste <alex.lacoste.shmu@gmail.com>
Signed-off-by: Cube Harness <cube-harness@example.com>
Co-authored-by: Cube Harness <cube-harness@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>1 parent ca29d14 commit fe5e465
7 files changed
Lines changed: 931 additions & 354 deletions
File tree
- openspec/changes/tool-consolidation
- scripts/smoke
- src/cube
- tools
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
0 commit comments