From 2039dcb545d971af04e537b8805f60d42f64a591 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 12 Mar 2026 15:45:47 -0700 Subject: [PATCH 1/3] Update the signatures of `CopilotSession.send()` and `send_and_wait()` --- python/README.md | 22 +++---- python/copilot/__init__.py | 2 - python/copilot/client.py | 4 +- python/copilot/session.py | 58 +++++++++++-------- python/copilot/types.py | 11 ---- python/e2e/test_agent_and_compact_rpc.py | 2 +- python/e2e/test_ask_user.py | 24 ++------ python/e2e/test_compaction.py | 12 ++-- python/e2e/test_hooks.py | 12 ++-- python/e2e/test_mcp_and_agents.py | 20 +++---- python/e2e/test_multi_client.py | 40 ++++--------- python/e2e/test_permissions.py | 28 ++++----- python/e2e/test_session.py | 49 ++++++---------- python/e2e/test_skills.py | 8 +-- python/e2e/test_streaming_fidelity.py | 10 ++-- python/e2e/test_tools.py | 20 +++---- python/samples/chat.py | 2 +- .../auth/byok-anthropic/python/main.py | 2 +- test/scenarios/auth/byok-azure/python/main.py | 2 +- .../scenarios/auth/byok-ollama/python/main.py | 2 +- .../scenarios/auth/byok-openai/python/main.py | 2 +- test/scenarios/auth/gh-app/python/main.py | 2 +- .../app-backend-to-server/python/main.py | 2 +- .../bundling/app-direct-server/python/main.py | 2 +- .../bundling/container-proxy/python/main.py | 2 +- .../bundling/fully-bundled/python/main.py | 2 +- test/scenarios/callbacks/hooks/python/main.py | 4 +- .../callbacks/permissions/python/main.py | 4 +- .../callbacks/user-input/python/main.py | 8 +-- test/scenarios/modes/default/python/main.py | 2 +- test/scenarios/modes/minimal/python/main.py | 2 +- .../prompts/attachments/python/main.py | 6 +- .../prompts/reasoning-effort/python/main.py | 2 +- .../prompts/system-message/python/main.py | 2 +- .../concurrent-sessions/python/main.py | 4 +- .../sessions/infinite-sessions/python/main.py | 2 +- .../sessions/session-resume/python/main.py | 4 +- .../sessions/streaming/python/main.py | 2 +- .../tools/custom-agents/python/main.py | 2 +- .../tools/mcp-servers/python/main.py | 2 +- test/scenarios/tools/no-tools/python/main.py | 2 +- test/scenarios/tools/skills/python/main.py | 2 +- .../tools/tool-filtering/python/main.py | 2 +- .../tools/tool-overrides/python/main.py | 2 +- .../tools/virtual-filesystem/python/main.py | 8 +-- .../transport/reconnect/python/main.py | 4 +- test/scenarios/transport/stdio/python/main.py | 2 +- test/scenarios/transport/tcp/python/main.py | 2 +- 48 files changed, 166 insertions(+), 246 deletions(-) diff --git a/python/README.md b/python/README.md index a585ea11..3d36105c 100644 --- a/python/README.md +++ b/python/README.md @@ -47,7 +47,7 @@ async def main(): session.on(on_event) # Send a message and wait for completion - await session.send({"prompt": "What is 2+2?"}) + await session.send("What is 2+2?") await done.wait() # Clean up @@ -61,7 +61,7 @@ Sessions also support the `async with` context manager pattern for automatic cle ```python async with await client.create_session({"model": "gpt-5"}) as session: - await session.send({"prompt": "What is 2+2?"}) + await session.send("What is 2+2?") # session is automatically disconnected when leaving the block ``` @@ -93,7 +93,7 @@ def on_event(event): print(f"Event: {event['type']}") session.on(on_event) -await session.send({"prompt": "Hello!"}) +await session.send("Hello!") # ... wait for events ... @@ -235,21 +235,21 @@ async def edit_file(params: EditFileParams) -> str: The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path: ```python -await session.send({ - "prompt": "What's in this image?", - "attachments": [ +await session.send( + "What's in this image?", + attachments=[ { "type": "file", "path": "/path/to/image.jpg", } - ] -}) + ], +) ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: ```python -await session.send({"prompt": "What does the most recent jpg in this directory portray?"}) +await session.send("What does the most recent jpg in this directory portray?") ``` ## Streaming @@ -294,7 +294,7 @@ async def main(): done.set() session.on(on_event) - await session.send({"prompt": "Tell me a short story"}) + await session.send("Tell me a short story") await done.wait() # Wait for streaming to complete await session.disconnect() @@ -371,7 +371,7 @@ session = await client.create_session({ }, }) -await session.send({"prompt": "Hello!"}) +await session.send("Hello!") ``` **Example with custom OpenAI-compatible API:** diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index f5f7ed0b..9ec50216 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -16,7 +16,6 @@ MCPLocalServerConfig, MCPRemoteServerConfig, MCPServerConfig, - MessageOptions, ModelBilling, ModelCapabilities, ModelInfo, @@ -52,7 +51,6 @@ "MCPLocalServerConfig", "MCPRemoteServerConfig", "MCPServerConfig", - "MessageOptions", "ModelBilling", "ModelCapabilities", "ModelInfo", diff --git a/python/copilot/client.py b/python/copilot/client.py index 239c4f79..ae378d1c 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -9,7 +9,7 @@ >>> >>> async with CopilotClient() as client: ... session = await client.create_session() - ... await session.send({"prompt": "Hello!"}) + ... await session.send("Hello!") """ import asyncio @@ -104,7 +104,7 @@ class CopilotClient: ... "model": "gpt-4", ... }) >>> session.on(lambda event: print(event.type)) - >>> await session.send({"prompt": "Hello!"}) + >>> await session.send("Hello!") >>> >>> # Clean up >>> await session.disconnect() diff --git a/python/copilot/session.py b/python/copilot/session.py index b4ae210d..2d864d13 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -25,7 +25,6 @@ from .generated.session_events import SessionEvent, SessionEventType, session_event_from_dict from .jsonrpc import JsonRpcError, ProcessExitedError from .types import ( - MessageOptions, PermissionRequest, PermissionRequestResult, SessionHooks, @@ -63,7 +62,7 @@ class CopilotSession: ... unsubscribe = session.on(lambda event: print(event.type)) ... ... # Send a message - ... await session.send({"prompt": "Hello, world!"}) + ... await session.send("Hello, world!") ... ... # Clean up ... unsubscribe() @@ -115,7 +114,13 @@ def workspace_path(self) -> str | None: """ return self._workspace_path - async def send(self, options: MessageOptions) -> str: + async def send( + self, + prompt: str, + *, + attachments: list[Any] | None = None, + mode: str | None = None, + ) -> str: """ Send a message to this session and wait for the response. @@ -123,9 +128,9 @@ async def send(self, options: MessageOptions) -> str: to receive streaming responses and other session events. Args: - options: Message options including the prompt and optional attachments. - Must contain a "prompt" key with the message text. Can optionally - include "attachments" and "mode" keys. + prompt: The message text to send. + attachments: Optional file, directory, or selection attachments. + mode: Message delivery mode (``"enqueue"`` or ``"immediate"``). Returns: The message ID of the response, which can be used to correlate events. @@ -134,25 +139,30 @@ async def send(self, options: MessageOptions) -> str: Exception: If the session has been disconnected or the connection fails. Example: - >>> message_id = await session.send({ - ... "prompt": "Explain this code", - ... "attachments": [{"type": "file", "path": "./src/main.py"}] - ... }) + >>> message_id = await session.send( + ... "Explain this code", + ... attachments=[{"type": "file", "path": "./src/main.py"}], + ... ) """ params: dict[str, Any] = { "sessionId": self.session_id, - "prompt": options["prompt"], + "prompt": prompt, } - if "attachments" in options: - params["attachments"] = options["attachments"] - if "mode" in options: - params["mode"] = options["mode"] + if attachments is not None: + params["attachments"] = attachments + if mode is not None: + params["mode"] = mode response = await self._client.request("session.send", params) return response["messageId"] async def send_and_wait( - self, options: MessageOptions, timeout: float | None = None + self, + prompt: str, + *, + attachments: list[Any] | None = None, + mode: str | None = None, + timeout: float = 60.0, ) -> SessionEvent | None: """ Send a message to this session and wait until the session becomes idle. @@ -164,7 +174,9 @@ async def send_and_wait( Events are still delivered to handlers registered via :meth:`on` while waiting. Args: - options: Message options including the prompt and optional attachments. + prompt: The message text to send. + attachments: Optional file, directory, or selection attachments. + mode: Message delivery mode (``"enqueue"`` or ``"immediate"``). timeout: Timeout in seconds (default: 60). Controls how long to wait; does not abort in-flight agent work. @@ -176,12 +188,10 @@ async def send_and_wait( Exception: If the session has been disconnected or the connection fails. Example: - >>> response = await session.send_and_wait({"prompt": "What is 2+2?"}) + >>> response = await session.send_and_wait("What is 2+2?") >>> if response: ... print(response.data.content) """ - effective_timeout = timeout if timeout is not None else 60.0 - idle_event = asyncio.Event() error_event: Exception | None = None last_assistant_message: SessionEvent | None = None @@ -200,13 +210,13 @@ def handler(event: SessionEventTypeAlias) -> None: unsubscribe = self.on(handler) try: - await self.send(options) - await asyncio.wait_for(idle_event.wait(), timeout=effective_timeout) + await self.send(prompt, attachments=attachments, mode=mode) + await asyncio.wait_for(idle_event.wait(), timeout=timeout) if error_event: raise error_event return last_assistant_message except TimeoutError: - raise TimeoutError(f"Timeout after {effective_timeout}s waiting for session.idle") + raise TimeoutError(f"Timeout after {timeout}s waiting for session.idle") finally: unsubscribe() @@ -712,7 +722,7 @@ async def abort(self) -> None: >>> >>> # Start a long-running request >>> task = asyncio.create_task( - ... session.send({"prompt": "Write a very long story..."}) + ... session.send("Write a very long story...") ... ) >>> >>> # Abort after 5 seconds diff --git a/python/copilot/types.py b/python/copilot/types.py index e8b8d1d4..4d8e4984 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -603,17 +603,6 @@ class ResumeSessionConfig(TypedDict, total=False): on_event: Callable[[SessionEvent], None] -# Options for sending a message to a session -class MessageOptions(TypedDict): - """Options for sending a message to a session""" - - prompt: str # The prompt/message to send - # Optional file/directory attachments - attachments: NotRequired[list[Attachment]] - # Message processing mode - mode: NotRequired[Literal["enqueue", "immediate"]] - - # Event handler type SessionEventHandler = Callable[[SessionEvent], None] diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index cee6814f..c17161e6 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -182,7 +182,7 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC ) # Send a message to create some history - await session.send_and_wait({"prompt": "What is 2+2?"}) + await session.send_and_wait("What is 2+2?") # Compact the session result = await session.rpc.compaction.compact() diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py index bddc062d..b9800156 100644 --- a/python/e2e/test_ask_user.py +++ b/python/e2e/test_ask_user.py @@ -37,12 +37,8 @@ async def on_user_input_request(request, invocation): ) await session.send_and_wait( - { - "prompt": ( - "Ask me to choose between 'Option A' and 'Option B' using the ask_user " - "tool. Wait for my response before continuing." - ) - } + "Ask me to choose between 'Option A' and 'Option B' using the ask_user " + "tool. Wait for my response before continuing." ) # Should have received at least one user input request @@ -76,12 +72,8 @@ async def on_user_input_request(request, invocation): ) await session.send_and_wait( - { - "prompt": ( - "Use the ask_user tool to ask me to pick between exactly two options: " - "'Red' and 'Blue'. These should be provided as choices. Wait for my answer." - ) - } + "Use the ask_user tool to ask me to pick between exactly two options: " + "'Red' and 'Blue'. These should be provided as choices. Wait for my answer." ) # Should have received a request @@ -117,12 +109,8 @@ async def on_user_input_request(request, invocation): ) response = await session.send_and_wait( - { - "prompt": ( - "Ask me a question using ask_user and then include my answer in your " - "response. The question should be 'What is your favorite color?'" - ) - } + "Ask me a question using ask_user and then include my answer in your " + "response. The question should be 'What is your favorite color?'" ) # Should have received a request diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index 5447b4ba..13104070 100644 --- a/python/e2e/test_compaction.py +++ b/python/e2e/test_compaction.py @@ -41,13 +41,11 @@ def on_event(event): session.on(on_event) # Send multiple messages to fill up the context window - await session.send_and_wait({"prompt": "Tell me a story about a dragon. Be detailed."}) + await session.send_and_wait("Tell me a story about a dragon. Be detailed.") await session.send_and_wait( - {"prompt": "Continue the story with more details about the dragon's castle."} - ) - await session.send_and_wait( - {"prompt": "Now describe the dragon's treasure in great detail."} + "Continue the story with more details about the dragon's castle." ) + await session.send_and_wait("Now describe the dragon's treasure in great detail.") # Should have triggered compaction at least once assert len(compaction_start_events) >= 1, "Expected at least 1 compaction_start event" @@ -62,7 +60,7 @@ def on_event(event): assert last_complete.data.tokens_removed > 0, "Expected tokensRemoved > 0" # Verify the session still works after compaction - answer = await session.send_and_wait({"prompt": "What was the story about?"}) + answer = await session.send_and_wait("What was the story about?") assert answer is not None assert answer.data.content is not None # Should remember it was about a dragon (context preserved via summary) @@ -89,7 +87,7 @@ def on_event(event): session.on(on_event) - await session.send_and_wait({"prompt": "What is 2+2?"}) + await session.send_and_wait("What is 2+2?") # Should not have any compaction events when disabled assert len(compaction_events) == 0, "Expected no compaction events when disabled" diff --git a/python/e2e/test_hooks.py b/python/e2e/test_hooks.py index c886c6e2..a4956482 100644 --- a/python/e2e/test_hooks.py +++ b/python/e2e/test_hooks.py @@ -33,9 +33,7 @@ async def on_pre_tool_use(input_data, invocation): # Create a file for the model to read write_file(ctx.work_dir, "hello.txt", "Hello from the test!") - await session.send_and_wait( - {"prompt": "Read the contents of hello.txt and tell me what it says"} - ) + await session.send_and_wait("Read the contents of hello.txt and tell me what it says") # Should have received at least one preToolUse hook call assert len(pre_tool_use_inputs) > 0 @@ -66,9 +64,7 @@ async def on_post_tool_use(input_data, invocation): # Create a file for the model to read write_file(ctx.work_dir, "world.txt", "World from the test!") - await session.send_and_wait( - {"prompt": "Read the contents of world.txt and tell me what it says"} - ) + await session.send_and_wait("Read the contents of world.txt and tell me what it says") # Should have received at least one postToolUse hook call assert len(post_tool_use_inputs) > 0 @@ -106,7 +102,7 @@ async def on_post_tool_use(input_data, invocation): write_file(ctx.work_dir, "both.txt", "Testing both hooks!") - await session.send_and_wait({"prompt": "Read the contents of both.txt"}) + await session.send_and_wait("Read the contents of both.txt") # Both hooks should have been called assert len(pre_tool_use_inputs) > 0 @@ -143,7 +139,7 @@ async def on_pre_tool_use(input_data, invocation): write_file(ctx.work_dir, "protected.txt", original_content) response = await session.send_and_wait( - {"prompt": "Edit protected.txt and replace 'Original' with 'Modified'"} + "Edit protected.txt and replace 'Original' with 'Modified'" ) # The hook should have been called diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index fd99cc2c..8fffbe88 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -39,7 +39,7 @@ async def test_should_accept_mcp_server_configuration_on_session_create( assert session.session_id is not None # Simple interaction to verify session works - message = await session.send_and_wait({"prompt": "What is 2+2?"}) + message = await session.send_and_wait("What is 2+2?") assert message is not None assert "4" in message.data.content @@ -54,7 +54,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - await session1.send_and_wait({"prompt": "What is 1+1?"}) + await session1.send_and_wait("What is 1+1?") # Resume with MCP servers mcp_servers: dict[str, MCPServerConfig] = { @@ -73,7 +73,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( assert session2.session_id == session_id - message = await session2.send_and_wait({"prompt": "What is 3+3?"}) + message = await session2.send_and_wait("What is 3+3?") assert message is not None assert "6" in message.data.content @@ -104,10 +104,8 @@ async def test_should_pass_literal_env_values_to_mcp_server_subprocess( assert session.session_id is not None message = await session.send_and_wait( - { - "prompt": "Use the env-echo/get_env tool to read the TEST_SECRET " - "environment variable. Reply with just the value, nothing else." - } + "Use the env-echo/get_env tool to read the TEST_SECRET " + "environment variable. Reply with just the value, nothing else." ) assert message is not None assert "hunter2" in message.data.content @@ -137,7 +135,7 @@ async def test_should_accept_custom_agent_configuration_on_session_create( assert session.session_id is not None # Simple interaction to verify session works - message = await session.send_and_wait({"prompt": "What is 5+5?"}) + message = await session.send_and_wait("What is 5+5?") assert message is not None assert "10" in message.data.content @@ -152,7 +150,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - await session1.send_and_wait({"prompt": "What is 1+1?"}) + await session1.send_and_wait("What is 1+1?") # Resume with custom agents custom_agents: list[CustomAgentConfig] = [ @@ -174,7 +172,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( assert session2.session_id == session_id - message = await session2.send_and_wait({"prompt": "What is 6+6?"}) + message = await session2.send_and_wait("What is 6+6?") assert message is not None assert "12" in message.data.content @@ -212,7 +210,7 @@ async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETe assert session.session_id is not None - await session.send({"prompt": "What is 7+7?"}) + await session.send("What is 7+7?") message = await get_final_assistant_message(session) assert "14" in message.data.content diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index caf58cd5..183beb1a 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -212,9 +212,7 @@ def magic_number(params: SeedParams, invocation: ToolInvocation) -> str: session2.on(lambda event: client2_events.append(event)) # Send a prompt that triggers the custom tool - await session1.send( - {"prompt": "Use the magic_number tool with seed 'hello' and tell me the result"} - ) + await session1.send("Use the magic_number tool with seed 'hello' and tell me the result") response = await get_final_assistant_message(session1) assert "MAGIC_hello_42" in (response.data.content or "") @@ -259,9 +257,7 @@ async def test_one_client_approves_permission_and_both_see_the_result( session2.on(lambda event: client2_events.append(event)) # Send a prompt that triggers a write operation (requires permission) - await session1.send( - {"prompt": "Create a file called hello.txt containing the text 'hello world'"} - ) + await session1.send("Create a file called hello.txt containing the text 'hello world'") response = await get_final_assistant_message(session1) assert response.data.content @@ -313,7 +309,7 @@ async def test_one_client_rejects_permission_and_both_see_the_result( with open(test_file, "w") as f: f.write("protected content") - await session1.send({"prompt": "Edit protected.txt and replace 'protected' with 'hacked'."}) + await session1.send("Edit protected.txt and replace 'protected' with 'hacked'.") await get_final_assistant_message(session1) # Verify the file was NOT modified (permission was denied) @@ -368,17 +364,13 @@ def currency_lookup(params: CountryCodeParams, invocation: ToolInvocation) -> st # Send prompts sequentially to avoid nondeterministic tool_call ordering await session1.send( - {"prompt": "Use the city_lookup tool with countryCode 'US' and tell me the result."} + "Use the city_lookup tool with countryCode 'US' and tell me the result." ) response1 = await get_final_assistant_message(session1) assert "CITY_FOR_US" in (response1.data.content or "") await session1.send( - { - "prompt": ( - "Now use the currency_lookup tool with countryCode 'US' and tell me the result." - ) - } + "Now use the currency_lookup tool with countryCode 'US' and tell me the result." ) response2 = await get_final_assistant_message(session1) assert "CURRENCY_FOR_US" in (response2.data.content or "") @@ -419,19 +411,11 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: # Verify both tools work before disconnect. # Sequential prompts avoid nondeterministic tool_call ordering. - await session1.send( - { - "prompt": "Use the stable_tool with input 'test1' and tell me the result.", - } - ) + await session1.send("Use the stable_tool with input 'test1' and tell me the result.") stable_response = await get_final_assistant_message(session1) assert "STABLE_test1" in (stable_response.data.content or "") - await session1.send( - { - "prompt": "Use the ephemeral_tool with input 'test2' and tell me the result.", - } - ) + await session1.send("Use the ephemeral_tool with input 'test2' and tell me the result.") ephemeral_response = await get_final_assistant_message(session1) assert "EPHEMERAL_test2" in (ephemeral_response.data.content or "") @@ -447,13 +431,9 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: # Now only stable_tool should be available await session1.send( - { - "prompt": ( - "Use the stable_tool with input 'still_here'." - " Also try using ephemeral_tool" - " if it is available." - ) - } + "Use the stable_tool with input 'still_here'." + " Also try using ephemeral_tool" + " if it is available." ) after_response = await get_final_assistant_message(session1) assert "STABLE_still_here" in (after_response.data.content or "") diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index 609003e8..d18b15b2 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -30,9 +30,7 @@ def on_permission_request( write_file(ctx.work_dir, "test.txt", "original content") - await session.send_and_wait( - {"prompt": "Edit test.txt and replace 'original' with 'modified'"} - ) + await session.send_and_wait("Edit test.txt and replace 'original' with 'modified'") # Should have received at least one permission request assert len(permission_requests) > 0 @@ -56,9 +54,7 @@ def on_permission_request( original_content = "protected content" write_file(ctx.work_dir, "protected.txt", original_content) - await session.send_and_wait( - {"prompt": "Edit protected.txt and replace 'protected' with 'hacked'."} - ) + await session.send_and_wait("Edit protected.txt and replace 'protected' with 'hacked'.") # Verify the file was NOT modified content = read_file(ctx.work_dir, "protected.txt") @@ -94,7 +90,7 @@ def on_event(event): session.on(on_event) - await session.send({"prompt": "Run 'node --version'"}) + await session.send("Run 'node --version'") await asyncio.wait_for(done_event.wait(), timeout=60) assert len(denied_events) > 0 @@ -109,7 +105,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - await session1.send_and_wait({"prompt": "What is 1+1?"}) + await session1.send_and_wait("What is 1+1?") def deny_all(request, invocation): return PermissionRequestResult() @@ -134,7 +130,7 @@ def on_event(event): session2.on(on_event) - await session2.send({"prompt": "Run 'node --version'"}) + await session2.send("Run 'node --version'") await asyncio.wait_for(done_event.wait(), timeout=60) assert len(denied_events) > 0 @@ -147,7 +143,7 @@ async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETes {"on_permission_request": PermissionHandler.approve_all} ) - message = await session.send_and_wait({"prompt": "What is 2+2?"}) + message = await session.send_and_wait("What is 2+2?") assert message is not None assert "4" in message.data.content @@ -168,7 +164,7 @@ async def on_permission_request( session = await ctx.client.create_session({"on_permission_request": on_permission_request}) - await session.send_and_wait({"prompt": "Run 'echo test' and tell me what happens"}) + await session.send_and_wait("Run 'echo test' and tell me what happens") assert len(permission_requests) > 0 @@ -183,7 +179,7 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - await session1.send_and_wait({"prompt": "What is 1+1?"}) + await session1.send_and_wait("What is 1+1?") # Resume with permission handler def on_permission_request( @@ -196,7 +192,7 @@ def on_permission_request( session_id, {"on_permission_request": on_permission_request} ) - await session2.send_and_wait({"prompt": "Run 'echo resumed' for me"}) + await session2.send_and_wait("Run 'echo resumed' for me") # Should have permission requests from resumed session assert len(permission_requests) > 0 @@ -213,9 +209,7 @@ def on_permission_request( session = await ctx.client.create_session({"on_permission_request": on_permission_request}) - message = await session.send_and_wait( - {"prompt": "Run 'echo test'. If you can't, say 'failed'."} - ) + message = await session.send_and_wait("Run 'echo test'. If you can't, say 'failed'.") # Should handle the error and deny permission assert message is not None @@ -240,7 +234,7 @@ def on_permission_request( session = await ctx.client.create_session({"on_permission_request": on_permission_request}) - await session.send_and_wait({"prompt": "Run 'echo test'"}) + await session.send_and_wait("Run 'echo test'") assert received_tool_call_id diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 79fb661d..a4024646 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -35,13 +35,11 @@ async def test_should_have_stateful_conversation(self, ctx: E2ETestContext): {"on_permission_request": PermissionHandler.approve_all} ) - assistant_message = await session.send_and_wait({"prompt": "What is 1+1?"}) + assistant_message = await session.send_and_wait("What is 1+1?") assert assistant_message is not None assert "2" in assistant_message.data.content - second_message = await session.send_and_wait( - {"prompt": "Now if you double that, what do you get?"} - ) + second_message = await session.send_and_wait("Now if you double that, what do you get?") assert second_message is not None assert "4" in second_message.data.content @@ -56,7 +54,7 @@ async def test_should_create_a_session_with_appended_systemMessage_config( } ) - await session.send({"prompt": "What is your full name?"}) + await session.send("What is your full name?") assistant_message = await get_final_assistant_message(session) assert "GitHub" in assistant_message.data.content assert "Have a nice day!" in assistant_message.data.content @@ -78,7 +76,7 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( } ) - await session.send({"prompt": "What is your full name?"}) + await session.send("What is your full name?") assistant_message = await get_final_assistant_message(session) assert "GitHub" not in assistant_message.data.content assert "Testy" in assistant_message.data.content @@ -96,7 +94,7 @@ async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestCon } ) - await session.send({"prompt": "What is 1+1?"}) + await session.send("What is 1+1?") await get_final_assistant_message(session) # It only tells the model about the specified tools and no others @@ -112,7 +110,7 @@ async def test_should_create_a_session_with_excludedTools(self, ctx: E2ETestCont {"excluded_tools": ["view"], "on_permission_request": PermissionHandler.approve_all} ) - await session.send({"prompt": "What is 1+1?"}) + await session.send("What is 1+1?") await get_final_assistant_message(session) # It has other tools, but not the one we excluded @@ -160,7 +158,7 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - answer = await session1.send_and_wait({"prompt": "What is 1+1?"}) + answer = await session1.send_and_wait("What is 1+1?") assert answer is not None assert "2" in answer.data.content @@ -173,9 +171,7 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC assert "2" in answer2.data.content # Can continue the conversation statefully - answer3 = await session2.send_and_wait( - {"prompt": "Now if you double that, what do you get?"} - ) + answer3 = await session2.send_and_wait("Now if you double that, what do you get?") assert answer3 is not None assert "4" in answer3.data.content @@ -185,7 +181,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - answer = await session1.send_and_wait({"prompt": "What is 1+1?"}) + answer = await session1.send_and_wait("What is 1+1?") assert answer is not None assert "2" in answer.data.content @@ -214,9 +210,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont assert "session.resume" in message_types # Can continue the conversation statefully - answer2 = await session2.send_and_wait( - {"prompt": "Now if you double that, what do you get?"} - ) + answer2 = await session2.send_and_wait("Now if you double that, what do you get?") assert answer2 is not None assert "4" in answer2.data.content finally: @@ -235,11 +229,11 @@ async def test_should_list_sessions(self, ctx: E2ETestContext): session1 = await ctx.client.create_session( {"on_permission_request": PermissionHandler.approve_all} ) - await session1.send_and_wait({"prompt": "Say hello"}) + await session1.send_and_wait("Say hello") session2 = await ctx.client.create_session( {"on_permission_request": PermissionHandler.approve_all} ) - await session2.send_and_wait({"prompt": "Say goodbye"}) + await session2.send_and_wait("Say goodbye") # Small delay to ensure session files are written to disk await asyncio.sleep(0.2) @@ -278,7 +272,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): session = await ctx.client.create_session( {"on_permission_request": PermissionHandler.approve_all} ) - await session.send_and_wait({"prompt": "Hello"}) + await session.send_and_wait("Hello") session_id = session.session_id # Small delay to ensure session file is written to disk @@ -310,7 +304,7 @@ async def test_should_get_last_session_id(self, ctx: E2ETestContext): session = await ctx.client.create_session( {"on_permission_request": PermissionHandler.approve_all} ) - await session.send_and_wait({"prompt": "Say hello"}) + await session.send_and_wait("Say hello") # Small delay to ensure session data is flushed to disk await asyncio.sleep(0.5) @@ -347,7 +341,7 @@ def get_secret_number_handler(invocation): } ) - answer = await session.send_and_wait({"prompt": "What is the secret number for key ALPHA?"}) + answer = await session.send_and_wait("What is the secret number for key ALPHA?") assert answer is not None assert "54321" in answer.data.content @@ -418,12 +412,7 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext): # Send a message that will trigger a long-running shell command await session.send( - { - "prompt": ( - "run the shell command 'sleep 100' " - "(note this works on both bash and PowerShell)" - ) - } + "run the shell command 'sleep 100' (note this works on both bash and PowerShell)" ) # Wait for the tool to start executing @@ -444,7 +433,7 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext): assert len(abort_events) > 0, "Expected an abort event in messages" # We should be able to send another message - answer = await session.send_and_wait({"prompt": "What is 2+2?"}) + answer = await session.send_and_wait("What is 2+2?") assert "4" in answer.data.content async def test_should_receive_session_events(self, ctx: E2ETestContext): @@ -478,7 +467,7 @@ def on_event(event): session.on(on_event) # Send a message to trigger events - await session.send({"prompt": "What is 100+200?"}) + await session.send("What is 100+200?") # Wait for session to become idle try: @@ -511,7 +500,7 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo assert session.session_id # Session should work normally with custom config dir - await session.send({"prompt": "What is 1+1?"}) + await session.send("What is 1+1?") assistant_message = await get_final_assistant_message(session) assert "2" in assistant_message.data.content diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index 166840e5..066669f2 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -65,7 +65,7 @@ async def test_should_load_and_apply_skill_from_skilldirectories(self, ctx: E2ET assert session.session_id is not None # The skill instructs the model to include a marker - verify it appears - message = await session.send_and_wait({"prompt": "Say hello briefly using the test skill."}) + message = await session.send_and_wait("Say hello briefly using the test skill.") assert message is not None assert SKILL_MARKER in message.data.content @@ -87,7 +87,7 @@ async def test_should_not_apply_skill_when_disabled_via_disabledskills( assert session.session_id is not None # The skill is disabled, so the marker should NOT appear - message = await session.send_and_wait({"prompt": "Say hello briefly using the test skill."}) + message = await session.send_and_wait("Say hello briefly using the test skill.") assert message is not None assert SKILL_MARKER not in message.data.content @@ -110,7 +110,7 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( session_id = session1.session_id # First message without skill - marker should not appear - message1 = await session1.send_and_wait({"prompt": "Say hi."}) + message1 = await session1.send_and_wait("Say hi.") assert message1 is not None assert SKILL_MARKER not in message1.data.content @@ -126,7 +126,7 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( assert session2.session_id == session_id # Now the skill should be applied - message2 = await session2.send_and_wait({"prompt": "Say hello again using the test skill."}) + message2 = await session2.send_and_wait("Say hello again using the test skill.") assert message2 is not None assert SKILL_MARKER in message2.data.content diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index d347015a..d50caa98 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -20,7 +20,7 @@ async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: events = [] session.on(lambda event: events.append(event)) - await session.send_and_wait({"prompt": "Count from 1 to 5, separated by commas."}) + await session.send_and_wait("Count from 1 to 5, separated by commas.") types = [e.type.value for e in events] @@ -52,7 +52,7 @@ async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E events = [] session.on(lambda event: events.append(event)) - await session.send_and_wait({"prompt": "Say 'hello world'."}) + await session.send_and_wait("Say 'hello world'.") delta_events = [e for e in events if e.type.value == "assistant.message_delta"] @@ -69,7 +69,7 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont session = await ctx.client.create_session( {"streaming": False, "on_permission_request": PermissionHandler.approve_all} ) - await session.send_and_wait({"prompt": "What is 3 + 6?"}) + await session.send_and_wait("What is 3 + 6?") await session.disconnect() # Resume using a new client @@ -93,9 +93,7 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont events = [] session2.on(lambda event: events.append(event)) - answer = await session2.send_and_wait( - {"prompt": "Now if you double that, what do you get?"} - ) + answer = await session2.send_and_wait("Now if you double that, what do you get?") assert answer is not None assert "18" in answer.data.content diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index b692e3f6..0831ecc1 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -27,7 +27,7 @@ async def test_invokes_built_in_tools(self, ctx: E2ETestContext): {"on_permission_request": PermissionHandler.approve_all} ) - await session.send({"prompt": "What's the first line of README.md in this directory?"}) + await session.send("What's the first line of README.md in this directory?") assistant_message = await get_final_assistant_message(session) assert "ELIZA" in assistant_message.data.content @@ -43,7 +43,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: {"tools": [encrypt_string], "on_permission_request": PermissionHandler.approve_all} ) - await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) + await session.send("Use encrypt_string to encrypt this string: Hello") assistant_message = await get_final_assistant_message(session) assert "HELLO" in assistant_message.data.content @@ -56,9 +56,7 @@ def get_user_location() -> str: {"tools": [get_user_location], "on_permission_request": PermissionHandler.approve_all} ) - await session.send( - {"prompt": "What is my location? If you can't find out, just say 'unknown'."} - ) + await session.send("What is my location? If you can't find out, just say 'unknown'.") answer = await get_final_assistant_message(session) # Check the underlying traffic @@ -123,10 +121,8 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]: expected_session_id = session.session_id await session.send( - { - "prompt": "Perform a DB query for the 'cities' table using IDs 12 and 19, " - "sorting ascending. Reply only with lines of the form: [cityname] [population]" - } + "Perform a DB query for the 'cities' table using IDs 12 and 19, " + "sorting ascending. Reply only with lines of the form: [cityname] [population]" ) assistant_message = await get_final_assistant_message(session) @@ -154,7 +150,7 @@ def custom_grep(params: GrepParams, invocation: ToolInvocation) -> str: {"tools": [custom_grep], "on_permission_request": PermissionHandler.approve_all} ) - await session.send({"prompt": "Use grep to search for the word 'hello'"}) + await session.send("Use grep to search for the word 'hello'") assistant_message = await get_final_assistant_message(session) assert "CUSTOM_GREP_RESULT" in assistant_message.data.content @@ -179,7 +175,7 @@ def on_permission_request(request, invocation): } ) - await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) + await session.send("Use encrypt_string to encrypt this string: Hello") assistant_message = await get_final_assistant_message(session) assert "HELLO" in assistant_message.data.content @@ -210,7 +206,7 @@ def on_permission_request(request, invocation): } ) - await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) + await session.send("Use encrypt_string to encrypt this string: Hello") await get_final_assistant_message(session) # The tool handler should NOT have been called since permission was denied diff --git a/python/samples/chat.py b/python/samples/chat.py index eb781e4e..908a125d 100644 --- a/python/samples/chat.py +++ b/python/samples/chat.py @@ -34,7 +34,7 @@ def on_event(event): continue print() - reply = await session.send_and_wait({"prompt": user_input}) + reply = await session.send_and_wait(user_input) print(f"\nAssistant: {reply.data.content if reply else None}\n") diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index e50a33c1..8dbcd6bc 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -34,7 +34,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 89f37178..9f37a719 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -38,7 +38,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index b86c76ba..fc59f173 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -32,7 +32,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index b501bb10..101c8db5 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -29,7 +29,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index 4886fe07..8e852558 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -85,7 +85,7 @@ async def main(): try: session = await client.create_session({"model": "claude-haiku-4.5"}) - response = await session.send_and_wait({"prompt": "What is the capital of France?"}) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) await session.disconnect() diff --git a/test/scenarios/bundling/app-backend-to-server/python/main.py b/test/scenarios/bundling/app-backend-to-server/python/main.py index 29563149..ca8473b2 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -18,7 +18,7 @@ async def ask_copilot(prompt: str) -> str: try: session = await client.create_session({"model": "claude-haiku-4.5"}) - response = await session.send_and_wait({"prompt": prompt}) + response = await session.send_and_wait(prompt) await session.disconnect() diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index c407d4fe..a180f213 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -12,7 +12,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index c407d4fe..a180f213 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -12,7 +12,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index d1441361..1a488c05 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -13,7 +13,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index 8df61b9d..9c2b141a 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -62,9 +62,7 @@ async def main(): ) response = await session.send_and_wait( - { - "prompt": "List the files in the current directory using the glob tool with pattern '*.md'.", - } + "List the files in the current directory using the glob tool with pattern '*.md'." ) if response: diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 9674da91..e5bd5cd7 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -31,9 +31,7 @@ async def main(): ) response = await session.send_and_wait( - { - "prompt": "List the files in the current directory using glob with pattern '*.md'." - } + "List the files in the current directory using glob with pattern '*.md'." ) if response: diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index dc8d9fa9..be282d67 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -36,12 +36,8 @@ async def main(): ) response = await session.send_and_wait( - { - "prompt": ( - "I want to learn about a city. Use the ask_user tool to ask me " - "which city I'm interested in. Then tell me about that city." - ) - } + "I want to learn about a city. Use the ask_user tool to ask me " + "which city I'm interested in. Then tell me about that city." ) if response: diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index dadc0e7b..0b06e321 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -14,7 +14,7 @@ async def main(): "model": "claude-haiku-4.5", }) - response = await session.send_and_wait({"prompt": "Use the grep tool to search for the word 'SDK' in README.md and show the matching lines."}) + response = await session.send_and_wait("Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.") if response: print(f"Response: {response.data.content}") diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 0b243caf..dd249829 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -19,7 +19,7 @@ async def main(): }, }) - response = await session.send_and_wait({"prompt": "Use the grep tool to search for 'SDK' in README.md."}) + response = await session.send_and_wait("Use the grep tool to search for 'SDK' in README.md.") if response: print(f"Response: {response.data.content}") diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index c7e21e8b..077bdfd2 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -24,10 +24,8 @@ async def main(): sample_file = os.path.abspath(sample_file) response = await session.send_and_wait( - { - "prompt": "What languages are listed in the attached file?", - "attachments": [{"type": "file", "path": sample_file}], - } + "What languages are listed in the attached file?", + attachments=[{"type": "file", "path": sample_file}], ) if response: diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index b38452a8..fdbea369 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -21,7 +21,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index 5e396c8c..65df015d 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -21,7 +21,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index ebca8990..bda15834 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -32,10 +32,10 @@ async def main(): response1, response2 = await asyncio.gather( session1.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ), session2.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ), ) diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 23749d06..c944e8a8 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -31,7 +31,7 @@ async def main(): ] for prompt in prompts: - response = await session.send_and_wait({"prompt": prompt}) + response = await session.send_and_wait(prompt) if response: print(f"Q: {prompt}") print(f"A: {response.data.content}\n") diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index 7eb5e0ca..768f7860 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -20,7 +20,7 @@ async def main(): # 2. Send the secret word await session.send_and_wait( - {"prompt": "Remember this: the secret word is PINEAPPLE."} + "Remember this: the secret word is PINEAPPLE." ) # 3. Get the session ID (don't disconnect — resume needs the session to persist) @@ -32,7 +32,7 @@ async def main(): # 5. Ask for the secret word response = await resumed.send_and_wait( - {"prompt": "What was the secret word I told you?"} + "What was the secret word I told you?" ) if response: diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index 94569de1..3a05143f 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -27,7 +27,7 @@ def on_event(event): session.on(on_event) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index 0b5f073d..1b1b042f 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -26,7 +26,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "What custom agents are available? Describe the researcher agent and its capabilities."} + "What custom agents are available? Describe the researcher agent and its capabilities." ) if response: diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index f092fb9a..0ba370f3 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -36,7 +36,7 @@ async def main(): session = await client.create_session(session_config) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index a3824bab..f2d41004 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -24,7 +24,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "Use the bash tool to run 'echo hello'."} + "Use the bash tool to run 'echo hello'." ) if response: diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 3e06650b..c2e9f4c3 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -26,7 +26,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "Use the greeting skill to greet someone named Alice."} + "Use the greeting skill to greet someone named Alice." ) if response: diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 1fdfacc7..475892f6 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -21,7 +21,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "What tools do you have available? List each one by name."} + "What tools do you have available? List each one by name." ) if response: diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 1f1099f0..bac950ca 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -31,7 +31,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "Use grep to search for the word 'hello'"} + "Use grep to search for the word 'hello'" ) if response: diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index 9a51e7ef..12af5746 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -63,12 +63,8 @@ async def main(): ) response = await session.send_and_wait( - { - "prompt": ( - "Create a file called plan.md with a brief 3-item project plan " - "for building a CLI tool. Then read it back and tell me what you wrote." - ) - } + "Create a file called plan.md with a brief 3-item project plan " + "for building a CLI tool. Then read it back and tell me what you wrote." ) if response: diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index 1b82b109..0005e0c1 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -15,7 +15,7 @@ async def main(): session1 = await client.create_session({"model": "claude-haiku-4.5"}) response1 = await session1.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response1 and response1.data.content: @@ -32,7 +32,7 @@ async def main(): session2 = await client.create_session({"model": "claude-haiku-4.5"}) response2 = await session2.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response2 and response2.data.content: diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index d1441361..1a488c05 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -13,7 +13,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index c407d4fe..a180f213 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -12,7 +12,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: From 3f683e31c36257b98a701c869a00fbca42339959 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 13 Mar 2026 11:45:53 -0700 Subject: [PATCH 2/3] Fix some code that didn't get migrated from the last merge with main --- python/e2e/test_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index 7a738303..5d5823d9 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -157,7 +157,7 @@ def tracking_handler(request, invocation): {"tools": [safe_lookup], "on_permission_request": tracking_handler} ) - await session.send({"prompt": "Use safe_lookup to look up 'test123'"}) + await session.send("Use safe_lookup to look up 'test123'") assistant_message = await get_final_assistant_message(session) assert "RESULT: test123" in assistant_message.data.content assert not did_run_permission_request From 7a8942dcb679a15be1c2d31459bb5835a6685b7c Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 13 Mar 2026 11:59:33 -0700 Subject: [PATCH 3/3] Add better typing --- python/copilot/session.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/python/copilot/session.py b/python/copilot/session.py index a2c91a2f..e4a17f2f 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -9,7 +9,7 @@ import inspect import threading from collections.abc import Callable -from typing import Any, cast +from typing import Any, Literal, cast from .generated.rpc import ( Kind, @@ -26,6 +26,7 @@ from .jsonrpc import JsonRpcError, ProcessExitedError from .telemetry import get_trace_context, trace_context from .types import ( + Attachment, PermissionRequest, PermissionRequestResult, SessionHooks, @@ -119,14 +120,15 @@ async def send( self, prompt: str, *, - attachments: list[Any] | None = None, - mode: str | None = None, + attachments: list[Attachment] | None = None, + mode: Literal["enqueue", "immediate"] | None = None, ) -> str: """ - Send a message to this session and wait for the response. + Send a message to this session. The message is processed asynchronously. Subscribe to events via :meth:`on` - to receive streaming responses and other session events. + to receive streaming responses and other session events. Use + :meth:`send_and_wait` to block until the assistant finishes processing. Args: prompt: The message text to send. @@ -134,7 +136,7 @@ async def send( mode: Message delivery mode (``"enqueue"`` or ``"immediate"``). Returns: - The message ID of the response, which can be used to correlate events. + The message ID assigned by the server, which can be used to correlate events. Raises: Exception: If the session has been disconnected or the connection fails. @@ -162,8 +164,8 @@ async def send_and_wait( self, prompt: str, *, - attachments: list[Any] | None = None, - mode: str | None = None, + attachments: list[Attachment] | None = None, + mode: Literal["enqueue", "immediate"] | None = None, timeout: float = 60.0, ) -> SessionEvent | None: """