mirror of
https://github.com/strands-agents/harness-sdk.git
synced 2026-10-02 02:44:48 +08:00
feat(bidi): support cancellation from custom tools (#4664)
This commit is contained in:
@@ -177,7 +177,9 @@ Each grouped tool-use message stays adjacent to its matching result message, eve
|
||||
|
||||
Tool-result messages use `metadata["custom"]["bidi"]["kind"]` to distinguish `tool_dispatch` acknowledgements from `tool_result` messages.
|
||||
|
||||
After the group finishes, the agent loop checks `request_state["stop_event_loop"]` to trigger graceful shutdown instead of sending tool results back to the model. Any tool can set this flag to stop the conversation. The SDK's experimental `stop` tool uses this mechanism.
|
||||
To let a tool end the conversation, call `tool_context.agent.cancel()`. Cancellation takes effect only after the tool group completes. Requests from other contexts remain pending until then.
|
||||
|
||||
See [Graceful shutdown](./quickstart.mdx#graceful-shutdown) for a custom tool example.
|
||||
|
||||
### Connection Lifecycle
|
||||
|
||||
|
||||
@@ -225,8 +225,7 @@ Emitted when the streaming connection is closed.
|
||||
- `"timeout"`: Connection timed out
|
||||
- `"error"`: Error occurred
|
||||
- `"complete"`: Conversation completed normally
|
||||
- `"user_request"`: User requested closure (via the SDK's experimental `stop`
|
||||
tool or any tool that sets `request_state["stop_event_loop"]`)
|
||||
- `"user_request"`: Cancellation requested through `agent.cancel()`
|
||||
|
||||
### Response Lifecycle Events
|
||||
|
||||
|
||||
@@ -88,12 +88,10 @@ Pass your I/O streams to the agent's `run()` method to connect them to the agent
|
||||
import asyncio
|
||||
|
||||
from strands.experimental.bidi.agent import BidiAgent
|
||||
from strands.experimental.tools import stop
|
||||
|
||||
|
||||
async def main():
|
||||
# stop tool allows user to verbally stop agent execution.
|
||||
agent = BidiAgent(tools=[stop])
|
||||
agent = BidiAgent()
|
||||
await agent.run(inputs=[MyInputStream()], outputs=[MyOutputStream()])
|
||||
|
||||
|
||||
@@ -104,6 +102,8 @@ The `run()` method handles startup, execution, and shutdown for the agent and it
|
||||
streams. Inputs and outputs run concurrently, so you can mix and match implementations.
|
||||
If an I/O task fails, `run()` cancels the remaining tasks, stops the streams, and
|
||||
re-raises the exception.
|
||||
For a tool that lets users end the conversation, see
|
||||
[Graceful shutdown](./quickstart.mdx#graceful-shutdown).
|
||||
|
||||
## Audio I/O
|
||||
|
||||
@@ -126,12 +126,10 @@ import asyncio
|
||||
|
||||
from strands.experimental.bidi.agent import BidiAgent
|
||||
from strands.experimental.bidi.io import AudioIO
|
||||
from strands.experimental.tools import stop
|
||||
|
||||
|
||||
async def main():
|
||||
# stop tool allows user to verbally stop agent execution.
|
||||
agent = BidiAgent(tools=[stop])
|
||||
agent = BidiAgent()
|
||||
audio_io = AudioIO(input_device_index=1)
|
||||
|
||||
await agent.run(
|
||||
@@ -232,7 +230,6 @@ import asyncio
|
||||
from strands.experimental.bidi.agent import BidiAgent
|
||||
from strands.experimental.bidi.io import ConsoleIO
|
||||
from strands.experimental.bidi.models import OpenAIRealtimeModel
|
||||
from strands.experimental.tools import stop
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -241,7 +238,7 @@ async def main():
|
||||
transcription_model_id=None,
|
||||
params={"output_modalities": ["text"]},
|
||||
)
|
||||
agent = BidiAgent(model=model, tools=[stop])
|
||||
agent = BidiAgent(model=model)
|
||||
console_io = ConsoleIO()
|
||||
|
||||
await agent.run(
|
||||
@@ -280,11 +277,10 @@ import asyncio
|
||||
|
||||
from strands.experimental.bidi.agent import BidiAgent
|
||||
from strands.experimental.bidi.io import AudioIO, ConsoleIO
|
||||
from strands.experimental.tools import stop
|
||||
|
||||
|
||||
async def main():
|
||||
agent = BidiAgent(tools=[stop])
|
||||
agent = BidiAgent()
|
||||
console_io = ConsoleIO(placeholder="Type or speak…")
|
||||
audio_io = AudioIO(console=console_io)
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ import asyncio
|
||||
from strands.experimental.bidi.agent import BidiAgent
|
||||
from strands.experimental.bidi.io import AudioIO
|
||||
from strands.experimental.bidi.models import BedrockNovaSonicModel
|
||||
from strands.experimental.tools import stop
|
||||
from strands.vended_tools import notebook
|
||||
|
||||
|
||||
@@ -59,7 +58,7 @@ async def main() -> None:
|
||||
region="us-east-1",
|
||||
voice="tiffany",
|
||||
)
|
||||
agent = BidiAgent(model=model, tools=[notebook, stop])
|
||||
agent = BidiAgent(model=model, tools=[notebook])
|
||||
audio_io = AudioIO()
|
||||
await agent.run(inputs=[audio_io.input()], outputs=[audio_io.output()])
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ import asyncio
|
||||
from strands.experimental.bidi.agent import BidiAgent
|
||||
from strands.experimental.bidi.io import AudioIO
|
||||
from strands.experimental.bidi.models import GoogleGeminiLiveModel
|
||||
from strands.experimental.tools import stop
|
||||
from strands.vended_tools import notebook
|
||||
|
||||
|
||||
@@ -55,8 +54,7 @@ async def main() -> None:
|
||||
voice="Kore",
|
||||
client_args={"api_key": "<GOOGLE_API_KEY>"},
|
||||
)
|
||||
# stop tool allows user to verbally stop agent execution.
|
||||
agent = BidiAgent(model=model, tools=[notebook, stop])
|
||||
agent = BidiAgent(model=model, tools=[notebook])
|
||||
|
||||
audio_io = AudioIO()
|
||||
await agent.run(inputs=[audio_io.input()], outputs=[audio_io.output()])
|
||||
|
||||
@@ -44,7 +44,6 @@ import asyncio
|
||||
from strands.experimental.bidi.agent import BidiAgent
|
||||
from strands.experimental.bidi.io import AudioIO
|
||||
from strands.experimental.bidi.models import OpenAIRealtimeModel
|
||||
from strands.experimental.tools import stop
|
||||
from strands.vended_tools import notebook
|
||||
|
||||
|
||||
@@ -55,8 +54,7 @@ async def main() -> None:
|
||||
voice="coral",
|
||||
api_key="<OPENAI_API_KEY>",
|
||||
)
|
||||
# stop tool allows user to verbally stop agent execution.
|
||||
agent = BidiAgent(model=model, tools=[notebook, stop])
|
||||
agent = BidiAgent(model=model, tools=[notebook])
|
||||
|
||||
audio_io = AudioIO()
|
||||
await agent.run(inputs=[audio_io.input()], outputs=[audio_io.output()])
|
||||
|
||||
@@ -406,22 +406,28 @@ See [Controlling Conversation Lifecycle](#controlling-conversation-lifecycle) fo
|
||||
|
||||
## Graceful Shutdown
|
||||
|
||||
Use the SDK's experimental `stop` tool to allow users to end conversations
|
||||
naturally. It sets `request_state["stop_event_loop"]`, which the agent loop checks
|
||||
to trigger a graceful shutdown:
|
||||
To let users end a conversation by voice, define a tool that calls `agent.cancel()`:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from strands import LocalAgent, ToolContext, tool
|
||||
from strands.experimental.bidi.agent import BidiAgent
|
||||
from strands.experimental.bidi.io import AudioIO
|
||||
from strands.experimental.bidi.models import BedrockNovaSonicModel
|
||||
from strands.experimental.tools import stop
|
||||
|
||||
|
||||
@tool(context=True)
|
||||
def end_conversation(tool_context: ToolContext[LocalAgent]) -> str:
|
||||
"""End the conversation when the user asks to stop."""
|
||||
tool_context.agent.cancel()
|
||||
return "Ending conversation"
|
||||
|
||||
|
||||
model = BedrockNovaSonicModel(model_id="amazon.nova-2-sonic-v1:0")
|
||||
agent = BidiAgent(
|
||||
model=model,
|
||||
tools=[stop],
|
||||
system_prompt="You are a helpful assistant. When the user says 'stop conversation', use the stop tool."
|
||||
tools=[end_conversation],
|
||||
system_prompt="You are a helpful assistant.",
|
||||
)
|
||||
|
||||
audio_io = AudioIO()
|
||||
@@ -431,24 +437,12 @@ async def main():
|
||||
inputs=[audio_io.input()],
|
||||
outputs=[audio_io.output()]
|
||||
)
|
||||
# Conversation ends when user says "stop conversation"
|
||||
# run() returns after the agent calls end_conversation.
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
You can also create custom stop tools using the `request_state["stop_event_loop"]` flag:
|
||||
|
||||
```python
|
||||
from strands import tool
|
||||
|
||||
@tool
|
||||
def end_session(request_state: dict) -> str:
|
||||
request_state["stop_event_loop"] = True
|
||||
return "Goodbye!"
|
||||
```
|
||||
|
||||
The agent will gracefully close the connection when any tool sets `request_state["stop_event_loop"] = True`.
|
||||
|
||||
Bidi checks for cancellation after a tool group completes and its results are recorded. Requests from other contexts remain pending until that checkpoint.
|
||||
|
||||
## Debug Logs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user