-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
fix(subagent): enforce handoff args and provider fallback #6873
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
f7897d1
15662ae
f7de29a
d8429ae
a6a98d7
2601a7e
1ad91d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,8 @@ def __init__( | |
| def default_parameters(self) -> dict: | ||
| return { | ||
| "type": "object", | ||
| "required": ["input"], | ||
| "additionalProperties": False, | ||
|
Comment on lines
+42
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Making With |
||
| "properties": { | ||
| "input": { | ||
| "type": "string", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -343,3 +343,96 @@ async def _fake_convert_to_file_path(self): | |
| ) | ||
|
|
||
| assert image_urls == [] | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_execute_handoff_rejects_empty_input(): | ||
| async def _fake_get_current_chat_provider_id(_umo): | ||
| return "provider-id" | ||
|
|
||
| async def _fake_tool_loop_agent(**_kwargs): | ||
| return SimpleNamespace(completion_text="ok") | ||
|
|
||
| context = SimpleNamespace( | ||
| get_current_chat_provider_id=_fake_get_current_chat_provider_id, | ||
|
Comment on lines
+348
to
+357
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): Strengthen empty-input test by asserting the handoff agent is never invoked To fully validate the early-rejection behavior, also assert that Suggested implementation: @pytest.mark.asyncio
async def test_execute_handoff_rejects_empty_input():
tool_loop_called = False
async def _fake_get_current_chat_provider_id(_umo):
return "provider-id"
async def _fake_tool_loop_agent(**_kwargs):
nonlocal tool_loop_called
tool_loop_called = True
return SimpleNamespace(completion_text="ok")
context = SimpleNamespace(
get_current_chat_provider_id=_fake_get_current_chat_provider_id,
tool_loop_agent=_fake_tool_loop_agent,To fully implement the strengthened test, you should also:
|
||
| tool_loop_agent=_fake_tool_loop_agent, | ||
| get_config=lambda **_kwargs: {"provider_settings": {}}, | ||
| ) | ||
| event = _DummyEvent([]) | ||
| run_context = ContextWrapper(context=SimpleNamespace(event=event, context=context)) | ||
| tool = SimpleNamespace( | ||
| name="transfer_to_subagent", | ||
| provider_id=None, | ||
| agent=SimpleNamespace( | ||
| name="subagent", | ||
| tools=[], | ||
| instructions="subagent-instructions", | ||
| begin_dialogs=[], | ||
| run_hooks=None, | ||
| ), | ||
| ) | ||
|
|
||
| results = [] | ||
| async for result in FunctionToolExecutor._execute_handoff( | ||
| tool, | ||
| run_context, | ||
| image_urls_prepared=True, | ||
| input=" ", | ||
| image_urls=[], | ||
| ): | ||
| results.append(result) | ||
|
|
||
| assert len(results) == 1 | ||
| assert isinstance(results[0], mcp.types.CallToolResult) | ||
| text_content = results[0].content[0] | ||
| assert isinstance(text_content, mcp.types.TextContent) | ||
| assert "missing_or_empty_input" in text_content.text | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_execute_handoff_falls_back_to_current_provider_when_configured_missing(): | ||
| captured: dict = {} | ||
|
|
||
| class _DummyProviderManager: | ||
| async def get_provider_by_id(self, _provider_id: str): | ||
| return None | ||
|
|
||
| async def _fake_get_current_chat_provider_id(_umo): | ||
| return "fallback-provider" | ||
|
|
||
| async def _fake_tool_loop_agent(**kwargs): | ||
| captured.update(kwargs) | ||
| return SimpleNamespace(completion_text="ok") | ||
|
|
||
| context = SimpleNamespace( | ||
| provider_manager=_DummyProviderManager(), | ||
| get_current_chat_provider_id=_fake_get_current_chat_provider_id, | ||
| tool_loop_agent=_fake_tool_loop_agent, | ||
| get_config=lambda **_kwargs: {"provider_settings": {}}, | ||
| ) | ||
| event = _DummyEvent([]) | ||
| run_context = ContextWrapper(context=SimpleNamespace(event=event, context=context)) | ||
| tool = SimpleNamespace( | ||
| name="transfer_to_subagent", | ||
| provider_id="missing-provider-id", | ||
| agent=SimpleNamespace( | ||
| name="subagent", | ||
| tools=[], | ||
| instructions="subagent-instructions", | ||
| begin_dialogs=[], | ||
| run_hooks=None, | ||
| ), | ||
| ) | ||
|
|
||
| results = [] | ||
| async for result in FunctionToolExecutor._execute_handoff( | ||
| tool, | ||
| run_context, | ||
| image_urls_prepared=True, | ||
| input="hello", | ||
| image_urls=[], | ||
| ): | ||
| results.append(result) | ||
|
|
||
| assert len(results) == 1 | ||
| assert captured["chat_provider_id"] == "fallback-provider" | ||
Uh oh!
There was an error while loading. Please reload this page.