feat: expose background_tasks in PluginResult for fire-and-forget synchronization#33
Conversation
…chronization Adds a background_tasks field to PluginResult containing the asyncio.Task handles created for FIRE_AND_FORGET plugins. Callers can now await background tasks deterministically instead of relying on arbitrary sleep delays. Closes contextforge-org#25 Signed-off-by: Alex Bozarth <ajbozart@us.ibm.com>
araujof
left a comment
There was a problem hiding this comment.
Thanks for this PR @ajbozarth! Nice work. This is a good change that preserves backwards compatibility. The test migration from asyncio.sleep() to direct task awaits is also a good win for determinism and CI reliability.
What I verified
- All scheduling paths (
execute()normal completion +_build_halt_resulthalt path) correctly populatebackground_tasks. exclude=Truekeeps the field out ofmodel_dump()/model_dump_json(), so MCP, gRPC, and isolated-worker serialization are unaffected.arbitrary_types_allowed=Trueis consistent with the existing pattern onPluginPayload.- Task lifecycle is improved, callers now hold strong references to fire-and-forget tasks via the result.
Worth noting
- Remote callers (MCP/gRPC/isolated) always receive
background_tasks=[]; they can't synchronize with tasks in the remote process. Not a bug, but worth a docstring mention if remote sync ever becomes a requirement. - Callers now have
.cancel()access on fire-and-forget tasks. If that's undesirable, wrapping inasyncio.shieldor returningSequence[Awaitable]would restrict it. Not required today.
@terylt can you review to see if you spot any issues, specially any undesirable interactions with the upcoming Rust Core for CPEX?
|
Overall @ajbozarth Nice work and thanks for the contribution! I have one suggestion just to help keep parity with the rust core we are building out. Rather than exposing The internal list can stay for advanced use cases, but the method gives a cleaner public API and a consistent return type for error handling. This also aligns with the Rust core — |
Signed-off-by: Alex Bozarth <ajbozart@us.ibm.com>
|
Thanks for the feedback — addressed in the second commit.
|
terylt
left a comment
There was a problem hiding this comment.
LGTM. Nice work @ajbozarth!
Summary
Adds a
background_tasksfield toPluginResultcontaining theasyncio.Taskhandles created for
FIRE_AND_FORGETplugins. Callers can now await backgroundtasks deterministically instead of using arbitrary sleep delays.
Closes: #25
Changes
PluginResultgains abackground_tasks: list[asyncio.Task]field (excludedfrom serialization; defaults to empty list for backward compatibility)
_fire_and_forget_tasks()now returns the list of task handles instead ofNoneexecute()populateresult.background_taskstest_plugin_modes.pyupdated to useawait asyncio.gather(*result.background_tasks, return_exceptions=True)in place of
asyncio.sleep()workaroundsPluginResultentry indocs/specs/plugin-framework-spec.mdupdatedChecks
make lintpassesmake testpassesNotes
arbitrary_types_allowed=Trueis added toPluginResult.model_configtoallow Pydantic to accept
asyncio.Taskas a field type — the same configalready used on
PluginPayload. Theexclude=Trueon the field keeps.model_dump()/.model_dump_json()output unchanged.