diff --git a/ai/prompt-io/opencode/20260819T184640Z_481ba003_prompt_io.md b/ai/prompt-io/opencode/20260819T184640Z_481ba003_prompt_io.md new file mode 100644 index 00000000..71bd88ea --- /dev/null +++ b/ai/prompt-io/opencode/20260819T184640Z_481ba003_prompt_io.md @@ -0,0 +1,56 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3 +timestamp: 2026-08-19T18:46:40Z +git_ref: 481ba003 +scope: code +substantive: true +raw_file: 20260819T184640Z_481ba003_prompt_io.raw.md +--- + +## Prompt + +Rebase PR #484 onto final PR #481, migrate every affected one-shot call +to the new positional target API and continue through downstream tests, +examples and documentation review. + +## Response summary + +Converted stale target keyword calls to target partials so previously +named inputs remain explicit while placement/runtime controls stay +direct. Updated error expectations for local signature validation and +linked remote error propagation, then corrected docs which still +described the removed one-shot implementation. Linked spawning and +context lifecycle prose to the corresponding API methods and detailed +context guide. + +## Files changed + +- `docs/api/core.rst` - describe linked one-shot context execution. +- `docs/guide/rpc.rst` - update placement and target call semantics. +- `docs/guide/spawning.rst` - document positional target inputs. +- `examples/debugging/multi_nested_subactors_error_up_through_nurseries.py` - migrate nested actor target inputs. +- `examples/debugging/root_cancelled_but_child_is_in_tty_lock.py` - preserve named recursive target inputs with partials. +- `tests/test_advanced_streaming.py` - migrate streaming target inputs. +- `tests/test_cancellation.py` - migrate calls and tighten errors. +- `tests/test_infected_asyncio.py` - bind asyncio target options. +- `tests/test_rpc.py` - migrate RPC target argument binding. +- `tests/test_runtime.py` - preserve named runtime target inputs. +- `tests/test_spawning.py` - preserve named spawning target inputs. + +## Human edits + +The human selected the stack order and final PR #481 base, asked the +agent to continue after each diagnostic step and required a complete +commit plan after independently force-pushing the rebased history. +After reviewing the migration, the human required every formerly named +target input to remain visibly named through `functools.partial()` +rather than becoming positional. These were human-directed agent edits; +the human also required plain `start_actor()` and `open_context()` +references in the spawning and RPC guides to link to their API methods +and the detailed context guide, then clarified that `to_actor.run()` +already uses the full context API while `Portal.run()` should share +linked lifecycle machinery without necessarily delegating through +`Portal.open_context()` or adding a `Started` message. The human made +no direct source-line edits. diff --git a/ai/prompt-io/opencode/20260819T184640Z_481ba003_prompt_io.raw.md b/ai/prompt-io/opencode/20260819T184640Z_481ba003_prompt_io.raw.md new file mode 100644 index 00000000..091bf7df --- /dev/null +++ b/ai/prompt-io/opencode/20260819T184640Z_481ba003_prompt_io.raw.md @@ -0,0 +1,30 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +timestamp: 2026-08-19T18:46:40Z +git_ref: 481ba003 +diff_cmd: git diff HEAD~1..HEAD +--- + +Migrate PR #484's downstream one-shot calls to PR #481's final +`tractor.to_actor.run()` contract after the stack rebase. + +> `git diff HEAD~1..HEAD -- docs examples tests` + +Pass target arguments positionally and bind target keyword-only inputs +with `functools.partial()`. Keep placement and runtime controls as +direct `to_actor.run()` keywords. Update the invalid-target-argument +test to expect local signature binding before actor startup and require +direct `RemoteActorError` propagation from linked one-shots. + +Update API and guide prose to describe positional target inputs, +linked `Portal.open_context()` execution and per-child reaping instead +of the removed `Portal.run()` and target-`**kwargs` conventions. + +Verification: + +- core and migrated runtime batches: `97 passed` +- discovery and related lifecycle batch: `33 passed, 1 skipped` +- changed executable examples: `9 passed` +- mapped debugger cases: `12 passed, 6 skipped` +- Ruff, compilation and `git diff --check`: clean diff --git a/docs/api/core.rst b/docs/api/core.rst index e57ba5c7..5c311754 100644 --- a/docs/api/core.rst +++ b/docs/api/core.rst @@ -58,10 +58,10 @@ One-shot task actors ``trio.to_thread.run_sync()`` and friends) is the *convenience* one-shot — spawn, run a single task, block on its result, reap — built entirely on - :meth:`ActorNursery.start_actor` + :meth:`Portal.run` + - :meth:`Portal.cancel_actor`, so don't design around it as the - core model. It supersedes the removed (legacy, non-blocking) - ``ActorNursery.run_in_actor()``. + :meth:`ActorNursery.start_actor`, a linked + :meth:`Portal.open_context` call and per-child cancellation/reaping, + so don't design around it as the core model. It supersedes the + removed (legacy, non-blocking) ``ActorNursery.run_in_actor()``. .. deprecated:: 0.1.0a6 diff --git a/docs/guide/rpc.rst b/docs/guide/rpc.rst index 29cef43c..5b76d824 100644 --- a/docs/guide/rpc.rst +++ b/docs/guide/rpc.rst @@ -89,7 +89,12 @@ in one blocking call: .. code:: python - final = await tractor.to_actor.run(fib, an=an, n=10) + from functools import partial + + final = await tractor.to_actor.run( + partial(fib, n=10), + an=an, + ) Semantics worth knowing: @@ -98,9 +103,10 @@ Semantics worth knowing: task. - "placement" is composable: ``an=`` spawns from an existing actor-nursery, ``portal=`` reuses an already-running actor - (no spawn/reap, just a ``Portal.run()``), and passing - neither opens a private call-scoped nursery (booting the - runtime if needed). + (no spawn/reap, just a linked + :meth:`~tractor.Portal.open_context` call; see the + :doc:`context guide `), and passing neither + opens a private call-scoped nursery (booting the runtime if needed). - concurrency composes the plain ``trio`` way: schedule multiple ``run()`` calls into a local task nursery (see ``examples/parallelism/to_actor_one_shots.py``). @@ -149,7 +155,8 @@ call tears down the entire sub-tree — SC, transitively. When to graduate to ``Context`` ------------------------------- -``portal.run()`` is great for one-shot, request-response calls. +The :meth:`~tractor.Portal.run` method is great for one-shot, +request-response calls. Reach for :meth:`~tractor.Portal.open_context` with an ``@tractor.context`` endpoint as soon as you want: @@ -162,10 +169,15 @@ Reach for :meth:`~tractor.Portal.open_context` with an :meth:`~tractor.Portal.cancel_actor` nukes the **entire** remote runtime and its process. -In fact the source plans for ``Portal.run()`` itself to be -rebuilt on top of ``open_context()`` — contexts *are* the core -inter-actor protocol. Take the full tour in -:doc:`/guide/context`. +:func:`tractor.to_actor.run` already enters the full +:meth:`~tractor.Portal.open_context` lifecycle. The older +:meth:`~tractor.Portal.run` path instead uses the ``Context`` returned +by the lower-level ``Actor.start_remote_task()`` directly, avoiding a +``Started`` handshake but owning less lifecycle machinery. A follow-up +should factor their shared linked-task lifecycle without requiring +``Portal.run()`` to delegate through the public context API or add +another wire message. Take the full tour in +:doc:`the context guide `. .. seealso:: diff --git a/docs/guide/spawning.rst b/docs/guide/spawning.rst index 8f01bc1a..f56ad4ec 100644 --- a/docs/guide/spawning.rst +++ b/docs/guide/spawning.rst @@ -91,17 +91,17 @@ somebody-ing: What's going on here? -- ``start_actor('frank', enable_modules=[__name__])`` forks off +- :meth:`~tractor.ActorNursery.start_actor` forks off a new process, boots a ``tractor`` runtime inside it, and allows it to serve functions from the current module (see the allowlist section below). -- each ``await portal.run(...)`` schedules a *new* task in +- each :meth:`~tractor.Portal.run` call schedules a *new* task in frank's task tree and waits on its result — the full RPC story lives in :doc:`/guide/rpc`. - frank has no main task to complete, so without the final - ``await portal.cancel_actor()`` the nursery block would wait - on him **forever**. Daemon lifetimes are *yours* to end; that - explicitness is the point. + :meth:`~tractor.Portal.cancel_actor` call the nursery block would + wait on him **forever**. Daemon lifetimes are *yours* to end; + that explicitness is the point. ``to_actor.run()``: quick one-shot parallelism ---------------------------------------------- @@ -126,7 +126,9 @@ A few details worth knowing: ``name='something_cuter'``. - the function's module is auto-added to the child's ``enable_modules`` allowlist. -- extra ``**kwargs`` are forwarded to the function itself. +- target arguments are positional; use ``functools.partial()`` + to bind target keyword arguments. Keywords passed directly to + ``run()`` configure actor placement and spawning. - the call blocks until the result (or error) lands and the child is *auto-cancelled* (reaped) right after — so remote errors raise directly in your calling task (causality_ is @@ -138,14 +140,16 @@ A few details worth knowing: .. note:: - ``to_actor.run()`` is a convenience, **not** the core model — - it's built *entirely* on ``start_actor()`` + ``Portal.run()`` - + ``Portal.cancel_actor()``. Teach your fingers to use it for - quick fire-and-collect parallelism — think a per-function - trio-parallel_ style one-shot — and reach for - ``start_actor()`` + ``open_context()`` for anything - long-lived, stateful or streaming - (:doc:`/guide/context`). + :func:`tractor.to_actor.run` is a convenience, **not** the core + model — it's built *entirely* on + :meth:`~tractor.ActorNursery.start_actor` plus a linked + :meth:`~tractor.Portal.open_context` call and per-child + cancellation/reaping. Teach your fingers to use it for quick + fire-and-collect parallelism — think a per-function trio-parallel_ + style one-shot — and reach for + :meth:`~tractor.ActorNursery.start_actor` plus + :meth:`~tractor.Portal.open_context` for anything long-lived, + stateful or streaming; see :doc:`/guide/context`. Actor lifetimes and teardown order ---------------------------------- @@ -154,10 +158,11 @@ So we have two lifetime flavors: - **one-shot** (``to_actor.run()``): lives exactly as long as its single task; reaped the moment its result (or error) arrives back in the (blocking) call. -- **daemon** (``start_actor()``): lives until *someone* cancels - it — an explicit ``await portal.cancel_actor()``, a bulk - ``await an.cancel()``, or the one-cancels-all strategy kicking - in on error. +- **daemon** (:meth:`~tractor.ActorNursery.start_actor`): lives + until *someone* cancels it — an explicit + :meth:`~tractor.Portal.cancel_actor`, a bulk + :meth:`~tractor.ActorNursery.cancel`, or the one-cancels-all + strategy kicking in on error. On a clean exit of the nursery block the teardown order is: diff --git a/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py b/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py index fe50d9a4..a18587dc 100644 --- a/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py +++ b/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py @@ -51,9 +51,11 @@ async def spawn_until(depth=0): # `name_error` relays through. depth -= 1 await tractor.to_actor.run( - spawn_until, + partial( + spawn_until, + depth=depth, + ), an=an, - depth=depth, name=f'spawn_until_{depth}', ) @@ -90,18 +92,22 @@ async def main(): tn.start_soon( partial( tractor.to_actor.run, - spawn_until, + partial( + spawn_until, + depth=3, + ), an=an, - depth=3, name='spawner0', ) ) tn.start_soon( partial( tractor.to_actor.run, - spawn_until, + partial( + spawn_until, + depth=4, + ), an=an, - depth=4, name='spawner1', ) ) diff --git a/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py b/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py index 083a7fb3..7b2e4d5a 100644 --- a/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py +++ b/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py @@ -18,9 +18,11 @@ async def spawn_until(depth=0): else: depth -= 1 await tractor.to_actor.run( - spawn_until, + partial( + spawn_until, + depth=depth, + ), an=an, - depth=depth, name=f'spawn_until_{depth}', ) @@ -51,9 +53,11 @@ async def main(): tn.start_soon( partial( tractor.to_actor.run, - spawn_until, + partial( + spawn_until, + depth=1, + ), an=an, - depth=1, name='spawner1', ) ) @@ -61,9 +65,11 @@ async def main(): # ..while blocking on the shallow (faster to fail) tree # whose propagated error triggers nursery cancellation. await tractor.to_actor.run( - spawn_until, + partial( + spawn_until, + depth=0, + ), an=an, - depth=0, name='spawner0', ) diff --git a/tests/test_advanced_streaming.py b/tests/test_advanced_streaming.py index 8d5b7d1b..194eb7d1 100644 --- a/tests/test_advanced_streaming.py +++ b/tests/test_advanced_streaming.py @@ -248,10 +248,12 @@ def test_dynamic_pub_sub( tn.start_soon( partial( tractor.to_actor.run, - consumer, + partial( + consumer, + subs=[sub], + ), an=an, name=f'consumer_{sub}', - subs=[sub], ) ) @@ -259,10 +261,12 @@ def test_dynamic_pub_sub( tn.start_soon( partial( tractor.to_actor.run, - consumer, + partial( + consumer, + subs=list(_registry.keys()), + ), an=an, name='consumer_dynamic', - subs=list(_registry.keys()), ) ) diff --git a/tests/test_cancellation.py b/tests/test_cancellation.py index 54a67981..2df852e5 100644 --- a/tests/test_cancellation.py +++ b/tests/test_cancellation.py @@ -104,7 +104,7 @@ async def do_nuthin(): [ # expected to be thrown in assert_err ({}, AssertionError), - # argument mismatch raised in _invoke() + # argument mismatch rejected locally before spawn ({'unexpected': 10}, TypeError) ], ids=['no_args', 'unexpected_args'], @@ -130,48 +130,34 @@ def test_remote_error( # `to_actor.run()` blocks on the one-shot's result and # raises the remote error directly here in the caller's - # task (a bad-arg `TypeError` likewise relays as a - # `RemoteActorError`). + # task. Invalid target args fail local signature binding + # before any one-shot actor is spawned. try: await tractor.to_actor.run( - assert_err, + partial(assert_err, **args), an=an, name='errorer', - **args ) except tractor.RemoteActorError as err: assert err.boxed_type == errtype print("Look Maa that actor failed hard, hehh") raise - # ensure boxed errors + # Invalid args never cross the process boundary. if args: - with pytest.raises(tractor.RemoteActorError) as excinfo: + with pytest.raises(errtype): + trio.run(main) + + else: + # The linked one-shot raises the child's boxed error + # directly in this caller task. + with pytest.raises( + tractor.RemoteActorError, + ) as excinfo: trio.run(main) assert excinfo.value.boxed_type == errtype - else: - # the root task will also error on the `Portal.result()` - # call so we expect an error from there AND the child. - # |_ tho seems like on new `trio` this doesn't always - # happen? - with pytest.raises(( - BaseExceptionGroup, - tractor.RemoteActorError, - )) as excinfo: - trio.run(main) - - # ensure boxed errors are `errtype` - err: BaseException = excinfo.value - if isinstance(err, BaseExceptionGroup): - suberrs: list[BaseException] = err.exceptions - else: - suberrs: list[BaseException] = [err] - - for exc in suberrs: - assert exc.boxed_type == errtype - def test_multierror( reg_addr: tuple[str, int], @@ -391,10 +377,9 @@ async def test_some_cancels_all( tn.start_soon( partial( tractor.to_actor.run, - func, + partial(func, **kwargs), an=an, name=f'actor_{i}', - **kwargs, ) ) @@ -469,12 +454,14 @@ async def spawn_and_error( if depth > 0: args = ( - spawn_and_error, + partial( + spawn_and_error, + breadth=breadth, + depth=depth - 1, + ), ) kwargs = { 'name': f'spawner_{i}_depth_{depth}', - 'breadth': breadth, - 'depth': depth - 1, } else: args = ( @@ -705,11 +692,13 @@ async def test_nested_multierrors( tn.start_soon( partial( tractor.to_actor.run, - spawn_and_error, + partial( + spawn_and_error, + breadth=subactor_breadth, + depth=depth, + ), an=an, name=f'spawner_{i}', - breadth=subactor_breadth, - depth=depth, ) ) except ( diff --git a/tests/test_infected_asyncio.py b/tests/test_infected_asyncio.py index 4ba1776a..933b243b 100644 --- a/tests/test_infected_asyncio.py +++ b/tests/test_infected_asyncio.py @@ -5,7 +5,7 @@ The hipster way to force SC onto the stdlib's "async": 'infection mode'. import asyncio import builtins from contextlib import ExitStack -# from functools import partial +from functools import partial import itertools import importlib import os @@ -209,10 +209,12 @@ def test_aio_simple_error( debug_mode=debug_mode, ) as an: await to_actor.run( - asyncio_actor, + partial( + asyncio_actor, + target='sleep_and_err', + expect_err='AssertionError', + ), an=an, - target='sleep_and_err', - expect_err='AssertionError', infect_asyncio=True, ) @@ -455,10 +457,14 @@ def test_aio_cancelled_from_aio_causes_trio_cancelled( # relays the remote error here in the caller's task. with trio.fail_after(1 + delay): await to_actor.run( - asyncio_actor, + partial( + asyncio_actor, + target='aio_cancel', + expect_err=( + 'tractor.to_asyncio.AsyncioCancelled' + ), + ), an=an, - target='aio_cancel', - expect_err='tractor.to_asyncio.AsyncioCancelled', infect_asyncio=True, ) @@ -663,10 +669,12 @@ def test_basic_interloop_channel_stream( ) as an: # should raise RAE diectly await to_actor.run( - stream_from_aio, + partial( + stream_from_aio, + fan_out=fan_out, + ), an=an, infect_asyncio=True, - fan_out=fan_out, ) trio.run(main) @@ -682,9 +690,11 @@ def test_trio_error_cancels_intertask_chan( ) as an: # should trigger remote actor error await to_actor.run( - stream_from_aio, + partial( + stream_from_aio, + trio_raise_err=True, + ), an=an, - trio_raise_err=True, infect_asyncio=True, ) @@ -719,9 +729,11 @@ def test_trio_closes_early_causes_aio_checkpoint_raise( # should raise RAE diectly print('waiting on final infected subactor result..') res: None = await to_actor.run( - stream_from_aio, + partial( + stream_from_aio, + trio_exit_early=True, + ), an=an, - trio_exit_early=True, infect_asyncio=True, ) assert res is None @@ -770,11 +782,13 @@ def test_aio_exits_early_relays_AsyncioTaskExited( # should raise RAE diectly print('waiting on final infected subactor result..') res: None = await to_actor.run( - stream_from_aio, + partial( + stream_from_aio, + trio_exit_early=False, + aio_exit_early=True, + ), an=an, infect_asyncio=True, - trio_exit_early=False, - aio_exit_early=True, ) assert res is None print(f'infected subactor returned result: {res!r}\n') @@ -809,9 +823,11 @@ def test_aio_errors_and_channel_propagates_and_closes( ) as an: # should trigger RAE directly, not an eg. await to_actor.run( - stream_from_aio, + partial( + stream_from_aio, + aio_raise_err=True, + ), an=an, - aio_raise_err=True, infect_asyncio=True, ) diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 0c69687a..80c5084a 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -4,6 +4,7 @@ related API and error checks. ''' import itertools +from functools import partial from unittest.mock import ( AsyncMock, Mock, @@ -239,19 +240,21 @@ def test_rpc_errors( actor = tractor.current_actor() assert actor.is_registrar await tractor.to_actor.run( - sleep_back_actor, + partial( + sleep_back_actor, + actor_name=subactor_requests_to, + func_name=funcname, + func_defined=bool(func_defined), + exposed_mods=exposed_mods, + reg_addr=reg_addr, + ), an=an, - actor_name=subactor_requests_to, name='subactor', - # function from the local exposed module space - # the subactor will invoke when it RPCs back to this actor - func_name=funcname, - exposed_mods=exposed_mods, - func_defined=True if func_defined else False, + # Function from the local exposed module space the + # subactor invokes when it RPCs back to this actor. enable_modules=subactor_exposed_mods, - reg_addr=reg_addr, ) def run(): diff --git a/tests/test_runtime.py b/tests/test_runtime.py index a168215e..817e51fb 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -2,6 +2,7 @@ Verifying internal runtime state and undocumented extras. """ +from functools import partial import os import pytest @@ -84,10 +85,12 @@ async def test_lifetime_stack_wipes_tmpfile( loglevel=loglevel, ) as an: await tractor.to_actor.run( - crash_and_clean_tmpdir, + partial( + crash_and_clean_tmpdir, + tmp_file_path=path, + error=error_in_child, + ), an=an, - tmp_file_path=path, - error=error_in_child, ) except ( tractor.RemoteActorError, diff --git a/tests/test_spawning.py b/tests/test_spawning.py index ce9db115..38e37400 100644 --- a/tests/test_spawning.py +++ b/tests/test_spawning.py @@ -51,18 +51,18 @@ async def spawn( # recursively spawn this same `spawn()` fn as the lone # task of a one-shot child subactor and get its result. result = await tractor.to_actor.run( - spawn, + partial( + spawn, + should_be_root=False, + data=data_to_pass_down, + reg_addr=reg_addr, + ), an=an, # spawning args name='sub-actor', enable_modules=[__name__], - # passed to a subactor-recursive RPC invoke - # of this same `spawn()` fn. - should_be_root=False, - data=data_to_pass_down, - reg_addr=reg_addr, ) assert result == 10 return result @@ -152,9 +152,11 @@ async def test_most_beautiful_word( debug_mode=debug_mode, ) as an: res: Any = await tractor.to_actor.run( - cellar_door, + partial( + cellar_door, + return_value=return_value, + ), an=an, - return_value=return_value, name='some_linguist', ) assert res == return_value @@ -204,10 +206,12 @@ def test_loglevel_propagated_to_subactor( ) as an: await tractor.to_actor.run( - check_loglevel, + partial( + check_loglevel, + level=level, + ), an=an, loglevel=level, - level=level, ) trio.run(main) @@ -273,19 +277,23 @@ def test_to_actor_run_can_skip_parent_main_inheritance( # Default: child receives parent __main__ bootstrap data await tractor.to_actor.run( - check_parent_main_inheritance, + partial( + check_parent_main_inheritance, + expect_inherited=True, + ), an=an, name='replaying-parent-main', - expect_inherited=True, ) # Opt-out: child gets no parent __main__ data await tractor.to_actor.run( - check_parent_main_inheritance, + partial( + check_parent_main_inheritance, + expect_inherited=False, + ), an=an, name='isolated-parent-main', inherit_parent_main=False, - expect_inherited=False, ) trio.run(main)