Description
A nested WorkflowExecutor forwards request-scoped runtime tools to its child workflow during the initial invocation and nested cancellation, but currently drops the tools when the child is resumed from a response.
I reproduced this on the current canonical main at:
3c670707766a8455da6491a9049cc9d575e019f0
The affected path is WorkflowExecutor._handle_response(), which currently resumes the child with:
await self.workflow.run(responses={request_id: response})
As a result, when the parent continuation is invoked with responses=... and tools=..., the resumed child response handler observes None from ctx.get_runtime_tools().
Expected behavior
Request-scoped runtime tools supplied to the parent continuation should be available to the resumed nested child.
This matches the behavior of the initial nested invocation and nested cancellation paths, which already forward the active runtime tools.
Actual behavior
The parent installs the continuation tools on its own runner context, but the nested child starts a separate Workflow.run() without those tools, leaving the child's runtime-tool context empty.
Code Sample
import asyncio
from typing import Any
from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowContext,
WorkflowExecutor,
handler,
response_handler,
)
async def main() -> None:
captured_tools: list[Any] = []
class RequestingExecutor(Executor):
@handler
async def start(self, input_data: str, ctx: WorkflowContext) -> None:
del input_data
await ctx.request_info(
"Continue?",
str,
request_id="child-request",
)
@response_handler
async def resume(
self,
request: str,
response: str,
ctx: WorkflowContext,
) -> None:
del request, response
captured_tools.append(ctx.get_runtime_tools())
child = WorkflowBuilder(
start_executor=RequestingExecutor(id="child-requester")
).build()
parent = WorkflowBuilder(
start_executor=WorkflowExecutor(
child,
id="child",
propagate_request=True,
)
).build()
first_result = await parent.run("start")
assert [
event.request_id
for event in first_result.get_request_info_events()
] == ["child-request"]
runtime_tool = object()
await parent.run(
responses={"child-request": "yes"},
tools=[runtime_tool],
)
assert captured_tools == [[runtime_tool]]
asyncio.run(main())
Error Messages / Stack Traces
No framework exception is raised.
The continuation succeeds and the child response handler executes, but the request-scoped runtime-tool context is lost:
assert [None] == [[<object object at 0x...>]]
At index 0 diff: None != [<object object at 0x...>]
Package Versions
agent-framework-core: 1.18.0 (editable source checkout at 3c67070; not separately reproduced against the published 1.18.0 wheel)
Python Version
Python 3.12.12
Additional Context
This appears to be a regression of previously supported behavior.
PR #7776 added tools=ctx.get_runtime_tools() to this exact nested response-continuation call site.
That runtime-tool argument is no longer present after the nested workflow invocation-kwargs changes in #7963.
Runtime tools are request-scoped, cleared after each workflow run, and are not checkpointed, so there is no persisted mechanism that supplies the parent continuation's tools to the resumed child.
I have a minimal fix and provider-free regression coverage ready locally. I verified the regression RED before the change and GREEN afterward, and the patched checkout passes the relevant workflow tests, the full core suite, syntax/format checks, source typing, test typing, aggregate core checks, and the core build.
The proposed change is intentionally narrow:
- forward the active continuation's runtime tools when resuming the nested child;
- leave stored
function_invocation_kwargs and client_kwargs behavior unchanged;
- leave nested cancellation behavior unchanged;
- leave checkpoint state and serialization unchanged;
- make no public API, exception-handling, or response-correlation changes.
This is a functional-correctness issue; I am not making a security-impact claim.
I would like to take this on and contribute the implementation. The minimal tested patch is already ready locally.
Per the repository guidance for approval/resume-related changes, I am checking with the core team while making the concrete implementation available as a draft for review.
Description
A nested
WorkflowExecutorforwards request-scoped runtime tools to its child workflow during the initial invocation and nested cancellation, but currently drops the tools when the child is resumed from a response.I reproduced this on the current canonical
mainat:3c670707766a8455da6491a9049cc9d575e019f0The affected path is
WorkflowExecutor._handle_response(), which currently resumes the child with:As a result, when the parent continuation is invoked with
responses=...andtools=..., the resumed child response handler observesNonefromctx.get_runtime_tools().Expected behavior
Request-scoped runtime tools supplied to the parent continuation should be available to the resumed nested child.
This matches the behavior of the initial nested invocation and nested cancellation paths, which already forward the active runtime tools.
Actual behavior
The parent installs the continuation tools on its own runner context, but the nested child starts a separate
Workflow.run()without those tools, leaving the child's runtime-tool context empty.Code Sample
import asyncio from typing import Any from agent_framework import ( Executor, WorkflowBuilder, WorkflowContext, WorkflowExecutor, handler, response_handler, ) async def main() -> None: captured_tools: list[Any] = [] class RequestingExecutor(Executor): @handler async def start(self, input_data: str, ctx: WorkflowContext) -> None: del input_data await ctx.request_info( "Continue?", str, request_id="child-request", ) @response_handler async def resume( self, request: str, response: str, ctx: WorkflowContext, ) -> None: del request, response captured_tools.append(ctx.get_runtime_tools()) child = WorkflowBuilder( start_executor=RequestingExecutor(id="child-requester") ).build() parent = WorkflowBuilder( start_executor=WorkflowExecutor( child, id="child", propagate_request=True, ) ).build() first_result = await parent.run("start") assert [ event.request_id for event in first_result.get_request_info_events() ] == ["child-request"] runtime_tool = object() await parent.run( responses={"child-request": "yes"}, tools=[runtime_tool], ) assert captured_tools == [[runtime_tool]] asyncio.run(main())Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.18.0 (editable source checkout at 3c67070; not separately reproduced against the published 1.18.0 wheel)
Python Version
Python 3.12.12
Additional Context
This appears to be a regression of previously supported behavior.
PR #7776 added
tools=ctx.get_runtime_tools()to this exact nested response-continuation call site.That runtime-tool argument is no longer present after the nested workflow invocation-kwargs changes in #7963.
Runtime tools are request-scoped, cleared after each workflow run, and are not checkpointed, so there is no persisted mechanism that supplies the parent continuation's tools to the resumed child.
I have a minimal fix and provider-free regression coverage ready locally. I verified the regression RED before the change and GREEN afterward, and the patched checkout passes the relevant workflow tests, the full core suite, syntax/format checks, source typing, test typing, aggregate core checks, and the core build.
The proposed change is intentionally narrow:
function_invocation_kwargsandclient_kwargsbehavior unchanged;This is a functional-correctness issue; I am not making a security-impact claim.
I would like to take this on and contribute the implementation. The minimal tested patch is already ready locally.
Per the repository guidance for approval/resume-related changes, I am checking with the core team while making the concrete implementation available as a draft for review.