Skip to content

feat(py): respond/restart improvements#5004

Draft
huangjeff5 wants to merge 9 commits intomainfrom
jh-interrupts
Draft

feat(py): respond/restart improvements#5004
huangjeff5 wants to merge 9 commits intomainfrom
jh-interrupts

Conversation

@huangjeff5
Copy link
Copy Markdown
Contributor

@huangjeff5 huangjeff5 commented Mar 26, 2026

Adds restart support in the Python SDK and other improvements.

Tools (or wrapped tools) can raise Interrupt, which stops the current generation turn and surfaces the pending ToolRequestPart with interrupt metadata.

Now, The execution flow is paused, giving the caller the option to either respond, and restart the tool execution with replaced input and resumed metadata.

See samples for update usage.

Core Changes

  • Moved interrupt helper off of ToolRunContext. Now you raise Interrupt to trigger interruptions.
  • Added define_interrupt primitive, which provides the default Interrupt tool, with the option to provide a input schema to constrain the model call.
  • Renamed tool_response helper to respond_to_interrupt, and added restart_interrupted_tool to support the restart use case.
  • In the restart use case, use ContextVars to store _resumed_metadata and _replaced_input and propagate those to the tool inside tool_fn_wrapper.
  • Resume options were flattened and added directly onto the generate() signature.

Related Changes

  • You can pass tools directly inline (as opposed to requiring a str). tools signature now accepts str, Action, or Callable.
    • Flattened prompt signature so that there isn't a separate opts argument; those PromptGenerationOptions are flat kwargs now.
  • I ran into 4xx issues with Gemini when sending tools with scalar inputs. To solve this, I have the genai plugin convert the input schema into an object with 'value' key. And in the core framework, when handling this, we handle this special case and unwrap it.
  • Tools were running sequentially, not in parallel. This PR fixes that.

@github-actions github-actions bot added docs Improvements or additions to documentation python Python config labels Mar 26, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant refactor of the tool interrupt and resume mechanism within the Genkit Python SDK. Key changes include replacing the generic tool_responses parameter with more explicit resume_respond, resume_restart, and resume_metadata options in the generate and prompt execution APIs. A new Interrupt exception has been added for tools to signal pauses, complemented by helper functions like respond_to_interrupt and restart_interrupted_tool for resuming execution. Additionally, the ExecutablePrompt API was updated to accept configuration options as keyword arguments rather than a single opts dictionary. The Google AI plugin was also updated to handle scalar tool inputs for Gemini and to correctly report the finish_reason in streaming responses. I have no feedback to provide as there are no review comments to assess.


async def resolve_tool(registry: Registry, tool_name: str) -> Action:
"""Resolve a tool by name from the registry."""
"""Resolve a :class:`~genkit.ActionKind.TOOL` action by name from the registry."""
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix comment weirdness

if tool is None:
raise ValueError(f'Unable to resolve tool {tool_name}')
return tool
if tool is not None:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert this

interrupt=interrupt,
metadata={'source': 'cli', 'path': 'respond'},
)
assert isinstance(interrupt_response, ToolResponsePart)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't need to assert here

},
]

pm.responses.append(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it make sense to have this be in the pm response? I think this is constructed via framework calls



@pytest.mark.asyncio
async def test_tool_either_interrupts_or_returns() -> None:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test is unclear

'toolResponse': {'ref': 'ra', 'name': 'a', 'output': {'done': True}},
'metadata': {'interruptResponse': True},
},
{'toolResponse': {'ref': 'rb', 'name': 'b', 'output': 'b-done'}},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirm no metadata is correct on restart case

resume=Resume(),
),
)
assert 'replies' in ei.value.original_message or 'restarts' in ei.value.original_message
Copy link
Copy Markdown
Contributor Author

@huangjeff5 huangjeff5 Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better assert, can't tell this is is verifying the GenkitError



@pytest.mark.asyncio
async def test_pending_output_trp_yields_tool_response_with_source_pending() -> None:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this scenario doesn't seem realistic because there is no interrupt in this case?

resume=Resume(),
),
)
assert 'model' in ei.value.original_message.lower()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better assert, can't tell this is is verifying the GenkitError

out = pay.restart(None, interrupt=interrupt_trp, resumed_metadata={'k': 'v'})
assert isinstance(out, ToolRequestPart)
assert out.metadata is not None
assert out.metadata.get('resumed') == {'k': 'v'}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it include: resolvedInterrupt: {'reason': 'hold'}

with pytest.raises(GenkitError) as ei:
await run_tool_after_restart(action, restart_trp)
msg = ei.value.original_message.lower()
assert 'restart' in msg or 'nested' in msg or 'not supported' in msg
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more clear assert

Comment on lines +250 to +251
async def test_run_tool_after_restart_response_preserves_ref() -> None:
"""run_tool_after_restart produces a ToolResponsePart whose ref matches the restart TRP's ref."""
Copy link
Copy Markdown
Contributor Author

@huangjeff5 huangjeff5 Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set up a more realistic scenario
confirm that run_tool_after_restart uses the new input

system: str | list[Part] | None = None,
messages: list[Message] | None = None,
tools: list[str] | None = None,
tools: Sequence[str | Tool] | None = None,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document reasoning for Sequence vs. list.

"""Stream generated text, returning a ModelStreamResponse with .stream and .response."""
"""Stream generated text, returning a ModelStreamResponse with .stream and .response.

Middleware (``use=``) uses the same signatures as :meth:`generate`:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary comment (line 957-960)

def tools_to_action_names(
tools: Sequence[str | Tool] | None,
) -> list[str] | None:
"""Normalize tool arguments to registry names for :class:`GenerateActionOptions`.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove :class: and :meth: from comment.

async def _resolve_tool_request(tool: Action, tool_request_part: ToolRequestPart) -> tuple[Part | None, Part | None]:
"""Execute a tool and return (response_part, interrupt_part)."""
def _interrupt_from_tool_exc(exc: BaseException) -> Interrupt | None:
"""If ``exc`` is (or wraps) :class:`~genkit._ai._tools.Interrupt`, return that interrupt."""
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove :class:

"""Resolve a single tool request from pending output or resume.respond list."""
async def _resolve_resumed_tool_request(
registry: Registry, raw_request: GenerateActionOptions, tool_request_part: Part
) -> tuple[Part, Part]:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_resolve_resumed_tool_request should return tuple[ToolRequestPart, ToolResponsePart]. not [Part, Part]


P = ParamSpec('P')
T = TypeVar('T')
# ------------------------------------------------------------------
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these # ---- Sections

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config docs Improvements or additions to documentation python Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants