From 8283fb9f26b1655cf50c3ce67cbe8774d885ff0a Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 25 Feb 2026 15:16:14 -0800 Subject: [PATCH 01/11] Refactor `CopilotClient.create_session()` to have parameters --- docs/auth/byok.md | 15 +- docs/getting-started.md | 29 ++-- docs/guides/session-persistence.md | 7 +- docs/guides/setup/azure-managed-identity.md | 44 +++--- docs/guides/setup/backend-services.md | 7 +- docs/guides/setup/bundled-cli.md | 4 +- docs/guides/setup/byok.md | 13 +- docs/guides/setup/github-oauth.md | 7 +- docs/guides/setup/local-cli.md | 4 +- docs/guides/skills.md | 21 +-- docs/hooks/error-handling.md | 6 +- docs/hooks/overview.md | 8 +- docs/hooks/post-tool-use.md | 6 +- docs/hooks/pre-tool-use.md | 6 +- docs/hooks/session-lifecycle.md | 14 +- docs/hooks/user-prompt-submitted.md | 6 +- docs/mcp/overview.md | 39 +++-- python/README.md | 138 +++++++++-------- python/copilot/__init__.py | 2 - python/copilot/client.py | 129 ++++++++-------- python/copilot/types.py | 49 ------ python/e2e/test_agent_and_compact_rpc.py | 100 ++++++------- python/e2e/test_ask_user.py | 18 +-- python/e2e/test_client.py | 8 +- python/e2e/test_compaction.py | 24 ++- python/e2e/test_hooks.py | 30 ++-- python/e2e/test_mcp_and_agents.py | 25 +--- python/e2e/test_permissions.py | 24 ++- python/e2e/test_rpc.py | 16 +- python/e2e/test_session.py | 139 +++++++----------- python/e2e/test_skills.py | 17 +-- python/e2e/test_tools.py | 26 +--- python/samples/chat.py | 6 +- python/test_client.py | 20 +-- .../auth/byok-anthropic/python/main.py | 15 +- test/scenarios/auth/byok-azure/python/main.py | 15 +- .../scenarios/auth/byok-ollama/python/main.py | 15 +- .../scenarios/auth/byok-openai/python/main.py | 11 +- test/scenarios/auth/gh-app/python/main.py | 4 +- .../app-backend-to-server/python/main.py | 4 +- .../bundling/app-direct-server/python/main.py | 4 +- .../bundling/container-proxy/python/main.py | 4 +- .../bundling/fully-bundled/python/main.py | 4 +- test/scenarios/callbacks/hooks/python/main.py | 24 ++- .../callbacks/permissions/python/main.py | 10 +- .../callbacks/user-input/python/main.py | 12 +- test/scenarios/modes/default/python/main.py | 6 +- test/scenarios/modes/minimal/python/main.py | 13 +- .../prompts/attachments/python/main.py | 11 +- .../prompts/reasoning-effort/python/main.py | 15 +- .../prompts/system-message/python/main.py | 11 +- .../concurrent-sessions/python/main.py | 20 ++- .../sessions/infinite-sessions/python/main.py | 15 +- .../sessions/session-resume/python/main.py | 9 +- .../sessions/streaming/python/main.py | 9 +- .../tools/custom-agents/python/main.py | 25 ++-- .../tools/mcp-servers/python/main.py | 11 +- test/scenarios/tools/no-tools/python/main.py | 11 +- test/scenarios/tools/skills/python/main.py | 14 +- .../tools/tool-filtering/python/main.py | 11 +- .../tools/virtual-filesystem/python/main.py | 12 +- .../transport/reconnect/python/main.py | 6 +- test/scenarios/transport/stdio/python/main.py | 4 +- test/scenarios/transport/tcp/python/main.py | 4 +- 64 files changed, 572 insertions(+), 764 deletions(-) diff --git a/docs/auth/byok.md b/docs/auth/byok.md index 13ad8b055..abab53ae0 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -23,7 +23,7 @@ Azure AI Foundry (formerly Azure OpenAI) is a common BYOK deployment target for ```python import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler FOUNDRY_MODEL_URL = "https://your-resource.openai.azure.com/openai/v1/" # Set FOUNDRY_API_KEY environment variable @@ -32,14 +32,11 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-5.2-codex", # Your deployment name - "provider": { - "type": "openai", - "base_url": FOUNDRY_MODEL_URL, - "wire_api": "responses", # Use "completions" for older models - "api_key": os.environ["FOUNDRY_API_KEY"], - }, + session = await client.create_session(PermissionHandler.approve_all, "gpt-5.2-codex", provider={ + "type": "openai", + "base_url": FOUNDRY_MODEL_URL, + "wire_api": "responses", # Use "completions" for older models + "api_key": os.environ["FOUNDRY_API_KEY"], }) done = asyncio.Event() diff --git a/docs/getting-started.md b/docs/getting-started.md index 56c6a9c46..a4482640d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -129,13 +129,13 @@ Create `main.py`: ```python import asyncio -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): client = CopilotClient() await client.start() - session = await client.create_session({"model": "gpt-4.1"}) + session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1") response = await session.send_and_wait({"prompt": "What is 2 + 2?"}) print(response.data.content) @@ -274,17 +274,14 @@ Update `main.py`: ```python import asyncio import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from copilot.generated.session_events import SessionEventType async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "streaming": True, - }) + session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", streaming=True) # Listen for response chunks def handle_event(event): @@ -565,7 +562,7 @@ Update `main.py`: import asyncio import random import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from copilot.tools import define_tool from copilot.generated.session_events import SessionEventType from pydantic import BaseModel, Field @@ -588,11 +585,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "streaming": True, - "tools": [get_weather], - }) + session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", streaming=True, tools=[get_weather]) def handle_event(event): if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: @@ -837,7 +830,7 @@ Create `weather_assistant.py`: import asyncio import random import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from copilot.tools import define_tool from copilot.generated.session_events import SessionEventType from pydantic import BaseModel, Field @@ -857,11 +850,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "streaming": True, - "tools": [get_weather], - }) + session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", streaming=True, tools=[get_weather]) def handle_event(event): if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: @@ -1218,7 +1207,7 @@ client = CopilotClient({ await client.start() # Use the client normally -session = await client.create_session({"on_permission_request": PermissionHandler.approve_all}) +session = await client.create_session(PermissionHandler.approve_all) # ... ``` diff --git a/docs/guides/session-persistence.md b/docs/guides/session-persistence.md index 527f5ecc7..de58ecd35 100644 --- a/docs/guides/session-persistence.md +++ b/docs/guides/session-persistence.md @@ -46,16 +46,13 @@ await session.sendAndWait({ prompt: "Analyze my codebase" }); ### Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler client = CopilotClient() await client.start() # Create a session with a meaningful ID -session = await client.create_session({ - "session_id": "user-123-task-456", - "model": "gpt-5.2-codex", -}) +session = await client.create_session(PermissionHandler.approve_all, "gpt-5.2-codex", session_id="user-123-task-456") # Do some work... await session.send_and_wait({"prompt": "Analyze my codebase"}) diff --git a/docs/guides/setup/azure-managed-identity.md b/docs/guides/setup/azure-managed-identity.md index bfafc6f91..c0296fe28 100644 --- a/docs/guides/setup/azure-managed-identity.md +++ b/docs/guides/setup/azure-managed-identity.md @@ -42,7 +42,7 @@ import asyncio import os from azure.identity import DefaultAzureCredential -from copilot import CopilotClient, ProviderConfig, SessionConfig +from copilot import CopilotClient, PermissionHandler COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default" @@ -58,15 +58,14 @@ async def main(): await client.start() session = await client.create_session( - SessionConfig( - model="gpt-4.1", - provider=ProviderConfig( - type="openai", - base_url=f"{foundry_url.rstrip('/')}/openai/v1/", - bearer_token=token, # Short-lived bearer token - wire_api="responses", - ), - ) + PermissionHandler.approve_all, + "gpt-4.1", + provider={ + "type": "openai", + "base_url": f"{foundry_url.rstrip('/')}/openai/v1/", + "bearer_token": token, # Short-lived bearer token + "wire_api": "responses", + }, ) response = await session.send_and_wait({"prompt": "Hello from Managed Identity!"}) @@ -84,7 +83,7 @@ Bearer tokens expire (typically after ~1 hour). For servers or long-running agen ```python from azure.identity import DefaultAzureCredential -from copilot import CopilotClient, ProviderConfig, SessionConfig +from copilot import CopilotClient, PermissionHandler COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default" @@ -98,24 +97,21 @@ class ManagedIdentityCopilotAgent: self.credential = DefaultAzureCredential() self.client = CopilotClient() - def _get_session_config(self) -> SessionConfig: - """Build a SessionConfig with a fresh bearer token.""" + def _get_provider_config(self) -> dict: + """Build a provider config dict with a fresh bearer token.""" token = self.credential.get_token(COGNITIVE_SERVICES_SCOPE).token - return SessionConfig( - model=self.model, - provider=ProviderConfig( - type="openai", - base_url=f"{self.foundry_url}/openai/v1/", - bearer_token=token, - wire_api="responses", - ), - ) + return { + "type": "openai", + "base_url": f"{self.foundry_url}/openai/v1/", + "bearer_token": token, + "wire_api": "responses", + } async def chat(self, prompt: str) -> str: """Send a prompt and return the response text.""" # Fresh token for each session - config = self._get_session_config() - session = await self.client.create_session(config) + provider = self._get_provider_config() + session = await self.client.create_session(PermissionHandler.approve_all, self.model, provider=provider) response = await session.send_and_wait({"prompt": prompt}) await session.destroy() diff --git a/docs/guides/setup/backend-services.md b/docs/guides/setup/backend-services.md index c9bc13f8d..81b552db5 100644 --- a/docs/guides/setup/backend-services.md +++ b/docs/guides/setup/backend-services.md @@ -111,17 +111,14 @@ res.json({ content: response?.data.content }); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler client = CopilotClient({ "cli_url": "localhost:4321", }) await client.start() -session = await client.create_session({ - "session_id": f"user-{user_id}-{int(time.time())}", - "model": "gpt-4.1", -}) +session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", session_id=f"user-{user_id}-{int(time.time())}") response = await session.send_and_wait({"prompt": message}) ``` diff --git a/docs/guides/setup/bundled-cli.md b/docs/guides/setup/bundled-cli.md index 6daf57b56..2429d5104 100644 --- a/docs/guides/setup/bundled-cli.md +++ b/docs/guides/setup/bundled-cli.md @@ -85,7 +85,7 @@ await client.stop(); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from pathlib import Path client = CopilotClient({ @@ -93,7 +93,7 @@ client = CopilotClient({ }) await client.start() -session = await client.create_session({"model": "gpt-4.1"}) +session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1") response = await session.send_and_wait({"prompt": "Hello!"}) print(response.data.content) diff --git a/docs/guides/setup/byok.md b/docs/guides/setup/byok.md index 5b8b8a460..2ca844259 100644 --- a/docs/guides/setup/byok.md +++ b/docs/guides/setup/byok.md @@ -93,18 +93,15 @@ await client.stop(); ```python import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler client = CopilotClient() await client.start() -session = await client.create_session({ - "model": "gpt-4.1", - "provider": { - "type": "openai", - "base_url": "https://api.openai.com/v1", - "api_key": os.environ["OPENAI_API_KEY"], - }, +session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", provider={ + "type": "openai", + "base_url": "https://api.openai.com/v1", + "api_key": os.environ["OPENAI_API_KEY"], }) response = await session.send_and_wait({"prompt": "Hello!"}) diff --git a/docs/guides/setup/github-oauth.md b/docs/guides/setup/github-oauth.md index 07251c8fb..94d859e3a 100644 --- a/docs/guides/setup/github-oauth.md +++ b/docs/guides/setup/github-oauth.md @@ -145,7 +145,7 @@ const response = await session.sendAndWait({ prompt: "Hello!" }); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler def create_client_for_user(user_token: str) -> CopilotClient: return CopilotClient({ @@ -157,10 +157,7 @@ def create_client_for_user(user_token: str) -> CopilotClient: client = create_client_for_user("gho_user_access_token") await client.start() -session = await client.create_session({ - "session_id": f"user-{user_id}-session", - "model": "gpt-4.1", -}) +session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", session_id=f"user-{user_id}-session") response = await session.send_and_wait({"prompt": "Hello!"}) ``` diff --git a/docs/guides/setup/local-cli.md b/docs/guides/setup/local-cli.md index a5fa906b8..4923d6710 100644 --- a/docs/guides/setup/local-cli.md +++ b/docs/guides/setup/local-cli.md @@ -51,12 +51,12 @@ await client.stop(); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler client = CopilotClient() await client.start() -session = await client.create_session({"model": "gpt-4.1"}) +session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1") response = await session.send_and_wait({"prompt": "Hello!"}) print(response.data.content) diff --git a/docs/guides/skills.md b/docs/guides/skills.md index b2ea3ae7a..75af80c36 100644 --- a/docs/guides/skills.md +++ b/docs/guides/skills.md @@ -48,14 +48,14 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "skill_directories": [ + session = await client.create_session( + lambda req: {"kind": "approved"}, + "gpt-4.1", + skill_directories=[ "./skills/code-review", "./skills/documentation", ], - "on_permission_request": lambda req: {"kind": "approved"}, - }) + ) # Copilot now has access to skills in those directories await session.send_and_wait({"prompt": "Review this code for security issues"}) @@ -159,10 +159,13 @@ const session = await client.createSession({ Python ```python -session = await client.create_session({ - "skill_directories": ["./skills"], - "disabled_skills": ["experimental-feature", "deprecated-tool"], -}) +from copilot import PermissionHandler + +session = await client.create_session( + PermissionHandler.approve_all, + skill_directories=["./skills"], + disabled_skills=["experimental-feature", "deprecated-tool"], +) ``` diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md index 0f705868d..b8e15f102 100644 --- a/docs/hooks/error-handling.md +++ b/docs/hooks/error-handling.md @@ -107,15 +107,15 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + async def on_error_occurred(input_data, invocation): print(f"[{invocation['session_id']}] Error: {input_data['error']}") print(f" Context: {input_data['errorContext']}") print(f" Recoverable: {input_data['recoverable']}") return None -session = await client.create_session({ - "hooks": {"on_error_occurred": on_error_occurred} -}) +session = await client.create_session(PermissionHandler.approve_all, hooks={"on_error_occurred": on_error_occurred}) ``` diff --git a/docs/hooks/overview.md b/docs/hooks/overview.md index a51ef0464..e54a57237 100644 --- a/docs/hooks/overview.md +++ b/docs/hooks/overview.md @@ -53,7 +53,7 @@ const session = await client.createSession({ Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): client = CopilotClient() @@ -70,13 +70,11 @@ async def main(): async def on_session_start(input_data, invocation): return {"additionalContext": "User prefers concise answers."} - session = await client.create_session({ - "hooks": { + session = await client.create_session(PermissionHandler.approve_all, hooks={ "on_pre_tool_use": on_pre_tool_use, "on_post_tool_use": on_post_tool_use, "on_session_start": on_session_start, - } - }) + }) ``` diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md index 0021e20a0..797458961 100644 --- a/docs/hooks/post-tool-use.md +++ b/docs/hooks/post-tool-use.md @@ -106,15 +106,15 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + async def on_post_tool_use(input_data, invocation): print(f"[{invocation['session_id']}] Tool: {input_data['toolName']}") print(f" Args: {input_data['toolArgs']}") print(f" Result: {input_data['toolResult']}") return None # Pass through unchanged -session = await client.create_session({ - "hooks": {"on_post_tool_use": on_post_tool_use} -}) +session = await client.create_session(PermissionHandler.approve_all, hooks={"on_post_tool_use": on_post_tool_use}) ``` diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md index ac12df4fa..7fb369cc6 100644 --- a/docs/hooks/pre-tool-use.md +++ b/docs/hooks/pre-tool-use.md @@ -114,14 +114,14 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + async def on_pre_tool_use(input_data, invocation): print(f"[{invocation['session_id']}] Calling {input_data['toolName']}") print(f" Args: {input_data['toolArgs']}") return {"permissionDecision": "allow"} -session = await client.create_session({ - "hooks": {"on_pre_tool_use": on_pre_tool_use} -}) +session = await client.create_session(PermissionHandler.approve_all, hooks={"on_pre_tool_use": on_pre_tool_use}) ``` diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index 74f4666f4..50a6b3ba2 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -113,6 +113,8 @@ Package manager: ${projectInfo.packageManager} Python ```python +from copilot import PermissionHandler + async def on_session_start(input_data, invocation): print(f"Session {invocation['session_id']} started ({input_data['source']})") @@ -126,9 +128,7 @@ Package manager: {project_info['packageManager']} """.strip() } -session = await client.create_session({ - "hooks": {"on_session_start": on_session_start} -}) +session = await client.create_session(PermissionHandler.approve_all, hooks={"on_session_start": on_session_start}) ``` @@ -309,6 +309,8 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + session_start_times = {} async def on_session_start(input_data, invocation): @@ -328,12 +330,10 @@ async def on_session_end(input_data, invocation): session_start_times.pop(invocation["session_id"], None) return None -session = await client.create_session({ - "hooks": { +session = await client.create_session(PermissionHandler.approve_all, hooks={ "on_session_start": on_session_start, "on_session_end": on_session_end, - } -}) + }) ``` diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md index 3205b95cd..4a2bda0ed 100644 --- a/docs/hooks/user-prompt-submitted.md +++ b/docs/hooks/user-prompt-submitted.md @@ -102,13 +102,13 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + async def on_user_prompt_submitted(input_data, invocation): print(f"[{invocation['session_id']}] User: {input_data['prompt']}") return None -session = await client.create_session({ - "hooks": {"on_user_prompt_submitted": on_user_prompt_submitted} -}) +session = await client.create_session(PermissionHandler.approve_all, hooks={"on_user_prompt_submitted": on_user_prompt_submitted}) ``` diff --git a/docs/mcp/overview.md b/docs/mcp/overview.md index aa2fba668..637079c78 100644 --- a/docs/mcp/overview.md +++ b/docs/mcp/overview.md @@ -59,32 +59,29 @@ const session = await client.createSession({ ```python import asyncio -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-5", - "mcp_servers": { - # Local MCP server (stdio) - "my-local-server": { - "type": "local", - "command": "python", - "args": ["./mcp_server.py"], - "env": {"DEBUG": "true"}, - "cwd": "./servers", - "tools": ["*"], - "timeout": 30000, - }, - # Remote MCP server (HTTP) - "github": { - "type": "http", - "url": "https://api.githubcopilot.com/mcp/", - "headers": {"Authorization": "Bearer ${TOKEN}"}, - "tools": ["*"], - }, + session = await client.create_session(PermissionHandler.approve_all, "gpt-5", mcp_servers={ + # Local MCP server (stdio) + "my-local-server": { + "type": "local", + "command": "python", + "args": ["./mcp_server.py"], + "env": {"DEBUG": "true"}, + "cwd": "./servers", + "tools": ["*"], + "timeout": 30000, + }, + # Remote MCP server (HTTP) + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": {"Authorization": "Bearer ${TOKEN}"}, + "tools": ["*"], }, }) diff --git a/python/README.md b/python/README.md index aa82e0c34..867af45f0 100644 --- a/python/README.md +++ b/python/README.md @@ -25,7 +25,7 @@ python chat.py ```python import asyncio -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): # Create and start client @@ -33,7 +33,7 @@ async def main(): await client.start() # Create a session - session = await client.create_session({"model": "gpt-5"}) + session = await client.create_session(PermissionHandler.approve_all, "gpt-5") # Wait for response using session.idle event done = asyncio.Event() @@ -80,7 +80,7 @@ client = CopilotClient({ }) await client.start() -session = await client.create_session({"model": "gpt-5"}) +session = await client.create_session(PermissionHandler.approve_all, "gpt-5") def on_event(event): print(f"Event: {event['type']}") @@ -107,18 +107,31 @@ await client.stop() - `github_token` (str): GitHub token for authentication. When provided, takes priority over other auth methods. - `use_logged_in_user` (bool): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). Cannot be used with `cli_url`. -**SessionConfig Options (for `create_session`):** +**`create_session` Parameters:** -- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** +- `on_permission_request` (callable): **Required.** Handler for permission requests from the server. +- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). + +The parameters below are keyword-only: + +- `session_id` (str): Custom session ID for resuming or identifying sessions. +- `client_name` (str): Client name to identify the application using the SDK. Included in the User-Agent header for API requests. - `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `list_models()` to check which models support this option. -- `session_id` (str): Custom session ID -- `tools` (list): Custom tools exposed to the CLI -- `system_message` (dict): System message configuration -- `streaming` (bool): Enable streaming delta events -- `provider` (dict): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. -- `infinite_sessions` (dict): Automatic context compaction configuration +- `tools` (list): Custom tools exposed to the CLI. +- `system_message` (dict): System message configuration. +- `available_tools` (list[str]): List of tool names to allow. Takes precedence over `excluded_tools`. +- `excluded_tools` (list[str]): List of tool names to disable. Ignored if `available_tools` is set. - `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `hooks` (dict): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. +- `working_directory` (str): Working directory for the session. Tool operations will be relative to this directory. +- `provider` (dict): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. +- `streaming` (bool): Enable streaming delta events. +- `mcp_servers` (dict): MCP server configurations for the session. +- `custom_agents` (list): Custom agent configurations for the session. +- `config_dir` (str): Override the default configuration directory location. +- `skill_directories` (list[str]): Directories to load skills from. +- `disabled_skills` (list[str]): List of skill names to disable. +- `infinite_sessions` (dict): Automatic context compaction configuration. **Session Lifecycle Methods:** @@ -155,7 +168,7 @@ Define tools with automatic JSON schema generation using the `@define_tool` deco ```python from pydantic import BaseModel, Field -from copilot import CopilotClient, define_tool +from copilot import CopilotClient, define_tool, PermissionHandler class LookupIssueParams(BaseModel): id: str = Field(description="Issue identifier") @@ -165,10 +178,11 @@ async def lookup_issue(params: LookupIssueParams) -> str: issue = await fetch_issue(params.id) return issue.summary -session = await client.create_session({ - "model": "gpt-5", - "tools": [lookup_issue], -}) +session = await client.create_session( + PermissionHandler.approve_all, + "gpt-5", + tools=[lookup_issue], +) ``` > **Note:** When using `from __future__ import annotations`, define Pydantic models at module level (not inside functions). @@ -178,7 +192,7 @@ session = await client.create_session({ For users who prefer manual schema definition: ```python -from copilot import CopilotClient, Tool +from copilot import CopilotClient, Tool, PermissionHandler async def lookup_issue(invocation): issue_id = invocation["arguments"]["id"] @@ -189,9 +203,10 @@ async def lookup_issue(invocation): "sessionLog": f"Fetched issue {issue_id}", } -session = await client.create_session({ - "model": "gpt-5", - "tools": [ +session = await client.create_session( + PermissionHandler.approve_all, + "gpt-5", + tools=[ Tool( name="lookup_issue", description="Fetch issue details from our tracker", @@ -205,7 +220,7 @@ session = await client.create_session({ handler=lookup_issue, ) ], -}) +) ``` The SDK automatically handles `tool.call`, executes your handler (sync or async), and responds with the final result when the tool completes. @@ -238,16 +253,17 @@ Enable streaming to receive assistant response chunks as they're generated: ```python import asyncio -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-5", - "streaming": True - }) + session = await client.create_session( + PermissionHandler.approve_all, + "gpt-5", + streaming=True, + ) # Use asyncio.Event to wait for completion done = asyncio.Event() @@ -298,27 +314,29 @@ By default, sessions use **infinite sessions** which automatically manage contex ```python # Default: infinite sessions enabled with default thresholds -session = await client.create_session({"model": "gpt-5"}) +session = await client.create_session(PermissionHandler.approve_all, "gpt-5") # Access the workspace path for checkpoints and files print(session.workspace_path) # => ~/.copilot/session-state/{session_id}/ # Custom thresholds -session = await client.create_session({ - "model": "gpt-5", - "infinite_sessions": { +session = await client.create_session( + PermissionHandler.approve_all, + "gpt-5", + infinite_sessions={ "enabled": True, "background_compaction_threshold": 0.80, # Start compacting at 80% context usage "buffer_exhaustion_threshold": 0.95, # Block at 95% until compaction completes }, -}) +) # Disable infinite sessions -session = await client.create_session({ - "model": "gpt-5", - "infinite_sessions": {"enabled": False}, -}) +session = await client.create_session( + PermissionHandler.approve_all, + "gpt-5", + infinite_sessions={"enabled": False}, +) ``` When enabled, sessions emit compaction events: @@ -342,14 +360,15 @@ The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own K **Example with Ollama:** ```python -session = await client.create_session({ - "model": "deepseek-coder-v2:16b", # Required when using custom provider - "provider": { +session = await client.create_session( + PermissionHandler.approve_all, + "deepseek-coder-v2:16b", # Model to use with the custom provider + provider={ "type": "openai", "base_url": "http://localhost:11434/v1", # Ollama endpoint # api_key not required for Ollama }, -}) +) await session.send({"prompt": "Hello!"}) ``` @@ -359,14 +378,15 @@ await session.send({"prompt": "Hello!"}) ```python import os -session = await client.create_session({ - "model": "gpt-4", - "provider": { +session = await client.create_session( + PermissionHandler.approve_all, + "gpt-4", + provider={ "type": "openai", "base_url": "https://my-api.example.com/v1", "api_key": os.environ["MY_API_KEY"], }, -}) +) ``` **Example with Azure OpenAI:** @@ -374,9 +394,10 @@ session = await client.create_session({ ```python import os -session = await client.create_session({ - "model": "gpt-4", - "provider": { +session = await client.create_session( + PermissionHandler.approve_all, + "gpt-4", + provider={ "type": "azure", # Must be "azure" for Azure endpoints, NOT "openai" "base_url": "https://my-resource.openai.azure.com", # Just the host, no path "api_key": os.environ["AZURE_OPENAI_KEY"], @@ -384,11 +405,10 @@ session = await client.create_session({ "api_version": "2024-10-21", }, }, -}) +) ``` > **Important notes:** -> - When using a custom provider, the `model` parameter is **required**. The SDK will throw an error if no model is specified. > - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`. > - The `base_url` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically. @@ -401,21 +421,22 @@ async def handle_user_input(request, invocation): # request["question"] - The question to ask # request.get("choices") - Optional list of choices for multiple choice # request.get("allowFreeform", True) - Whether freeform input is allowed - + print(f"Agent asks: {request['question']}") if request.get("choices"): print(f"Choices: {', '.join(request['choices'])}") - + # Return the user's response return { "answer": "User's answer here", "wasFreeform": True, # Whether the answer was freeform (not from choices) } -session = await client.create_session({ - "model": "gpt-5", - "on_user_input_request": handle_user_input, -}) +session = await client.create_session( + PermissionHandler.approve_all, + "gpt-5", + on_user_input_request=handle_user_input, +) ``` ## Session Hooks @@ -459,9 +480,10 @@ async def on_error_occurred(input, invocation): "errorHandling": "retry", # "retry", "skip", or "abort" } -session = await client.create_session({ - "model": "gpt-5", - "hooks": { +session = await client.create_session( + PermissionHandler.approve_all, + "gpt-5", + hooks={ "on_pre_tool_use": on_pre_tool_use, "on_post_tool_use": on_post_tool_use, "on_user_prompt_submitted": on_user_prompt_submitted, @@ -469,7 +491,7 @@ session = await client.create_session({ "on_session_end": on_session_end, "on_error_occurred": on_error_occurred, }, -}) +) ``` **Available hooks:** diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index f5f7ed0b1..d65c32b2d 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -27,7 +27,6 @@ PingResponse, ProviderConfig, ResumeSessionConfig, - SessionConfig, SessionContext, SessionEvent, SessionListFilter, @@ -63,7 +62,6 @@ "PingResponse", "ProviderConfig", "ResumeSessionConfig", - "SessionConfig", "SessionContext", "SessionEvent", "SessionListFilter", diff --git a/python/copilot/client.py b/python/copilot/client.py index 774569afb..6f73748e2 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -5,10 +5,10 @@ to the Copilot CLI server and provides session management capabilities. Example: - >>> from copilot import CopilotClient + >>> from copilot import CopilotClient, PermissionHandler >>> >>> async with CopilotClient() as client: - ... session = await client.create_session() + ... session = await client.create_session(PermissionHandler.approve_all) ... await session.send({"prompt": "Hello!"}) """ @@ -34,20 +34,27 @@ CustomAgentConfig, GetAuthStatusResponse, GetStatusResponse, + InfiniteSessionConfig, + MCPServerConfig, ModelInfo, PingResponse, ProviderConfig, + ReasoningEffort, ResumeSessionConfig, - SessionConfig, + SessionHooks, SessionLifecycleEvent, SessionLifecycleEventType, SessionLifecycleHandler, SessionListFilter, SessionMetadata, StopError, + SystemMessageConfig, + Tool, ToolHandler, ToolInvocation, ToolResult, + UserInputHandler, + _PermissionHandlerFn, ) @@ -417,7 +424,30 @@ async def force_stop(self) -> None: if not self._is_external_server: self._actual_port = None - async def create_session(self, config: SessionConfig) -> CopilotSession: + async def create_session( + self, + on_permission_request: _PermissionHandlerFn, + model: str | None = None, + *, + session_id: str | None = None, + client_name: str | None = None, + reasoning_effort: ReasoningEffort | None = None, + tools: list[Tool] | None = None, + system_message: SystemMessageConfig | None = None, + available_tools: list[str] | None = None, + excluded_tools: list[str] | None = None, + on_user_input_request: UserInputHandler | None = None, + hooks: SessionHooks | None = None, + working_directory: str | None = None, + provider: ProviderConfig | None = None, + streaming: bool | None = None, + mcp_servers: dict[str, MCPServerConfig] | None = None, + custom_agents: list[CustomAgentConfig] | None = None, + config_dir: str | None = None, + skill_directories: list[str] | None = None, + disabled_skills: list[str] | None = None, + infinite_sessions: InfiniteSessionConfig | None = None, + ) -> CopilotSession: """ Create a new conversation session with the Copilot CLI. @@ -426,8 +456,26 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: automatically start the connection. Args: - config: Optional configuration for the session, including model selection, - custom tools, system messages, and more. + on_permission_request: Handler for permission requests from the server. + model: Model to use for this session. + session_id: Custom session ID. + client_name: Client name to identify the application using the SDK. + reasoning_effort: Reasoning effort level ("low", "medium", "high", "xhigh"). + tools: Custom tools exposed to the CLI. + system_message: System message configuration. + available_tools: List of tool names to allow (takes precedence over excluded_tools). + excluded_tools: List of tool names to disable (ignored if available_tools is set). + on_user_input_request: Handler for user input requests (enables ask_user tool). + hooks: Hook handlers for intercepting session lifecycle events. + working_directory: Working directory for the session. + provider: Custom provider configuration (BYOK - Bring Your Own Key). + streaming: Enable streaming of assistant message and reasoning chunks. + mcp_servers: MCP server configurations for the session. + custom_agents: Custom agent configurations for the session. + config_dir: Override the default configuration directory location. + skill_directories: Directories to load skills from. + disabled_skills: List of skill names to disable. + infinite_sessions: Infinite session configuration for persistent workspaces. Returns: A :class:`CopilotSession` instance for the new session. @@ -436,16 +484,14 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: RuntimeError: If the client is not connected and auto_start is disabled. Example: - >>> # Basic session - >>> config = {"on_permission_request": PermissionHandler.approve_all} - >>> session = await client.create_session(config) + >>> session = await client.create_session(PermissionHandler.approve_all) >>> >>> # Session with model and streaming - >>> session = await client.create_session({ - ... "on_permission_request": PermissionHandler.approve_all, - ... "model": "gpt-4", - ... "streaming": True - ... }) + >>> session = await client.create_session( + ... PermissionHandler.approve_all, + ... "gpt-4", + ... streaming=True, + ... ) """ if not self._client: if self.options["auto_start"]: @@ -453,17 +499,7 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: else: raise RuntimeError("Client not connected. Call start() first.") - cfg = config - - if not cfg.get("on_permission_request"): - raise ValueError( - "An on_permission_request handler is required when creating a session. " - "For example, to allow all permissions, use " - '{"on_permission_request": PermissionHandler.approve_all}.' - ) - tool_defs = [] - tools = cfg.get("tools") if tools: for tool in tools: definition = { @@ -475,89 +511,60 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: tool_defs.append(definition) payload: dict[str, Any] = {} - if cfg.get("model"): - payload["model"] = cfg["model"] - if cfg.get("session_id"): - payload["sessionId"] = cfg["session_id"] - if cfg.get("client_name"): - payload["clientName"] = cfg["client_name"] - if cfg.get("reasoning_effort"): - payload["reasoningEffort"] = cfg["reasoning_effort"] + if model: + payload["model"] = model + if session_id: + payload["sessionId"] = session_id + if client_name: + payload["clientName"] = client_name + if reasoning_effort: + payload["reasoningEffort"] = reasoning_effort if tool_defs: payload["tools"] = tool_defs - # Add system message configuration if provided - system_message = cfg.get("system_message") if system_message: payload["systemMessage"] = system_message - # Add tool filtering options - available_tools = cfg.get("available_tools") if available_tools is not None: payload["availableTools"] = available_tools - excluded_tools = cfg.get("excluded_tools") if excluded_tools: payload["excludedTools"] = excluded_tools - # Always enable permission request callback (deny by default if no handler provided) - on_permission_request = cfg.get("on_permission_request") payload["requestPermission"] = True - # Enable user input request callback if handler provided - on_user_input_request = cfg.get("on_user_input_request") if on_user_input_request: payload["requestUserInput"] = True - # Enable hooks callback if any hook handler provided - hooks = cfg.get("hooks") if hooks and any(hooks.values()): payload["hooks"] = True - # Add working directory if provided - working_directory = cfg.get("working_directory") if working_directory: payload["workingDirectory"] = working_directory - # Add streaming option if provided - streaming = cfg.get("streaming") if streaming is not None: payload["streaming"] = streaming - # Add provider configuration if provided - provider = cfg.get("provider") if provider: payload["provider"] = self._convert_provider_to_wire_format(provider) - # Add MCP servers configuration if provided - mcp_servers = cfg.get("mcp_servers") if mcp_servers: payload["mcpServers"] = mcp_servers payload["envValueMode"] = "direct" - # Add custom agents configuration if provided - custom_agents = cfg.get("custom_agents") if custom_agents: payload["customAgents"] = [ self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents ] - # Add config directory override if provided - config_dir = cfg.get("config_dir") if config_dir: payload["configDir"] = config_dir - # Add skill directories configuration if provided - skill_directories = cfg.get("skill_directories") if skill_directories: payload["skillDirectories"] = skill_directories - # Add disabled skills configuration if provided - disabled_skills = cfg.get("disabled_skills") if disabled_skills: payload["disabledSkills"] = disabled_skills - # Add infinite sessions configuration if provided - infinite_sessions = cfg.get("infinite_sessions") if infinite_sessions: wire_config: dict[str, Any] = {} if "enabled" in infinite_sessions: diff --git a/python/copilot/types.py b/python/copilot/types.py index 142aee474..959f8319c 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -463,55 +463,6 @@ class InfiniteSessionConfig(TypedDict, total=False): buffer_exhaustion_threshold: float -# Configuration for creating a session -class SessionConfig(TypedDict, total=False): - """Configuration for creating a session""" - - session_id: str # Optional custom session ID - # Client name to identify the application using the SDK. - # Included in the User-Agent header for API requests. - client_name: str - model: str # Model to use for this session. Use client.list_models() to see available models. - # Reasoning effort level for models that support it. - # Only valid for models where capabilities.supports.reasoning_effort is True. - reasoning_effort: ReasoningEffort - tools: list[Tool] - system_message: SystemMessageConfig # System message configuration - # List of tool names to allow (takes precedence over excluded_tools) - available_tools: list[str] - # List of tool names to disable (ignored if available_tools is set) - excluded_tools: list[str] - # Handler for permission requests from the server - on_permission_request: _PermissionHandlerFn - # Handler for user input requests from the agent (enables ask_user tool) - on_user_input_request: UserInputHandler - # Hook handlers for intercepting session lifecycle events - hooks: SessionHooks - # Working directory for the session. Tool operations will be relative to this directory. - working_directory: str - # Custom provider configuration (BYOK - Bring Your Own Key) - provider: ProviderConfig - # Enable streaming of assistant message and reasoning chunks - # When True, assistant.message_delta and assistant.reasoning_delta events - # with delta_content are sent as the response is generated - streaming: bool - # MCP server configurations for the session - mcp_servers: dict[str, MCPServerConfig] - # Custom agent configurations for the session - custom_agents: list[CustomAgentConfig] - # Override the default configuration directory location. - # When specified, the session will use this directory for storing config and state. - config_dir: str - # Directories to load skills from - skill_directories: list[str] - # List of skill names to disable - disabled_skills: list[str] - # Infinite session configuration for persistent workspaces and automatic compaction. - # When enabled (default), sessions automatically manage context limits and persist state. - # Set to {"enabled": False} to disable. - infinite_sessions: InfiniteSessionConfig - - # Azure-specific provider options class AzureProviderOptions(TypedDict, total=False): """Azure-specific provider configuration""" diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index a960c8426..58b3972c7 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -19,23 +19,21 @@ async def test_should_list_available_custom_agents(self): try: await client.start() session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - "custom_agents": [ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - }, - { - "name": "another-agent", - "display_name": "Another Agent", - "description": "Another test agent", - "prompt": "You are another agent.", - }, - ], - } + PermissionHandler.approve_all, + custom_agents=[ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + }, + { + "name": "another-agent", + "display_name": "Another Agent", + "description": "Another test agent", + "prompt": "You are another agent.", + }, + ], ) result = await session.rpc.agent.list() @@ -59,17 +57,15 @@ async def test_should_return_null_when_no_agent_is_selected(self): try: await client.start() session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - "custom_agents": [ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - } - ], - } + PermissionHandler.approve_all, + custom_agents=[ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + } + ], ) result = await session.rpc.agent.get_current() @@ -88,17 +84,15 @@ async def test_should_select_and_get_current_agent(self): try: await client.start() session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - "custom_agents": [ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - } - ], - } + PermissionHandler.approve_all, + custom_agents=[ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + } + ], ) # Select the agent @@ -127,17 +121,15 @@ async def test_should_deselect_current_agent(self): try: await client.start() session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - "custom_agents": [ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - } - ], - } + PermissionHandler.approve_all, + custom_agents=[ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + } + ], ) # Select then deselect @@ -160,9 +152,7 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): try: await client.start() - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await client.create_session(PermissionHandler.approve_all) result = await session.rpc.agent.list() assert result.agents == [] @@ -177,9 +167,7 @@ class TestSessionCompactionRpc: @pytest.mark.asyncio async def test_should_compact_session_history_after_messages(self, ctx: E2ETestContext): """Test compacting session history via RPC.""" - session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all) # Send a message to create some history await session.send_and_wait({"prompt": "What is 2+2?"}) diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py index f409e460c..9942e4e20 100644 --- a/python/e2e/test_ask_user.py +++ b/python/e2e/test_ask_user.py @@ -30,10 +30,8 @@ async def on_user_input_request(request, invocation): } session = await ctx.client.create_session( - { - "on_user_input_request": on_user_input_request, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + on_user_input_request=on_user_input_request, ) await session.send_and_wait( @@ -69,10 +67,8 @@ async def on_user_input_request(request, invocation): } session = await ctx.client.create_session( - { - "on_user_input_request": on_user_input_request, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + on_user_input_request=on_user_input_request, ) await session.send_and_wait( @@ -110,10 +106,8 @@ async def on_user_input_request(request, invocation): } session = await ctx.client.create_session( - { - "on_user_input_request": on_user_input_request, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + on_user_input_request=on_user_input_request, ) response = await session.send_and_wait( diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index cc5d31ac6..59386335c 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -51,7 +51,7 @@ async def test_should_return_errors_on_failed_cleanup(self): client = CopilotClient({"cli_path": CLI_PATH}) try: - await client.create_session({"on_permission_request": PermissionHandler.approve_all}) + await client.create_session(PermissionHandler.approve_all) # Kill the server process to force cleanup to fail process = client._process @@ -69,7 +69,7 @@ async def test_should_return_errors_on_failed_cleanup(self): async def test_should_force_stop_without_cleanup(self): client = CopilotClient({"cli_path": CLI_PATH}) - await client.create_session({"on_permission_request": PermissionHandler.approve_all}) + await client.create_session(PermissionHandler.approve_all) await client.force_stop() assert client.get_state() == "disconnected" @@ -206,9 +206,7 @@ async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): # Verify subsequent calls also fail (don't hang) with pytest.raises(Exception) as exc_info2: - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await client.create_session(PermissionHandler.approve_all) await session.send("test") # Error message varies by platform (EINVAL on Windows, EPIPE on Linux) error_msg = str(exc_info2.value).lower() diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index 5447b4bad..6df20bf02 100644 --- a/python/e2e/test_compaction.py +++ b/python/e2e/test_compaction.py @@ -17,16 +17,14 @@ async def test_should_trigger_compaction_with_low_threshold_and_emit_events( ): # Create session with very low compaction thresholds to trigger compaction quickly session = await ctx.client.create_session( - { - "infinite_sessions": { - "enabled": True, - # Trigger background compaction at 0.5% context usage (~1000 tokens) - "background_compaction_threshold": 0.005, - # Block at 1% to ensure compaction runs - "buffer_exhaustion_threshold": 0.01, - }, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + infinite_sessions={ + "enabled": True, + # Trigger background compaction at 0.5% context usage (~1000 tokens) + "background_compaction_threshold": 0.005, + # Block at 1% to ensure compaction runs + "buffer_exhaustion_threshold": 0.01, + }, ) compaction_start_events = [] @@ -72,10 +70,8 @@ async def test_should_not_emit_compaction_events_when_infinite_sessions_disabled self, ctx: E2ETestContext ): session = await ctx.client.create_session( - { - "infinite_sessions": {"enabled": False}, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + infinite_sessions={"enabled": False}, ) compaction_events = [] diff --git a/python/e2e/test_hooks.py b/python/e2e/test_hooks.py index 8278fb33c..a4d432b10 100644 --- a/python/e2e/test_hooks.py +++ b/python/e2e/test_hooks.py @@ -24,10 +24,8 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "allow"} session = await ctx.client.create_session( - { - "hooks": {"on_pre_tool_use": on_pre_tool_use}, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + hooks={"on_pre_tool_use": on_pre_tool_use}, ) # Create a file for the model to read @@ -57,10 +55,8 @@ async def on_post_tool_use(input_data, invocation): return None session = await ctx.client.create_session( - { - "hooks": {"on_post_tool_use": on_post_tool_use}, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + hooks={"on_post_tool_use": on_post_tool_use}, ) # Create a file for the model to read @@ -95,13 +91,11 @@ async def on_post_tool_use(input_data, invocation): return None session = await ctx.client.create_session( - { - "hooks": { - "on_pre_tool_use": on_pre_tool_use, - "on_post_tool_use": on_post_tool_use, - }, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + hooks={ + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + }, ) write_file(ctx.work_dir, "both.txt", "Testing both hooks!") @@ -132,10 +126,8 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "deny"} session = await ctx.client.create_session( - { - "hooks": {"on_pre_tool_use": on_pre_tool_use}, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + hooks={"on_pre_tool_use": on_pre_tool_use}, ) # Create a file diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index b29a54827..c8659f1c1 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -33,7 +33,7 @@ async def test_should_accept_mcp_server_configuration_on_session_create( } session = await ctx.client.create_session( - {"mcp_servers": mcp_servers, "on_permission_request": PermissionHandler.approve_all} + PermissionHandler.approve_all, mcp_servers=mcp_servers ) assert session.session_id is not None @@ -50,9 +50,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( ): """Test that MCP server configuration is accepted on session resume""" # Create a session first - session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session1 = await ctx.client.create_session(PermissionHandler.approve_all) session_id = session1.session_id await session1.send_and_wait({"prompt": "What is 1+1?"}) @@ -95,10 +93,7 @@ async def test_should_pass_literal_env_values_to_mcp_server_subprocess( } session = await ctx.client.create_session( - { - "mcp_servers": mcp_servers, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, mcp_servers=mcp_servers ) assert session.session_id is not None @@ -131,7 +126,7 @@ async def test_should_accept_custom_agent_configuration_on_session_create( ] session = await ctx.client.create_session( - {"custom_agents": custom_agents, "on_permission_request": PermissionHandler.approve_all} + PermissionHandler.approve_all, custom_agents=custom_agents ) assert session.session_id is not None @@ -148,9 +143,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( ): """Test that custom agent configuration is accepted on session resume""" # Create a session first - session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session1 = await ctx.client.create_session(PermissionHandler.approve_all) session_id = session1.session_id await session1.send_and_wait({"prompt": "What is 1+1?"}) @@ -203,11 +196,9 @@ async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETe ] session = await ctx.client.create_session( - { - "mcp_servers": mcp_servers, - "custom_agents": custom_agents, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + mcp_servers=mcp_servers, + custom_agents=custom_agents, ) assert session.session_id is not None diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index c116053ba..a4242f46c 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -27,7 +27,7 @@ def on_permission_request( # Approve the permission return {"kind": "approved"} - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request) write_file(ctx.work_dir, "test.txt", "original content") @@ -53,7 +53,7 @@ def on_permission_request( # Deny all permissions return {"kind": "denied-interactively-by-user"} - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request) original_content = "protected content" write_file(ctx.work_dir, "protected.txt", original_content) @@ -76,7 +76,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies( def deny_all(request, invocation): return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"} - session = await ctx.client.create_session({"on_permission_request": deny_all}) + session = await ctx.client.create_session(deny_all) denied_events = [] done_event = asyncio.Event() @@ -107,9 +107,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ self, ctx: E2ETestContext ): """Test that tool operations are denied after resume when handler explicitly denies""" - session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session1 = await ctx.client.create_session(PermissionHandler.approve_all) session_id = session1.session_id await session1.send_and_wait({"prompt": "What is 1+1?"}) @@ -145,9 +143,7 @@ def on_event(event): async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETestContext): """Test that sessions work with approve-all permission handler""" - session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all) message = await session.send_and_wait({"prompt": "What is 2+2?"}) @@ -168,7 +164,7 @@ async def on_permission_request( await asyncio.sleep(0.01) return {"kind": "approved"} - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request) await session.send_and_wait({"prompt": "Run 'echo test' and tell me what happens"}) @@ -181,9 +177,7 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC permission_requests = [] # Create initial session - session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session1 = await ctx.client.create_session(PermissionHandler.approve_all) session_id = session1.session_id await session1.send_and_wait({"prompt": "What is 1+1?"}) @@ -213,7 +207,7 @@ def on_permission_request( ) -> PermissionRequestResult: raise RuntimeError("Handler error") - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request) message = await session.send_and_wait( {"prompt": "Run 'echo test'. If you can't, say 'failed'."} @@ -240,7 +234,7 @@ def on_permission_request( assert len(request["toolCallId"]) > 0 return {"kind": "approved"} - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request) await session.send_and_wait({"prompt": "Run 'echo test'"}) diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index 240cd3730..96101759c 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -78,7 +78,7 @@ class TestSessionRpc: async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): """Test calling session.rpc.model.getCurrent""" session = await ctx.client.create_session( - {"model": "claude-sonnet-4.5", "on_permission_request": PermissionHandler.approve_all} + PermissionHandler.approve_all, "claude-sonnet-4.5" ) result = await session.rpc.model.get_current() @@ -92,7 +92,7 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext from copilot.generated.rpc import SessionModelSwitchToParams session = await ctx.client.create_session( - {"model": "claude-sonnet-4.5", "on_permission_request": PermissionHandler.approve_all} + PermissionHandler.approve_all, "claude-sonnet-4.5" ) # Get initial model @@ -116,9 +116,7 @@ async def test_get_and_set_session_mode(self): try: await client.start() - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await client.create_session(PermissionHandler.approve_all) # Get initial mode (default should be interactive) initial = await session.rpc.mode.get() @@ -152,9 +150,7 @@ async def test_read_update_and_delete_plan(self): try: await client.start() - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await client.create_session(PermissionHandler.approve_all) # Initially plan should not exist initial = await session.rpc.plan.read() @@ -195,9 +191,7 @@ async def test_create_list_and_read_workspace_files(self): try: await client.start() - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await client.create_session(PermissionHandler.approve_all) # Initially no files initial_files = await session.rpc.workspace.list_files() diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 4842d7829..438a5abca 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -14,9 +14,7 @@ class TestSessions: async def test_should_create_and_destroy_sessions(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - {"model": "fake-test-model", "on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all, "fake-test-model") assert session.session_id messages = await session.get_messages() @@ -31,9 +29,7 @@ async def test_should_create_and_destroy_sessions(self, ctx: E2ETestContext): await session.get_messages() async def test_should_have_stateful_conversation(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all) assistant_message = await session.send_and_wait({"prompt": "What is 1+1?"}) assert assistant_message is not None @@ -50,10 +46,8 @@ async def test_should_create_a_session_with_appended_systemMessage_config( ): system_message_suffix = "End each response with the phrase 'Have a nice day!'" session = await ctx.client.create_session( - { - "system_message": {"mode": "append", "content": system_message_suffix}, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + system_message={"mode": "append", "content": system_message_suffix}, ) await session.send({"prompt": "What is your full name?"}) @@ -72,10 +66,8 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( ): test_system_message = "You are an assistant called Testy McTestface. Reply succinctly." session = await ctx.client.create_session( - { - "system_message": {"mode": "replace", "content": test_system_message}, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + system_message={"mode": "replace", "content": test_system_message}, ) await session.send({"prompt": "What is your full name?"}) @@ -90,10 +82,8 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestContext): session = await ctx.client.create_session( - { - "available_tools": ["view", "edit"], - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + available_tools=["view", "edit"], ) await session.send({"prompt": "What is 1+1?"}) @@ -109,7 +99,7 @@ async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestCon async def test_should_create_a_session_with_excludedTools(self, ctx: E2ETestContext): session = await ctx.client.create_session( - {"excluded_tools": ["view"], "on_permission_request": PermissionHandler.approve_all} + PermissionHandler.approve_all, excluded_tools=["view"] ) await session.send({"prompt": "What is 1+1?"}) @@ -132,9 +122,9 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont import asyncio s1, s2, s3 = await asyncio.gather( - ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}), - ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}), - ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}), + ctx.client.create_session(PermissionHandler.approve_all), + ctx.client.create_session(PermissionHandler.approve_all), + ctx.client.create_session(PermissionHandler.approve_all), ) # All sessions should have unique IDs @@ -156,9 +146,7 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestContext): # Create initial session - session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session1 = await ctx.client.create_session(PermissionHandler.approve_all) session_id = session1.session_id answer = await session1.send_and_wait({"prompt": "What is 1+1?"}) assert answer is not None @@ -174,9 +162,7 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestContext): # Create initial session - session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session1 = await ctx.client.create_session(PermissionHandler.approve_all) session_id = session1.session_id answer = await session1.send_and_wait({"prompt": "What is 1+1?"}) assert answer is not None @@ -219,13 +205,9 @@ async def test_should_list_sessions(self, ctx: E2ETestContext): import asyncio # Create a couple of sessions and send messages to persist them - session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session1 = await ctx.client.create_session(PermissionHandler.approve_all) await session1.send_and_wait({"prompt": "Say hello"}) - session2 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session2 = await ctx.client.create_session(PermissionHandler.approve_all) await session2.send_and_wait({"prompt": "Say goodbye"}) # Small delay to ensure session files are written to disk @@ -262,9 +244,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): import asyncio # Create a session and send a message to persist it - session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all) await session.send_and_wait({"prompt": "Hello"}) session_id = session.session_id @@ -300,21 +280,19 @@ def get_secret_number_handler(invocation): } session = await ctx.client.create_session( - { - "tools": [ - Tool( - name="get_secret_number", - description="Gets the secret number", - handler=get_secret_number_handler, - parameters={ - "type": "object", - "properties": {"key": {"type": "string", "description": "Key"}}, - "required": ["key"], - }, - ) - ], - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + tools=[ + Tool( + name="get_secret_number", + description="Gets the secret number", + handler=get_secret_number_handler, + parameters={ + "type": "object", + "properties": {"key": {"type": "string", "description": "Key"}}, + "required": ["key"], + }, + ) + ], ) answer = await session.send_and_wait({"prompt": "What is the secret number for key ALPHA?"}) @@ -323,37 +301,31 @@ def get_secret_number_handler(invocation): async def test_should_create_session_with_custom_provider(self, ctx: E2ETestContext): session = await ctx.client.create_session( - { - "provider": { - "type": "openai", - "base_url": "https://api.openai.com/v1", - "api_key": "fake-key", - }, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + provider={ + "type": "openai", + "base_url": "https://api.openai.com/v1", + "api_key": "fake-key", + }, ) assert session.session_id async def test_should_create_session_with_azure_provider(self, ctx: E2ETestContext): session = await ctx.client.create_session( - { - "provider": { - "type": "azure", - "base_url": "https://my-resource.openai.azure.com", - "api_key": "fake-key", - "azure": { - "api_version": "2024-02-15-preview", - }, + PermissionHandler.approve_all, + provider={ + "type": "azure", + "base_url": "https://my-resource.openai.azure.com", + "api_key": "fake-key", + "azure": { + "api_version": "2024-02-15-preview", }, - "on_permission_request": PermissionHandler.approve_all, - } + }, ) assert session.session_id async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all) session_id = session.session_id # Resume the session with a provider @@ -374,9 +346,7 @@ async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestCont async def test_should_abort_a_session(self, ctx: E2ETestContext): import asyncio - session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all) # Set up event listeners BEFORE sending to avoid race conditions wait_for_tool_start = asyncio.create_task( @@ -422,9 +392,7 @@ async def test_should_receive_streaming_delta_events_when_streaming_is_enabled( ): import asyncio - session = await ctx.client.create_session( - {"streaming": True, "on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all, streaming=True) delta_contents = [] done_event = asyncio.Event() @@ -465,9 +433,7 @@ def on_event(event): async def test_should_pass_streaming_option_to_session_creation(self, ctx: E2ETestContext): # Verify that the streaming option is accepted without errors - session = await ctx.client.create_session( - {"streaming": True, "on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all, streaming=True) assert session.session_id @@ -479,9 +445,7 @@ async def test_should_pass_streaming_option_to_session_creation(self, ctx: E2ETe async def test_should_receive_session_events(self, ctx: E2ETestContext): import asyncio - session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all) received_events = [] idle_event = asyncio.Event() @@ -517,10 +481,7 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo custom_config_dir = os.path.join(ctx.home_dir, "custom-config") session = await ctx.client.create_session( - { - "config_dir": custom_config_dir, - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, config_dir=custom_config_dir ) assert session.session_id diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index 10d32695c..3eaa1cda4 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -56,10 +56,7 @@ async def test_should_load_and_apply_skill_from_skilldirectories(self, ctx: E2ET """Test that skills are loaded and applied from skillDirectories""" skills_dir = create_skill_dir(ctx.work_dir) session = await ctx.client.create_session( - { - "skill_directories": [skills_dir], - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, skill_directories=[skills_dir] ) assert session.session_id is not None @@ -77,11 +74,9 @@ async def test_should_not_apply_skill_when_disabled_via_disabledskills( """Test that disabledSkills prevents skill from being applied""" skills_dir = create_skill_dir(ctx.work_dir) session = await ctx.client.create_session( - { - "skill_directories": [skills_dir], - "disabled_skills": ["test-skill"], - "on_permission_request": PermissionHandler.approve_all, - } + PermissionHandler.approve_all, + skill_directories=[skills_dir], + disabled_skills=["test-skill"], ) assert session.session_id is not None @@ -104,9 +99,7 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( skills_dir = create_skill_dir(ctx.work_dir) # Create a session without skills first - session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session1 = await ctx.client.create_session(PermissionHandler.approve_all) session_id = session1.session_id # First message without skill - marker should not appear diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index e4a9f5f06..81b0533f3 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -18,9 +18,7 @@ async def test_invokes_built_in_tools(self, ctx: E2ETestContext): with open(readme_path, "w") as f: f.write("# ELIZA, the only chatbot you'll ever need") - session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all) await session.send({"prompt": "What's the first line of README.md in this directory?"}) assistant_message = await get_final_assistant_message(session) @@ -35,7 +33,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: return params.input.upper() session = await ctx.client.create_session( - {"tools": [encrypt_string], "on_permission_request": PermissionHandler.approve_all} + PermissionHandler.approve_all, tools=[encrypt_string] ) await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) @@ -48,7 +46,7 @@ def get_user_location() -> str: raise Exception("Melbourne") session = await ctx.client.create_session( - {"tools": [get_user_location], "on_permission_request": PermissionHandler.approve_all} + PermissionHandler.approve_all, tools=[get_user_location] ) await session.send( @@ -112,9 +110,7 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]: City(countryId=12, cityName="San Lorenzo", population=204356), ] - session = await ctx.client.create_session( - {"tools": [db_query], "on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all, tools=[db_query]) expected_session_id = session.session_id await session.send( @@ -147,12 +143,7 @@ def on_permission_request(request, invocation): permission_requests.append(request) return {"kind": "approved"} - session = await ctx.client.create_session( - { - "tools": [encrypt_string], - "on_permission_request": on_permission_request, - } - ) + session = await ctx.client.create_session(on_permission_request, tools=[encrypt_string]) await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) assistant_message = await get_final_assistant_message(session) @@ -178,12 +169,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: def on_permission_request(request, invocation): return {"kind": "denied-interactively-by-user"} - session = await ctx.client.create_session( - { - "tools": [encrypt_string], - "on_permission_request": on_permission_request, - } - ) + session = await ctx.client.create_session(on_permission_request, tools=[encrypt_string]) await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) await get_final_assistant_message(session) diff --git a/python/samples/chat.py b/python/samples/chat.py index eb781e4e2..c04b148dd 100644 --- a/python/samples/chat.py +++ b/python/samples/chat.py @@ -9,11 +9,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - } - ) + session = await client.create_session(PermissionHandler.approve_all) def on_event(event): output = None diff --git a/python/test_client.py b/python/test_client.py index c6ad027f5..bd4372dae 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -16,8 +16,8 @@ async def test_create_session_raises_without_permission_handler(self): client = CopilotClient({"cli_path": CLI_PATH}) await client.start() try: - with pytest.raises(ValueError, match="on_permission_request.*is required"): - await client.create_session({}) + with pytest.raises(TypeError, match="on_permission_request"): + await client.create_session() # type: ignore[call-arg] finally: await client.force_stop() @@ -26,9 +26,7 @@ async def test_resume_session_raises_without_permission_handler(self): client = CopilotClient({"cli_path": CLI_PATH}) await client.start() try: - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await client.create_session(PermissionHandler.approve_all) with pytest.raises(ValueError, match="on_permission_request.*is required"): await client.resume_session(session.session_id, {}) finally: @@ -42,9 +40,7 @@ async def test_returns_failure_when_tool_not_registered(self): await client.start() try: - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await client.create_session(PermissionHandler.approve_all) response = await client._handle_tool_call_request( { @@ -191,9 +187,7 @@ async def mock_request(method, params): return await original_request(method, params) client._client.request = mock_request - await client.create_session( - {"client_name": "my-app", "on_permission_request": PermissionHandler.approve_all} - ) + await client.create_session(PermissionHandler.approve_all, client_name="my-app") assert captured["session.create"]["clientName"] == "my-app" finally: await client.force_stop() @@ -204,9 +198,7 @@ async def test_resume_session_forwards_client_name(self): await client.start() try: - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await client.create_session(PermissionHandler.approve_all) captured = {} original_request = client._client.request diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index 7f5e5834c..34e3be00a 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514") @@ -19,19 +19,20 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({ - "model": ANTHROPIC_MODEL, - "provider": { + session = await client.create_session( + PermissionHandler.approve_all, + ANTHROPIC_MODEL, + provider={ "type": "anthropic", "base_url": ANTHROPIC_BASE_URL, "api_key": ANTHROPIC_API_KEY, }, - "available_tools": [], - "system_message": { + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely.", }, - }) + ) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 5376cac28..bcf8f74c4 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY") @@ -20,9 +20,10 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({ - "model": AZURE_OPENAI_MODEL, - "provider": { + session = await client.create_session( + PermissionHandler.approve_all, + AZURE_OPENAI_MODEL, + provider={ "type": "azure", "base_url": AZURE_OPENAI_ENDPOINT, "api_key": AZURE_OPENAI_API_KEY, @@ -30,12 +31,12 @@ async def main(): "api_version": AZURE_API_VERSION, }, }, - "available_tools": [], - "system_message": { + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely.", }, - }) + ) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index 0f9df7f54..8b1d33131 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1") OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b") @@ -18,18 +18,19 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({ - "model": OLLAMA_MODEL, - "provider": { + session = await client.create_session( + PermissionHandler.approve_all, + OLLAMA_MODEL, + provider={ "type": "openai", "base_url": OLLAMA_BASE_URL, }, - "available_tools": [], - "system_message": { + available_tools=[], + system_message={ "mode": "replace", "content": COMPACT_SYSTEM_PROMPT, }, - }) + ) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index 651a92cd6..961257689 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "claude-haiku-4.5") @@ -19,14 +19,15 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({ - "model": OPENAI_MODEL, - "provider": { + session = await client.create_session( + PermissionHandler.approve_all, + OPENAI_MODEL, + provider={ "type": "openai", "base_url": OPENAI_BASE_URL, "api_key": OPENAI_API_KEY, }, - }) + ) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index 4568c82b2..ee47d53a0 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -4,7 +4,7 @@ import time import urllib.request -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler DEVICE_CODE_URL = "https://github.com/login/device/code" @@ -84,7 +84,7 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") response = await session.send_and_wait({"prompt": "What is the capital of France?"}) if response: print(response.data.content) 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 218505f4a..34283a17c 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -5,7 +5,7 @@ import urllib.request from flask import Flask, request, jsonify -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler app = Flask(__name__) @@ -16,7 +16,7 @@ async def ask_copilot(prompt: str) -> str: client = CopilotClient({"cli_url": CLI_URL}) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") response = await session.send_and_wait({"prompt": prompt}) diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index 05aaa9270..a47c926e9 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -9,7 +9,7 @@ async def main(): }) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index 05aaa9270..a47c926e9 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -9,7 +9,7 @@ async def main(): }) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index 138bb5646..cfe12901f 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -10,7 +10,7 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index a00c18af7..abbef404e 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler hook_log: list[str] = [] @@ -47,18 +47,16 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": auto_approve_permission, - "hooks": { - "on_session_start": on_session_start, - "on_session_end": on_session_end, - "on_pre_tool_use": on_pre_tool_use, - "on_post_tool_use": on_post_tool_use, - "on_user_prompt_submitted": on_user_prompt_submitted, - "on_error_occurred": on_error_occurred, - }, - } + auto_approve_permission, + "claude-haiku-4.5", + hooks={ + "on_session_start": on_session_start, + "on_session_end": on_session_end, + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + "on_user_prompt_submitted": on_user_prompt_submitted, + "on_error_occurred": on_error_occurred, + }, ) response = await session.send_and_wait( diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 2da5133fa..0827c8e17 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler # Track which tools requested permission permission_log: list[str] = [] @@ -23,11 +23,9 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": log_permission, - "hooks": {"on_pre_tool_use": auto_approve_tool}, - } + log_permission, + "claude-haiku-4.5", + hooks={"on_pre_tool_use": auto_approve_tool}, ) response = await session.send_and_wait( diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index fb36eda5c..cef027b20 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler input_log: list[str] = [] @@ -27,12 +27,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": auto_approve_permission, - "on_user_input_request": handle_user_input, - "hooks": {"on_pre_tool_use": auto_approve_tool}, - } + auto_approve_permission, + "claude-haiku-4.5", + on_user_input_request=handle_user_input, + hooks={"on_pre_tool_use": auto_approve_tool}, ) response = await session.send_and_wait( diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index 0abc6b709..4f708957b 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -10,9 +10,7 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - }) + session = await client.create_session(PermissionHandler.approve_all, "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."}) if response: diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 74a98ba0e..73d2d7fc7 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -10,14 +10,15 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - "available_tools": [], - "system_message": { + session = await client.create_session( + PermissionHandler.approve_all, + "claude-haiku-4.5", + available_tools=[], + system_message={ "mode": "replace", "content": "You have no tools. Respond with text only.", }, - }) + ) response = await session.send_and_wait({"prompt": "Use the grep tool to search for 'SDK' in README.md."}) if response: diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index acf9c7af1..048eddaf6 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler SYSTEM_PROMPT = """You are a helpful assistant. Answer questions about attached files concisely.""" @@ -13,11 +13,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": [], - } + PermissionHandler.approve_all, + "claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=[], ) sample_file = os.path.join(os.path.dirname(__file__), "..", "sample-data.txt") diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index 74444e7bf..45095ae29 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -10,15 +10,16 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({ - "model": "claude-opus-4.6", - "reasoning_effort": "low", - "available_tools": [], - "system_message": { + session = await client.create_session( + PermissionHandler.approve_all, + "claude-opus-4.6", + reasoning_effort="low", + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely.", }, - }) + ) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index a3bfccdcf..90df1729b 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler PIRATE_PROMPT = """You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.""" @@ -13,11 +13,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, - "available_tools": [], - } + PermissionHandler.approve_all, + "claude-haiku-4.5", + system_message={"mode": "replace", "content": PIRATE_PROMPT}, + available_tools=[], ) response = await session.send_and_wait( diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index 171a202e4..52446baf9 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler PIRATE_PROMPT = "You are a pirate. Always say Arrr!" ROBOT_PROMPT = "You are a robot. Always say BEEP BOOP!" @@ -15,18 +15,16 @@ async def main(): try: session1, session2 = await asyncio.gather( client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, - "available_tools": [], - } + PermissionHandler.approve_all, + "claude-haiku-4.5", + system_message={"mode": "replace", "content": PIRATE_PROMPT}, + available_tools=[], ), client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": ROBOT_PROMPT}, - "available_tools": [], - } + PermissionHandler.approve_all, + "claude-haiku-4.5", + system_message={"mode": "replace", "content": ROBOT_PROMPT}, + available_tools=[], ), ) diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index fe39a7117..13fbb1623 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -10,19 +10,20 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - "available_tools": [], - "system_message": { + session = await client.create_session( + PermissionHandler.approve_all, + "claude-haiku-4.5", + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely in one sentence.", }, - "infinite_sessions": { + infinite_sessions={ "enabled": True, "background_compaction_threshold": 0.80, "buffer_exhaustion_threshold": 0.95, }, - }) + ) prompts = [ "What is the capital of France?", diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index b65370b97..cb176b92c 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -12,10 +12,9 @@ async def main(): try: # 1. Create a session session = await client.create_session( - { - "model": "claude-haiku-4.5", - "available_tools": [], - } + PermissionHandler.approve_all, + "claude-haiku-4.5", + available_tools=[], ) # 2. Send the secret word diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index 2bbc94e78..bbe99ba23 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -11,10 +11,9 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "streaming": True, - } + PermissionHandler.approve_all, + "claude-haiku-4.5", + streaming=True, ) chunk_count = 0 diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index d4e416716..bb1e8cc2d 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -11,18 +11,17 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "custom_agents": [ - { - "name": "researcher", - "display_name": "Research Agent", - "description": "A research agent that can only read and search files, not modify them", - "tools": ["grep", "glob", "view"], - "prompt": "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.", - }, - ], - } + PermissionHandler.approve_all, + "claude-haiku-4.5", + custom_agents=[ + { + "name": "researcher", + "display_name": "Research Agent", + "description": "A research agent that can only read and search files, not modify them", + "tools": ["grep", "glob", "view"], + "prompt": "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.", + }, + ], ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index 81d2e39ba..ba7494839 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -22,8 +22,7 @@ async def main(): "args": args, } - session_config = { - "model": "claude-haiku-4.5", + session_kwargs = { "available_tools": [], "system_message": { "mode": "replace", @@ -31,9 +30,11 @@ async def main(): }, } if mcp_servers: - session_config["mcp_servers"] = mcp_servers + session_kwargs["mcp_servers"] = mcp_servers - session = await client.create_session(session_config) + session = await client.create_session( + PermissionHandler.approve_all, "claude-haiku-4.5", **session_kwargs + ) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index d857183c0..5a3079e1c 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler SYSTEM_PROMPT = """You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -16,11 +16,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": [], - } + PermissionHandler.approve_all, + "claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=[], ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 5adb74b76..089af9876 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -15,14 +15,12 @@ async def main(): skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") session = await client.create_session( - { - "model": "claude-haiku-4.5", - "skill_directories": [skills_dir], - "on_permission_request": lambda _: {"kind": "approved"}, - "hooks": { - "on_pre_tool_use": lambda _: {"permission_decision": "allow"}, - }, - } + lambda _: {"kind": "approved"}, + "claude-haiku-4.5", + skill_directories=[skills_dir], + hooks={ + "on_pre_tool_use": lambda _: {"permission_decision": "allow"}, + }, ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 174be620e..66e8baee3 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler SYSTEM_PROMPT = """You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.""" @@ -13,11 +13,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": ["grep", "glob", "view"], - } + PermissionHandler.approve_all, + "claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=["grep", "glob", "view"], ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index b150c1a2a..526411228 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -53,13 +53,11 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "available_tools": [], - "tools": [create_file, read_file, list_files], - "on_permission_request": auto_approve_permission, - "hooks": {"on_pre_tool_use": auto_approve_tool}, - } + auto_approve_permission, + "claude-haiku-4.5", + available_tools=[], + tools=[create_file, read_file, list_files], + hooks={"on_pre_tool_use": auto_approve_tool}, ) response = await session.send_and_wait( diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index e8aecea50..5e76db1e2 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -12,7 +12,7 @@ async def main(): try: # First session print("--- Session 1 ---") - session1 = await client.create_session({"model": "claude-haiku-4.5"}) + session1 = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") response1 = await session1.send_and_wait( {"prompt": "What is the capital of France?"} @@ -29,7 +29,7 @@ async def main(): # Second session — tests that the server accepts new sessions print("--- Session 2 ---") - session2 = await client.create_session({"model": "claude-haiku-4.5"}) + session2 = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") response2 = await session2.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index 138bb5646..cfe12901f 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -10,7 +10,7 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index 05aaa9270..a47c926e9 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): @@ -9,7 +9,7 @@ async def main(): }) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") response = await session.send_and_wait( {"prompt": "What is the capital of France?"} From 84372b75fcd7d6e4b41c4fa4dc96377f3212d75b Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 26 Feb 2026 11:42:39 -0800 Subject: [PATCH 02/11] Address PR review comments for create_session refactor - Add validation for on_permission_request in create_session to fail fast when handler is missing/invalid - Fix lambda signatures to accept two args (request, invocation) in test scenario and docs - Fix permissionDecision key to use camelCase in pre_tool_use hook Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/guides/skills.md | 2 +- python/copilot/client.py | 6 ++++++ test/scenarios/tools/skills/python/main.py | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/guides/skills.md b/docs/guides/skills.md index 75af80c36..d87341b79 100644 --- a/docs/guides/skills.md +++ b/docs/guides/skills.md @@ -49,7 +49,7 @@ async def main(): await client.start() session = await client.create_session( - lambda req: {"kind": "approved"}, + lambda req, inv: {"kind": "approved"}, "gpt-4.1", skill_directories=[ "./skills/code-review", diff --git a/python/copilot/client.py b/python/copilot/client.py index 6f73748e2..043d1c00c 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -493,6 +493,12 @@ async def create_session( ... streaming=True, ... ) """ + if not on_permission_request or not callable(on_permission_request): + raise ValueError( + "A valid on_permission_request handler is required. " + "Use PermissionHandler.approve_all or provide a custom handler." + ) + if not self._client: if self.options["auto_start"]: await self.start() diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 089af9876..ab4ee08f9 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -15,11 +15,11 @@ async def main(): skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") session = await client.create_session( - lambda _: {"kind": "approved"}, + lambda _, __: {"kind": "approved"}, "claude-haiku-4.5", skill_directories=[skills_dir], hooks={ - "on_pre_tool_use": lambda _: {"permission_decision": "allow"}, + "on_pre_tool_use": lambda _, __: {"permissionDecision": "allow"}, }, ) From 6b03aaee129e0d9d95212b37d993aa0af14cc79e Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 26 Feb 2026 11:45:06 -0800 Subject: [PATCH 03/11] Add test for None permission handler validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/test_client.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/python/test_client.py b/python/test_client.py index bd4372dae..a7e61ca03 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -21,6 +21,16 @@ async def test_create_session_raises_without_permission_handler(self): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_session_raises_with_none_permission_handler(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + try: + with pytest.raises(ValueError, match="on_permission_request handler is required"): + await client.create_session(None) # type: ignore[arg-type] + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_resume_session_raises_without_permission_handler(self): client = CopilotClient({"cli_path": CLI_PATH}) From 90b0e461bb14cbd7232b5901362596f25ac151df Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 12 Mar 2026 10:39:45 -0700 Subject: [PATCH 04/11] Merge with main --- python/e2e/test_streaming_fidelity.py | 12 +++--------- python/test_client.py | 4 +--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index 13a08b960..7fdec82d5 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -13,9 +13,7 @@ class TestStreamingFidelity: async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - PermissionHandler.approve_all, streaming=True - ) + session = await ctx.client.create_session(PermissionHandler.approve_all, streaming=True) events = [] session.on(lambda event: events.append(event)) @@ -45,9 +43,7 @@ async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: await session.disconnect() async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - PermissionHandler.approve_all, streaming=False - ) + session = await ctx.client.create_session(PermissionHandler.approve_all, streaming=False) events = [] session.on(lambda event: events.append(event)) @@ -66,9 +62,7 @@ async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E await session.disconnect() async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - PermissionHandler.approve_all, streaming=False - ) + session = await ctx.client.create_session(PermissionHandler.approve_all, streaming=False) await session.send_and_wait({"prompt": "What is 3 + 6?"}) await session.disconnect() diff --git a/python/test_client.py b/python/test_client.py index 5d4c7e681..76ee52eba 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -197,9 +197,7 @@ async def mock_request(method, params): def grep(params) -> str: return "ok" - await client.create_session( - PermissionHandler.approve_all, tools=[grep] - ) + await client.create_session(PermissionHandler.approve_all, tools=[grep]) tool_defs = captured["session.create"]["tools"] assert len(tool_defs) == 1 assert tool_defs[0]["name"] == "grep" From 768ddc89fad62d7fd03af0f0a8ed7fb81546d176 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 13 Mar 2026 12:29:47 -0700 Subject: [PATCH 05/11] Fix test to use SubprocessConfig instead of dict Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/test_client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/python/test_client.py b/python/test_client.py index 541b9d180..2841f4e50 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -31,7 +31,7 @@ async def test_create_session_raises_without_permission_handler(self): @pytest.mark.asyncio async def test_create_session_raises_with_none_permission_handler(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: with pytest.raises(ValueError, match="on_permission_request handler is required"): @@ -64,7 +64,7 @@ async def test_resume_session_raises_without_permission_handler(self): try: session = await client.create_session(PermissionHandler.approve_all) with pytest.raises(ValueError, match="on_permission_request.*is required"): - await client.resume_session(session.session_id, {}) + await client.resume_session(session.session_id, None) finally: await client.force_stop() @@ -218,7 +218,8 @@ def grep(params) -> str: await client.resume_session( session.session_id, - {"tools": [grep], "on_permission_request": PermissionHandler.approve_all}, + PermissionHandler.approve_all, + tools=[grep], ) tool_defs = captured["session.resume"]["tools"] assert len(tool_defs) == 1 @@ -390,7 +391,8 @@ async def mock_request(method, params): client._client.request = mock_request await client.resume_session( session.session_id, - {"client_name": "my-app", "on_permission_request": PermissionHandler.approve_all}, + PermissionHandler.approve_all, + client_name="my-app", ) assert captured["session.resume"]["clientName"] == "my-app" finally: @@ -439,11 +441,9 @@ async def mock_request(method, params): client._client.request = mock_request await client.resume_session( session.session_id, - { - "agent": "test-agent", - "custom_agents": [{"name": "test-agent", "prompt": "You are a test agent."}], - "on_permission_request": PermissionHandler.approve_all, - }, + PermissionHandler.approve_all, + agent="test-agent", + custom_agents=[{"name": "test-agent", "prompt": "You are a test agent."}], ) assert captured["session.resume"]["agent"] == "test-agent" finally: From bae88622c89dd38f2db5df3a136ecf7612196de3 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 13 Mar 2026 12:45:03 -0700 Subject: [PATCH 06/11] Change the call signature of `resume_session()` --- docs/features/session-persistence.md | 2 +- python/README.md | 29 +++ python/copilot/__init__.py | 2 - python/copilot/client.py | 167 +++++++++--------- python/copilot/types.py | 50 ------ python/e2e/test_mcp_and_agents.py | 9 +- python/e2e/test_multi_client.py | 14 +- python/e2e/test_permissions.py | 6 +- python/e2e/test_session.py | 30 ++-- python/e2e/test_skills.py | 6 +- python/e2e/test_streaming_fidelity.py | 3 +- python/e2e/test_tools.py | 4 +- .../sessions/session-resume/python/main.py | 2 +- 13 files changed, 146 insertions(+), 178 deletions(-) diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index a38a59b51..adfeb1e23 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -157,7 +157,7 @@ await session.sendAndWait({ prompt: "What did we discuss earlier?" }); ```python # Resume from a different client instance (or after restart) -session = await client.resume_session("user-123-task-456") +session = await client.resume_session("user-123-task-456", PermissionHandler.approve_all) # Continue where you left off await session.send_and_wait({"prompt": "What did we discuss earlier?"}) diff --git a/python/README.md b/python/README.md index 80885c169..cb85580ee 100644 --- a/python/README.md +++ b/python/README.md @@ -160,6 +160,35 @@ The parameters below are keyword-only: - `disabled_skills` (list[str]): List of skill names to disable. - `infinite_sessions` (dict): Automatic context compaction configuration. +**`resume_session` Parameters:** + +- `session_id` (str): **Required.** The ID of the session to resume. +- `on_permission_request` (callable): **Required.** Handler for permission requests from the server. +- `model` (str): Model to use (can change the model when resuming). + +The parameters below are keyword-only: + +- `client_name` (str): Client name to identify the application using the SDK. +- `reasoning_effort` (str): Reasoning effort level ("low", "medium", "high", "xhigh"). +- `tools` (list): Custom tools exposed to the CLI. +- `system_message` (dict): System message configuration. +- `available_tools` (list[str]): List of tool names to allow. Takes precedence over `excluded_tools`. +- `excluded_tools` (list[str]): List of tool names to disable. Ignored if `available_tools` is set. +- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). +- `hooks` (dict): Hook handlers for session lifecycle events. +- `working_directory` (str): Working directory for the session. +- `provider` (dict): Custom API provider configuration (BYOK). +- `streaming` (bool): Enable streaming delta events. +- `mcp_servers` (dict): MCP server configurations for the session. +- `custom_agents` (list): Custom agent configurations for the session. +- `agent` (str): Name of the custom agent to activate when the session starts. +- `config_dir` (str): Override the default configuration directory location. +- `skill_directories` (list[str]): Directories to load skills from. +- `disabled_skills` (list[str]): List of skill names to disable. +- `infinite_sessions` (dict): Automatic context compaction configuration. +- `disable_resume` (bool): Skip emitting the session.resume event (default: False). +- `on_event` (callable): Event handler registered before the session.resume RPC. + **Session Lifecycle Methods:** ```python diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 4afd43333..a76a91408 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -27,7 +27,6 @@ PermissionRequestResult, PingResponse, ProviderConfig, - ResumeSessionConfig, SessionContext, SessionEvent, SessionListFilter, @@ -65,7 +64,6 @@ "PermissionRequestResult", "PingResponse", "ProviderConfig", - "ResumeSessionConfig", "SessionContext", "SessionEvent", "SessionListFilter", diff --git a/python/copilot/client.py b/python/copilot/client.py index 46506fbe0..812c48988 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -43,7 +43,6 @@ PingResponse, ProviderConfig, ReasoningEffort, - ResumeSessionConfig, SessionEvent, SessionHooks, SessionLifecycleEvent, @@ -635,7 +634,33 @@ async def create_session( return session - async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> CopilotSession: + async def resume_session( + self, + session_id: str, + on_permission_request: _PermissionHandlerFn, + model: str | None = None, + *, + client_name: str | None = None, + reasoning_effort: ReasoningEffort | None = None, + tools: list[Tool] | None = None, + system_message: SystemMessageConfig | None = None, + available_tools: list[str] | None = None, + excluded_tools: list[str] | None = None, + on_user_input_request: UserInputHandler | None = None, + hooks: SessionHooks | None = None, + working_directory: str | None = None, + provider: ProviderConfig | None = None, + streaming: bool | None = None, + mcp_servers: dict[str, MCPServerConfig] | None = None, + custom_agents: list[CustomAgentConfig] | None = None, + agent: str | None = None, + config_dir: str | None = None, + skill_directories: list[str] | None = None, + disabled_skills: list[str] | None = None, + infinite_sessions: InfiniteSessionConfig | None = None, + disable_resume: bool = False, + on_event: Callable[[SessionEvent], None] | None = None, + ) -> CopilotSession: """ Resume an existing conversation session by its ID. @@ -645,7 +670,30 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> Args: session_id: The ID of the session to resume. - config: Optional configuration for the resumed session. + on_permission_request: Handler for permission requests from the server. + model: Model to use for this session. Can change the model when resuming. + client_name: Client name to identify the application using the SDK. + reasoning_effort: Reasoning effort level ("low", "medium", "high", "xhigh"). + tools: Custom tools exposed to the CLI. + system_message: System message configuration. + available_tools: List of tool names to allow (takes precedence over excluded_tools). + excluded_tools: List of tool names to disable (ignored if available_tools is set). + on_user_input_request: Handler for user input requests (enables ask_user tool). + hooks: Hook handlers for intercepting session lifecycle events. + working_directory: Working directory for the session. + provider: Custom provider configuration (BYOK - Bring Your Own Key). + streaming: Enable streaming of assistant message and reasoning chunks. + mcp_servers: MCP server configurations for the session. + custom_agents: Custom agent configurations for the session. + agent: Name of the custom agent to activate when the session starts. + config_dir: Override the default configuration directory location. + skill_directories: Directories to load skills from. + disabled_skills: List of skill names to disable. + infinite_sessions: Infinite session configuration for persistent workspaces. + disable_resume: When True, skips emitting the session.resume event. + Useful for reconnecting without triggering resume-related side effects. + on_event: Event handler registered before the session.resume RPC, ensuring + early events (e.g. session.start) are not missed. Returns: A :class:`CopilotSession` instance for the resumed session. @@ -654,33 +702,32 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> RuntimeError: If the session does not exist or the client is not connected. Example: - >>> # Resume a previous session - >>> config = {"on_permission_request": PermissionHandler.approve_all} - >>> session = await client.resume_session("session-123", config) + >>> session = await client.resume_session( + ... "session-123", + ... PermissionHandler.approve_all, + ... ) >>> - >>> # Resume with new tools - >>> session = await client.resume_session("session-123", { - ... "on_permission_request": PermissionHandler.approve_all, - ... "tools": [my_new_tool] - ... }) + >>> # Resume with model and streaming + >>> session = await client.resume_session( + ... "session-123", + ... PermissionHandler.approve_all, + ... "gpt-4", + ... streaming=True, + ... ) """ + if not on_permission_request or not callable(on_permission_request): + raise ValueError( + "A valid on_permission_request handler is required. " + "Use PermissionHandler.approve_all or provide a custom handler." + ) + if not self._client: if self._auto_start: await self.start() else: raise RuntimeError("Client not connected. Call start() first.") - cfg = config - - if not cfg.get("on_permission_request"): - raise ValueError( - "An on_permission_request handler is required when resuming a session. " - "For example, to allow all permissions, use " - '{"on_permission_request": PermissionHandler.approve_all}.' - ) - tool_defs = [] - tools = cfg.get("tools") if tools: for tool in tools: definition: dict[str, Any] = { @@ -696,104 +743,64 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> tool_defs.append(definition) payload: dict[str, Any] = {"sessionId": session_id} - - # Add client name if provided - client_name = cfg.get("client_name") - if client_name: - payload["clientName"] = client_name - - # Add model if provided - model = cfg.get("model") if model: payload["model"] = model - - if cfg.get("reasoning_effort"): - payload["reasoningEffort"] = cfg["reasoning_effort"] + if client_name: + payload["clientName"] = client_name + if reasoning_effort: + payload["reasoningEffort"] = reasoning_effort if tool_defs: payload["tools"] = tool_defs - # Add system message configuration if provided - system_message = cfg.get("system_message") if system_message: payload["systemMessage"] = system_message - # Add available/excluded tools if provided - available_tools = cfg.get("available_tools") if available_tools is not None: payload["availableTools"] = available_tools - - excluded_tools = cfg.get("excluded_tools") if excluded_tools is not None: payload["excludedTools"] = excluded_tools - provider = cfg.get("provider") - if provider: - payload["provider"] = self._convert_provider_to_wire_format(provider) - - # Add streaming option if provided - streaming = cfg.get("streaming") - if streaming is not None: - payload["streaming"] = streaming - - # Always enable permission request callback (deny by default if no handler provided) - on_permission_request = cfg.get("on_permission_request") payload["requestPermission"] = True - # Enable user input request callback if handler provided - on_user_input_request = cfg.get("on_user_input_request") if on_user_input_request: payload["requestUserInput"] = True - # Enable hooks callback if any hook handler provided - hooks = cfg.get("hooks") if hooks and any(hooks.values()): payload["hooks"] = True - # Add working directory if provided - working_directory = cfg.get("working_directory") if working_directory: payload["workingDirectory"] = working_directory - # Add config directory if provided - config_dir = cfg.get("config_dir") - if config_dir: - payload["configDir"] = config_dir + if streaming is not None: + payload["streaming"] = streaming - # Add disable resume flag if provided - disable_resume = cfg.get("disable_resume") - if disable_resume: - payload["disableResume"] = True + if provider: + payload["provider"] = self._convert_provider_to_wire_format(provider) - # Add MCP servers configuration if provided - mcp_servers = cfg.get("mcp_servers") if mcp_servers: payload["mcpServers"] = mcp_servers payload["envValueMode"] = "direct" - # Add custom agents configuration if provided - custom_agents = cfg.get("custom_agents") if custom_agents: payload["customAgents"] = [ - self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents + self._convert_custom_agent_to_wire_format(ca) for ca in custom_agents ] - # Add agent selection if provided - agent = cfg.get("agent") if agent: payload["agent"] = agent - # Add skill directories configuration if provided - skill_directories = cfg.get("skill_directories") + if config_dir: + payload["configDir"] = config_dir + + if disable_resume: + payload["disableResume"] = True + if skill_directories: payload["skillDirectories"] = skill_directories - # Add disabled skills configuration if provided - disabled_skills = cfg.get("disabled_skills") if disabled_skills: payload["disabledSkills"] = disabled_skills - # Add infinite sessions configuration if provided - infinite_sessions = cfg.get("infinite_sessions") if infinite_sessions: wire_config: dict[str, Any] = {} if "enabled" in infinite_sessions: @@ -818,13 +825,12 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> # Create and register the session before issuing the RPC so that # events emitted by the CLI (e.g. session.start) are not dropped. session = CopilotSession(session_id, self._client, None) - session._register_tools(cfg.get("tools")) + session._register_tools(tools) session._register_permission_handler(on_permission_request) if on_user_input_request: session._register_user_input_handler(on_user_input_request) if hooks: session._register_hooks(hooks) - on_event = cfg.get("on_event") if on_event: session.on(on_event) with self._sessions_lock: @@ -1054,8 +1060,9 @@ async def get_last_session_id(self) -> str | None: Example: >>> last_id = await client.get_last_session_id() >>> if last_id: - ... config = {"on_permission_request": PermissionHandler.approve_all} - ... session = await client.resume_session(last_id, config) + ... session = await client.resume_session( + ... last_id, PermissionHandler.approve_all + ... ) """ if not self._client: raise RuntimeError("Client not connected") diff --git a/python/copilot/types.py b/python/copilot/types.py index 1d1de1fde..67e7d5a90 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -548,56 +548,6 @@ class ProviderConfig(TypedDict, total=False): azure: AzureProviderOptions # Azure-specific options -# Configuration for resuming a session -class ResumeSessionConfig(TypedDict, total=False): - """Configuration for resuming a session""" - - # Client name to identify the application using the SDK. - # Included in the User-Agent header for API requests. - client_name: str - # Model to use for this session. Can change the model when resuming. - model: str - tools: list[Tool] - system_message: SystemMessageConfig # System message configuration - # List of tool names to allow (takes precedence over excluded_tools) - available_tools: list[str] - # List of tool names to disable (ignored if available_tools is set) - excluded_tools: list[str] - provider: ProviderConfig - # Reasoning effort level for models that support it. - reasoning_effort: ReasoningEffort - on_permission_request: _PermissionHandlerFn - # Handler for user input requestsfrom the agent (enables ask_user tool) - on_user_input_request: UserInputHandler - # Hook handlers for intercepting session lifecycle events - hooks: SessionHooks - # Working directory for the session. Tool operations will be relative to this directory. - working_directory: str - # Override the default configuration directory location. - config_dir: str - # Enable streaming of assistant message chunks - streaming: bool - # MCP server configurations for the session - mcp_servers: dict[str, MCPServerConfig] - # Custom agent configurations for the session - custom_agents: list[CustomAgentConfig] - # Name of the custom agent to activate when the session starts. - # Must match the name of one of the agents in custom_agents. - agent: str - # Directories to load skills from - skill_directories: list[str] - # List of skill names to disable - disabled_skills: list[str] - # Infinite session configuration for persistent workspaces and automatic compaction. - infinite_sessions: InfiniteSessionConfig - # When True, skips emitting the session.resume event. - # Useful for reconnecting to a session without triggering resume-related side effects. - disable_resume: bool - # Optional event handler registered before the session.resume RPC is issued, - # ensuring early events are delivered. - on_event: Callable[[SessionEvent], None] - - # Options for sending a message to a session class MessageOptions(TypedDict): """Options for sending a message to a session""" diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index 007c8c74b..4d656c805 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -66,7 +66,8 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( session2 = await ctx.client.resume_session( session_id, - {"mcp_servers": mcp_servers, "on_permission_request": PermissionHandler.approve_all}, + PermissionHandler.approve_all, + mcp_servers=mcp_servers, ) assert session2.session_id == session_id @@ -159,10 +160,8 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( session2 = await ctx.client.resume_session( session_id, - { - "custom_agents": custom_agents, - "on_permission_request": PermissionHandler.approve_all, - }, + PermissionHandler.approve_all, + custom_agents=custom_agents, ) assert session2.session_id == session_id diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index 56caac8d4..d620d98d5 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -202,10 +202,8 @@ def magic_number(params: SeedParams, invocation: ToolInvocation) -> str: # Client 2 resumes with NO tools — should not overwrite client 1's tools session2 = await mctx.client2.resume_session( - session1.session_id, {"on_permission_request": PermissionHandler.approve_all} + session1.session_id, PermissionHandler.approve_all ) - - # Track events seen by each client client1_events = [] client2_events = [] session1.on(lambda event: client1_events.append(event)) @@ -248,7 +246,7 @@ async def test_one_client_approves_permission_and_both_see_the_result( # Client 2 resumes — its handler never resolves, so only client 1's approval takes effect session2 = await mctx.client2.resume_session( session1.session_id, - {"on_permission_request": lambda request, invocation: asyncio.Future()}, + lambda request, invocation: asyncio.Future(), ) client1_events = [] @@ -296,7 +294,7 @@ async def test_one_client_rejects_permission_and_both_see_the_result( # Client 2 resumes — its handler never resolves session2 = await mctx.client2.resume_session( session1.session_id, - {"on_permission_request": lambda request, invocation: asyncio.Future()}, + lambda request, invocation: asyncio.Future(), ) client1_events = [] @@ -359,7 +357,8 @@ def currency_lookup(params: CountryCodeParams, invocation: ToolInvocation) -> st # Client 2 resumes with tool B (different tool, union should have both) session2 = await mctx.client2.resume_session( session1.session_id, - {"on_permission_request": PermissionHandler.approve_all, "tools": [currency_lookup]}, + PermissionHandler.approve_all, + tools=[currency_lookup], ) # Send prompts sequentially to avoid nondeterministic tool_call ordering @@ -410,7 +409,8 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: # Client 2 resumes with ephemeral_tool await mctx.client2.resume_session( session1.session_id, - {"on_permission_request": PermissionHandler.approve_all, "tools": [ephemeral_tool]}, + PermissionHandler.approve_all, + tools=[ephemeral_tool], ) # Verify both tools work before disconnect. diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index 1b27301d0..7a45f5d86 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -112,7 +112,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ def deny_all(request, invocation): return PermissionRequestResult() - session2 = await ctx.client.resume_session(session_id, {"on_permission_request": deny_all}) + session2 = await ctx.client.resume_session(session_id, deny_all) denied_events = [] done_event = asyncio.Event() @@ -186,9 +186,7 @@ def on_permission_request( permission_requests.append(request) return PermissionRequestResult(kind="approved") - session2 = await ctx.client.resume_session( - session_id, {"on_permission_request": on_permission_request} - ) + session2 = await ctx.client.resume_session(session_id, on_permission_request) await session2.send_and_wait({"prompt": "Run 'echo resumed' for me"}) diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 5f7fb0e68..3ed9f386a 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -153,9 +153,7 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC assert "2" in answer.data.content # Resume using the same client - session2 = await ctx.client.resume_session( - session_id, {"on_permission_request": PermissionHandler.approve_all} - ) + session2 = await ctx.client.resume_session(session_id, PermissionHandler.approve_all) assert session2.session_id == session_id answer2 = await get_final_assistant_message(session2) assert "2" in answer2.data.content @@ -189,9 +187,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont ) try: - session2 = await new_client.resume_session( - session_id, {"on_permission_request": PermissionHandler.approve_all} - ) + session2 = await new_client.resume_session(session_id, PermissionHandler.approve_all) assert session2.session_id == session_id messages = await session2.get_messages() @@ -211,7 +207,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont async def test_should_throw_error_resuming_nonexistent_session(self, ctx: E2ETestContext): with pytest.raises(Exception): await ctx.client.resume_session( - "non-existent-session-id", {"on_permission_request": PermissionHandler.approve_all} + "non-existent-session-id", PermissionHandler.approve_all ) async def test_should_list_sessions(self, ctx: E2ETestContext): @@ -279,9 +275,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Verify we cannot resume the deleted session with pytest.raises(Exception): - await ctx.client.resume_session( - session_id, {"on_permission_request": PermissionHandler.approve_all} - ) + await ctx.client.resume_session(session_id, PermissionHandler.approve_all) async def test_should_get_last_session_id(self, ctx: E2ETestContext): import asyncio @@ -359,13 +353,11 @@ async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestCont # Resume the session with a provider session2 = await ctx.client.resume_session( session_id, - { - "provider": { - "type": "openai", - "base_url": "https://api.openai.com/v1", - "api_key": "fake-key", - }, - "on_permission_request": PermissionHandler.approve_all, + PermissionHandler.approve_all, + provider={ + "type": "openai", + "base_url": "https://api.openai.com/v1", + "api_key": "fake-key", }, ) @@ -523,9 +515,7 @@ async def test_should_set_model_with_reasoning_effort(self, ctx: E2ETestContext) """Test that setModel passes reasoningEffort and it appears in the model_change event.""" import asyncio - session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) + session = await ctx.client.create_session(PermissionHandler.approve_all) model_change_event = asyncio.get_event_loop().create_future() diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index 42153969c..f651912db 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -110,10 +110,8 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( # Resume with skillDirectories - skill should now be active session2 = await ctx.client.resume_session( session_id, - { - "skill_directories": [skills_dir], - "on_permission_request": PermissionHandler.approve_all, - }, + PermissionHandler.approve_all, + skill_directories=[skills_dir], ) assert session2.session_id == session_id diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index 2162f4f07..f07d1fbf5 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -82,7 +82,8 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont try: session2 = await new_client.resume_session( session.session_id, - {"streaming": True, "on_permission_request": PermissionHandler.approve_all}, + PermissionHandler.approve_all, + streaming=True, ) events = [] session2.on(lambda event: events.append(event)) diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index 901bfe73f..14f9ef7f8 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -153,9 +153,7 @@ def tracking_handler(request, invocation): did_run_permission_request = True return PermissionRequestResult(kind="no-result") - session = await ctx.client.create_session( - {"tools": [safe_lookup], "on_permission_request": tracking_handler} - ) + session = await ctx.client.create_session(tracking_handler, tools=[safe_lookup]) await session.send({"prompt": "Use safe_lookup to look up 'test123'"}) assistant_message = await get_final_assistant_message(session) diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index ff8b8ada4..d87233867 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -26,7 +26,7 @@ async def main(): session_id = session.session_id # 4. Resume the session with the same ID - resumed = await client.resume_session(session_id) + resumed = await client.resume_session(session_id, PermissionHandler.approve_all) print("Session resumed") # 5. Ask for the secret word From 2b9bdf189660a63a6c9519d04ce067a97745a2f5 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 16 Mar 2026 11:23:06 -0700 Subject: [PATCH 07/11] Fix formatting --- python/copilot/types.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/python/copilot/types.py b/python/copilot/types.py index 7855b7fea..b7779a2c2 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -548,10 +548,6 @@ class ProviderConfig(TypedDict, total=False): azure: AzureProviderOptions # Azure-specific options - - - - # Event handler type SessionEventHandler = Callable[[SessionEvent], None] From 92d2ef83bbfb33ef4add3a70a01cda9287e03961 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 16 Mar 2026 12:03:38 -0700 Subject: [PATCH 08/11] Make on_permission_request and model keyword-only in Python SDK Update create_session() and resume_session() signatures so that on_permission_request and model are keyword-only parameters (after *). Update all call sites across test scenarios, unit tests, E2E tests, samples, and documentation to use keyword argument syntax. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/auth/byok.md | 2 +- docs/features/custom-agents.md | 6 +- docs/features/hooks.md | 10 ++-- docs/features/image-input.md | 4 +- docs/features/mcp.md | 2 +- docs/features/session-persistence.md | 4 +- docs/features/skills.md | 6 +- docs/features/steering-and-queueing.md | 12 ++-- docs/getting-started.md | 12 ++-- docs/hooks/error-handling.md | 2 +- docs/hooks/index.md | 2 +- docs/hooks/post-tool-use.md | 2 +- docs/hooks/pre-tool-use.md | 2 +- docs/hooks/session-lifecycle.md | 4 +- docs/hooks/user-prompt-submitted.md | 2 +- docs/setup/azure-managed-identity.md | 6 +- docs/setup/backend-services.md | 2 +- docs/setup/bundled-cli.md | 2 +- docs/setup/github-oauth.md | 2 +- docs/setup/local-cli.md | 2 +- python/README.md | 57 +++++++++--------- python/copilot/client.py | 20 ++++--- python/e2e/test_agent_and_compact_rpc.py | 20 ++----- python/e2e/test_ask_user.py | 6 +- python/e2e/test_client.py | 6 +- python/e2e/test_hooks.py | 8 +-- python/e2e/test_mcp_and_agents.py | 16 ++--- python/e2e/test_multi_client.py | 22 +++---- python/e2e/test_permissions.py | 22 +++---- python/e2e/test_rpc.py | 10 ++-- python/e2e/test_session.py | 58 +++++++++---------- python/e2e/test_skills.py | 8 +-- python/e2e/test_streaming_fidelity.py | 8 +-- python/e2e/test_tools.py | 16 ++--- python/samples/chat.py | 2 +- python/test_client.py | 28 ++++----- .../auth/byok-anthropic/python/main.py | 4 +- test/scenarios/auth/byok-azure/python/main.py | 4 +- .../scenarios/auth/byok-ollama/python/main.py | 4 +- .../scenarios/auth/byok-openai/python/main.py | 4 +- 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 | 4 +- test/scenarios/modes/default/python/main.py | 2 +- test/scenarios/modes/minimal/python/main.py | 4 +- .../prompts/attachments/python/main.py | 4 +- .../prompts/reasoning-effort/python/main.py | 4 +- .../prompts/system-message/python/main.py | 4 +- .../concurrent-sessions/python/main.py | 8 +-- .../sessions/infinite-sessions/python/main.py | 4 +- .../sessions/session-resume/python/main.py | 6 +- .../sessions/streaming/python/main.py | 4 +- .../tools/custom-agents/python/main.py | 4 +- .../tools/mcp-servers/python/main.py | 2 +- test/scenarios/tools/no-tools/python/main.py | 4 +- test/scenarios/tools/skills/python/main.py | 4 +- .../tools/tool-filtering/python/main.py | 4 +- .../tools/tool-overrides/python/main.py | 2 +- .../tools/virtual-filesystem/python/main.py | 4 +- .../transport/reconnect/python/main.py | 4 +- test/scenarios/transport/stdio/python/main.py | 2 +- test/scenarios/transport/tcp/python/main.py | 2 +- 67 files changed, 248 insertions(+), 255 deletions(-) diff --git a/docs/auth/byok.md b/docs/auth/byok.md index bb27473ef..8d9650280 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -32,7 +32,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(PermissionHandler.approve_all, "gpt-5.2-codex", provider={ + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.2-codex", provider={ "type": "openai", "base_url": FOUNDRY_MODEL_URL, "wire_api": "responses", # Use "completions" for older models diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index 3c6f46484..47712d9cf 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -71,8 +71,8 @@ client = CopilotClient() await client.start() session = await client.create_session( - lambda req, inv: PermissionRequestResult(kind="approved"), - "gpt-4.1", + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", custom_agents=[ { "name": "researcher", @@ -259,7 +259,7 @@ const session = await client.createSession({ ```python session = await client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, custom_agents=[ { "name": "researcher", diff --git a/docs/features/hooks.md b/docs/features/hooks.md index 42d425cbd..1a01c5f1a 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -66,7 +66,7 @@ client = CopilotClient() await client.start() session = await client.create_session( - lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: {"kind": "approved"}, hooks={ "on_session_start": on_session_start, "on_pre_tool_use": on_pre_tool_use, @@ -246,7 +246,7 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "allow"} session = await client.create_session( - lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: {"kind": "approved"}, hooks={"on_pre_tool_use": on_pre_tool_use}, ) ``` @@ -568,7 +568,7 @@ async def on_session_end(input_data, invocation): return None session = await client.create_session( - lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: {"kind": "approved"}, hooks={ "on_session_start": on_session_start, "on_user_prompt_submitted": on_user_prompt_submitted, @@ -667,7 +667,7 @@ async def on_error_occurred(input_data, invocation): return None session = await client.create_session( - lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: {"kind": "approved"}, hooks={ "on_session_end": on_session_end, "on_error_occurred": on_error_occurred, @@ -906,7 +906,7 @@ async def on_session_end(input_data, invocation): return None session = await client.create_session( - lambda req, inv: {"kind": "approved"}, + on_permission_request=lambda req, inv: {"kind": "approved"}, hooks={ "on_session_start": on_session_start, "on_user_prompt_submitted": on_user_prompt_submitted, diff --git a/docs/features/image-input.md b/docs/features/image-input.md index ff4ce626b..1e290b462 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -71,8 +71,8 @@ client = CopilotClient() await client.start() session = await client.create_session( - lambda req, inv: PermissionRequestResult(kind="approved"), - "gpt-4.1", + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", ) await session.send({ diff --git a/docs/features/mcp.md b/docs/features/mcp.md index a79454775..62465c0bd 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -65,7 +65,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(PermissionHandler.approve_all, "gpt-5", mcp_servers={ + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5", mcp_servers={ # Local MCP server (stdio) "my-local-server": { "type": "local", diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index adfeb1e23..3b0e9f69b 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -52,7 +52,7 @@ client = CopilotClient() await client.start() # Create a session with a meaningful ID -session = await client.create_session(PermissionHandler.approve_all, "gpt-5.2-codex", session_id="user-123-task-456") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.2-codex", session_id="user-123-task-456") # Do some work... await session.send_and_wait({"prompt": "Analyze my codebase"}) @@ -157,7 +157,7 @@ await session.sendAndWait({ prompt: "What did we discuss earlier?" }); ```python # Resume from a different client instance (or after restart) -session = await client.resume_session("user-123-task-456", PermissionHandler.approve_all) +session = await client.resume_session("user-123-task-456", on_permission_request=PermissionHandler.approve_all) # Continue where you left off await session.send_and_wait({"prompt": "What did we discuss earlier?"}) diff --git a/docs/features/skills.md b/docs/features/skills.md index 2c8525aac..466c637ff 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -50,8 +50,8 @@ async def main(): await client.start() session = await client.create_session( - lambda req, inv: {"kind": "approved"}, - "gpt-4.1", + on_permission_request=lambda req, inv: {"kind": "approved"}, + model="gpt-4.1", skill_directories=[ "./skills/code-review", "./skills/documentation", @@ -163,7 +163,7 @@ const session = await client.createSession({ from copilot import PermissionHandler session = await client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, skill_directories=["./skills"], disabled_skills=["experimental-feature", "deprecated-tool"], ) diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md index 26d7f227f..7da349e1c 100644 --- a/docs/features/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -77,8 +77,8 @@ async def main(): await client.start() session = await client.create_session( - lambda req, inv: PermissionRequestResult(kind="approved"), - "gpt-4.1", + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", ) # Start a long-running task @@ -236,8 +236,8 @@ async def main(): await client.start() session = await client.create_session( - lambda req, inv: PermissionRequestResult(kind="approved"), - "gpt-4.1", + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", ) # Send an initial task @@ -432,8 +432,8 @@ await session.send({ ```python session = await client.create_session( - lambda req, inv: PermissionRequestResult(kind="approved"), - "gpt-4.1", + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", ) # Start a task diff --git a/docs/getting-started.md b/docs/getting-started.md index 242c200c8..6c0aee72e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -135,7 +135,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") response = await session.send_and_wait({"prompt": "What is 2 + 2?"}) response = await session.send_and_wait({"prompt": "What is 2 + 2?"}) @@ -282,7 +282,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", streaming=True) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True) # Listen for response chunks def handle_event(event): @@ -431,7 +431,7 @@ from copilot.generated.session_events import SessionEvent, SessionEventType client = CopilotClient() -session = client.create_session(lambda req, inv: {"kind": "approved"}) +session = client.create_session(on_permission_request=lambda req, inv: {"kind": "approved"}) # Subscribe to all events unsubscribe = session.on(lambda event: print(f"Event: {event.type}")) @@ -674,7 +674,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", streaming=True, tools=[get_weather]) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True, tools=[get_weather]) def handle_event(event): if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: @@ -939,7 +939,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", streaming=True, tools=[get_weather]) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True, tools=[get_weather]) def handle_event(event): if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: @@ -1298,7 +1298,7 @@ client = CopilotClient({ await client.start() # Use the client normally -session = await client.create_session(PermissionHandler.approve_all) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all) # ... ``` diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md index 1d3781256..a67906ac9 100644 --- a/docs/hooks/error-handling.md +++ b/docs/hooks/error-handling.md @@ -154,7 +154,7 @@ async def on_error_occurred(input_data, invocation): print(f" Recoverable: {input_data['recoverable']}") return None -session = await client.create_session(PermissionHandler.approve_all, hooks={"on_error_occurred": on_error_occurred}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_error_occurred": on_error_occurred}) ``` diff --git a/docs/hooks/index.md b/docs/hooks/index.md index 383a43de4..d83b11b2f 100644 --- a/docs/hooks/index.md +++ b/docs/hooks/index.md @@ -70,7 +70,7 @@ async def main(): async def on_session_start(input_data, invocation): return {"additionalContext": "User prefers concise answers."} - session = await client.create_session(PermissionHandler.approve_all, hooks={ + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={ "on_pre_tool_use": on_pre_tool_use, "on_post_tool_use": on_post_tool_use, "on_session_start": on_session_start, diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md index 997bd2316..029e9eb2f 100644 --- a/docs/hooks/post-tool-use.md +++ b/docs/hooks/post-tool-use.md @@ -153,7 +153,7 @@ async def on_post_tool_use(input_data, invocation): print(f" Result: {input_data['toolResult']}") return None # Pass through unchanged -session = await client.create_session(PermissionHandler.approve_all, hooks={"on_post_tool_use": on_post_tool_use}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_post_tool_use": on_post_tool_use}) ``` diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md index 2709ac04a..e1bb97495 100644 --- a/docs/hooks/pre-tool-use.md +++ b/docs/hooks/pre-tool-use.md @@ -160,7 +160,7 @@ async def on_pre_tool_use(input_data, invocation): print(f" Args: {input_data['toolArgs']}") return {"permissionDecision": "allow"} -session = await client.create_session(PermissionHandler.approve_all, hooks={"on_pre_tool_use": on_pre_tool_use}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_pre_tool_use": on_pre_tool_use}) ``` diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index 0cb42260c..4efd33ccc 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -167,7 +167,7 @@ Package manager: {project_info['packageManager']} """.strip() } -session = await client.create_session(PermissionHandler.approve_all, hooks={"on_session_start": on_session_start}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_session_start": on_session_start}) ``` @@ -392,7 +392,7 @@ async def on_session_end(input_data, invocation): session_start_times.pop(invocation["session_id"], None) return None -session = await client.create_session(PermissionHandler.approve_all, hooks={ +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={ "on_session_start": on_session_start, "on_session_end": on_session_end, }) diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md index 47a191c76..2aca7f1ce 100644 --- a/docs/hooks/user-prompt-submitted.md +++ b/docs/hooks/user-prompt-submitted.md @@ -147,7 +147,7 @@ async def on_user_prompt_submitted(input_data, invocation): print(f"[{invocation['session_id']}] User: {input_data['prompt']}") return None -session = await client.create_session(PermissionHandler.approve_all, hooks={"on_user_prompt_submitted": on_user_prompt_submitted}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_user_prompt_submitted": on_user_prompt_submitted}) ``` diff --git a/docs/setup/azure-managed-identity.md b/docs/setup/azure-managed-identity.md index 253456d48..40d87c5ba 100644 --- a/docs/setup/azure-managed-identity.md +++ b/docs/setup/azure-managed-identity.md @@ -58,8 +58,8 @@ async def main(): await client.start() session = await client.create_session( - PermissionHandler.approve_all, - "gpt-4.1", + on_permission_request=PermissionHandler.approve_all, + model="gpt-4.1", provider={ "type": "openai", "base_url": f"{foundry_url.rstrip('/')}/openai/v1/", @@ -111,7 +111,7 @@ class ManagedIdentityCopilotAgent: """Send a prompt and return the response text.""" # Fresh token for each session provider = self._get_provider_config() - session = await self.client.create_session(PermissionHandler.approve_all, self.model, provider=provider) + session = await self.client.create_session(on_permission_request=PermissionHandler.approve_all, model=self.model, provider=provider) response = await session.send_and_wait({"prompt": prompt}) await session.disconnect() diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index b89b8e35f..735adf4ff 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -118,7 +118,7 @@ client = CopilotClient({ }) await client.start() -session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", session_id=f"user-{user_id}-{int(time.time())}") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-{int(time.time())}") response = await session.send_and_wait({"prompt": message}) ``` diff --git a/docs/setup/bundled-cli.md b/docs/setup/bundled-cli.md index c8aa8f8f4..cdfe6df81 100644 --- a/docs/setup/bundled-cli.md +++ b/docs/setup/bundled-cli.md @@ -93,7 +93,7 @@ client = CopilotClient({ }) await client.start() -session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") response = await session.send_and_wait({"prompt": "Hello!"}) print(response.data.content) diff --git a/docs/setup/github-oauth.md b/docs/setup/github-oauth.md index 6b201643b..81d2b25a2 100644 --- a/docs/setup/github-oauth.md +++ b/docs/setup/github-oauth.md @@ -157,7 +157,7 @@ def create_client_for_user(user_token: str) -> CopilotClient: client = create_client_for_user("gho_user_access_token") await client.start() -session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1", session_id=f"user-{user_id}-session") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-session") response = await session.send_and_wait({"prompt": "Hello!"}) ``` diff --git a/docs/setup/local-cli.md b/docs/setup/local-cli.md index 472f91c1c..bb95a4d38 100644 --- a/docs/setup/local-cli.md +++ b/docs/setup/local-cli.md @@ -56,7 +56,7 @@ from copilot import CopilotClient, PermissionHandler client = CopilotClient() await client.start() -session = await client.create_session(PermissionHandler.approve_all, "gpt-4.1") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") response = await session.send_and_wait({"prompt": "Hello!"}) print(response.data.content) diff --git a/python/README.md b/python/README.md index 719384eb3..e245640e0 100644 --- a/python/README.md +++ b/python/README.md @@ -33,7 +33,7 @@ async def main(): await client.start() # Create a session - session = await client.create_session(PermissionHandler.approve_all, "gpt-5") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") # Wait for response using session.idle event done = asyncio.Event() @@ -60,7 +60,7 @@ asyncio.run(main()) Sessions also support the `async with` context manager pattern for automatic cleanup: ```python -async with await client.create_session(PermissionHandler.approve_all, "gpt-5") as session: +async with await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") as session: await session.send("What is 2+2?") # session is automatically disconnected when leaving the block ``` @@ -85,7 +85,7 @@ from copilot import CopilotClient, SubprocessConfig client = CopilotClient() # uses bundled CLI, stdio transport await client.start() -session = await client.create_session(PermissionHandler.approve_all, "gpt-5") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") def on_event(event): print(f"Event: {event['type']}") @@ -136,11 +136,10 @@ CopilotClient( **`create_session` Parameters:** +All parameters are keyword-only: + - `on_permission_request` (callable): **Required.** Handler for permission requests from the server. - `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). - -The parameters below are keyword-only: - - `session_id` (str): Custom session ID for resuming or identifying sessions. - `client_name` (str): Client name to identify the application using the SDK. Included in the User-Agent header for API requests. - `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `list_models()` to check which models support this option. @@ -163,11 +162,11 @@ The parameters below are keyword-only: **`resume_session` Parameters:** - `session_id` (str): **Required.** The ID of the session to resume. -- `on_permission_request` (callable): **Required.** Handler for permission requests from the server. -- `model` (str): Model to use (can change the model when resuming). The parameters below are keyword-only: +- `on_permission_request` (callable): **Required.** Handler for permission requests from the server. +- `model` (str): Model to use (can change the model when resuming). - `client_name` (str): Client name to identify the application using the SDK. - `reasoning_effort` (str): Reasoning effort level ("low", "medium", "high", "xhigh"). - `tools` (list): Custom tools exposed to the CLI. @@ -235,8 +234,8 @@ async def lookup_issue(params: LookupIssueParams) -> str: return issue.summary session = await client.create_session( - PermissionHandler.approve_all, - "gpt-5", + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", tools=[lookup_issue], ) ``` @@ -260,8 +259,8 @@ async def lookup_issue(invocation): } session = await client.create_session( - PermissionHandler.approve_all, - "gpt-5", + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", tools=[ Tool( name="lookup_issue", @@ -340,8 +339,8 @@ async def main(): await client.start() session = await client.create_session( - PermissionHandler.approve_all, - "gpt-5", + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", streaming=True, ) @@ -394,7 +393,7 @@ By default, sessions use **infinite sessions** which automatically manage contex ```python # Default: infinite sessions enabled with default thresholds -session = await client.create_session(PermissionHandler.approve_all, "gpt-5") +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") # Access the workspace path for checkpoints and files print(session.workspace_path) @@ -402,8 +401,8 @@ print(session.workspace_path) # Custom thresholds session = await client.create_session( - PermissionHandler.approve_all, - "gpt-5", + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", infinite_sessions={ "enabled": True, "background_compaction_threshold": 0.80, # Start compacting at 80% context usage @@ -413,8 +412,8 @@ session = await client.create_session( # Disable infinite sessions session = await client.create_session( - PermissionHandler.approve_all, - "gpt-5", + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", infinite_sessions={"enabled": False}, ) ``` @@ -441,8 +440,8 @@ The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own K ```python session = await client.create_session( - PermissionHandler.approve_all, - "deepseek-coder-v2:16b", # Model to use with the custom provider + on_permission_request=PermissionHandler.approve_all, + model="deepseek-coder-v2:16b", # Model to use with the custom provider provider={ "type": "openai", "base_url": "http://localhost:11434/v1", # Ollama endpoint @@ -459,8 +458,8 @@ await session.send("Hello!") import os session = await client.create_session( - PermissionHandler.approve_all, - "gpt-4", + on_permission_request=PermissionHandler.approve_all, + model="gpt-4", provider={ "type": "openai", "base_url": "https://my-api.example.com/v1", @@ -475,8 +474,8 @@ session = await client.create_session( import os session = await client.create_session( - PermissionHandler.approve_all, - "gpt-4", + on_permission_request=PermissionHandler.approve_all, + model="gpt-4", provider={ "type": "azure", # Must be "azure" for Azure endpoints, NOT "openai" "base_url": "https://my-resource.openai.azure.com", # Just the host, no path @@ -539,8 +538,8 @@ async def handle_user_input(request, invocation): } session = await client.create_session( - PermissionHandler.approve_all, - "gpt-5", + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", on_user_input_request=handle_user_input, ) ``` @@ -587,8 +586,8 @@ async def on_error_occurred(input, invocation): } session = await client.create_session( - PermissionHandler.approve_all, - "gpt-5", + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", hooks={ "on_pre_tool_use": on_pre_tool_use, "on_post_tool_use": on_post_tool_use, diff --git a/python/copilot/client.py b/python/copilot/client.py index ff93b9a5a..1b9fd1ef1 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -8,7 +8,7 @@ >>> from copilot import CopilotClient, PermissionHandler >>> >>> async with CopilotClient() as client: - ... session = await client.create_session(PermissionHandler.approve_all) + ... session = await client.create_session(on_permission_request=PermissionHandler.approve_all) ... await session.send("Hello!") """ @@ -433,9 +433,9 @@ async def force_stop(self) -> None: async def create_session( self, + *, on_permission_request: _PermissionHandlerFn, model: str | None = None, - *, session_id: str | None = None, client_name: str | None = None, reasoning_effort: ReasoningEffort | None = None, @@ -496,12 +496,14 @@ async def create_session( RuntimeError: If the client is not connected and auto_start is disabled. Example: - >>> session = await client.create_session(PermissionHandler.approve_all) + >>> session = await client.create_session( + ... on_permission_request=PermissionHandler.approve_all, + ... ) >>> >>> # Session with model and streaming >>> session = await client.create_session( - ... PermissionHandler.approve_all, - ... "gpt-4", + ... on_permission_request=PermissionHandler.approve_all, + ... model="gpt-4", ... streaming=True, ... ) """ @@ -639,9 +641,9 @@ async def create_session( async def resume_session( self, session_id: str, + *, on_permission_request: _PermissionHandlerFn, model: str | None = None, - *, client_name: str | None = None, reasoning_effort: ReasoningEffort | None = None, tools: list[Tool] | None = None, @@ -706,14 +708,14 @@ async def resume_session( Example: >>> session = await client.resume_session( ... "session-123", - ... PermissionHandler.approve_all, + ... on_permission_request=PermissionHandler.approve_all, ... ) >>> >>> # Resume with model and streaming >>> session = await client.resume_session( ... "session-123", - ... PermissionHandler.approve_all, - ... "gpt-4", + ... on_permission_request=PermissionHandler.approve_all, + ... model="gpt-4", ... streaming=True, ... ) """ diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index 71fd5bcfa..0a3e0ea71 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -19,16 +19,8 @@ async def test_should_list_available_custom_agents(self): try: await client.start() session = await client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, custom_agents=[ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - }, - { - "name": "another-agent", "display_name": "Another Agent", "description": "Another test agent", "prompt": "You are another agent.", @@ -57,7 +49,7 @@ async def test_should_return_null_when_no_agent_is_selected(self): try: await client.start() session = await client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, custom_agents=[ { "name": "test-agent", @@ -84,7 +76,7 @@ async def test_should_select_and_get_current_agent(self): try: await client.start() session = await client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, custom_agents=[ { "name": "test-agent", @@ -121,7 +113,7 @@ async def test_should_deselect_current_agent(self): try: await client.start() session = await client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, custom_agents=[ { "name": "test-agent", @@ -152,7 +144,7 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): try: await client.start() - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) result = await session.rpc.agent.list() assert result.agents == [] @@ -167,7 +159,7 @@ class TestSessionCompactionRpc: @pytest.mark.asyncio async def test_should_compact_session_history_after_messages(self, ctx: E2ETestContext): """Test compacting session history via RPC.""" - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) # Send a message to create some history await session.send_and_wait("What is 2+2?") diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py index b5738e301..fc4cc60b5 100644 --- a/python/e2e/test_ask_user.py +++ b/python/e2e/test_ask_user.py @@ -30,7 +30,7 @@ async def on_user_input_request(request, invocation): } session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, on_user_input_request=on_user_input_request, ) @@ -63,7 +63,7 @@ async def on_user_input_request(request, invocation): } session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, on_user_input_request=on_user_input_request, ) @@ -98,7 +98,7 @@ async def on_user_input_request(request, invocation): } session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, on_user_input_request=on_user_input_request, ) diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index 2fe5027de..90e12b973 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -49,7 +49,7 @@ async def test_should_raise_exception_group_on_failed_cleanup(self): client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) try: - await client.create_session(PermissionHandler.approve_all) + await client.create_session(on_permission_request=PermissionHandler.approve_all) # Kill the server process to force cleanup to fail process = client._process @@ -72,7 +72,7 @@ async def test_should_raise_exception_group_on_failed_cleanup(self): async def test_should_force_stop_without_cleanup(self): client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) - await client.create_session(PermissionHandler.approve_all) + await client.create_session(on_permission_request=PermissionHandler.approve_all) await client.force_stop() assert client.get_state() == "disconnected" @@ -209,7 +209,7 @@ async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): # Verify subsequent calls also fail (don't hang) with pytest.raises(Exception) as exc_info2: - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) await session.send("test") # Error message varies by platform (EINVAL on Windows, EPIPE on Linux) error_msg = str(exc_info2.value).lower() diff --git a/python/e2e/test_hooks.py b/python/e2e/test_hooks.py index d02d243b9..2858d40f2 100644 --- a/python/e2e/test_hooks.py +++ b/python/e2e/test_hooks.py @@ -24,7 +24,7 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "allow"} session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, hooks={"on_pre_tool_use": on_pre_tool_use}, ) @@ -53,7 +53,7 @@ async def on_post_tool_use(input_data, invocation): return None session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, hooks={"on_post_tool_use": on_post_tool_use}, ) @@ -87,7 +87,7 @@ async def on_post_tool_use(input_data, invocation): return None session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, hooks={ "on_pre_tool_use": on_pre_tool_use, "on_post_tool_use": on_post_tool_use, @@ -122,7 +122,7 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "deny"} session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, hooks={"on_pre_tool_use": on_pre_tool_use}, ) diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index e2dac00e8..846db36a3 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -33,7 +33,7 @@ async def test_should_accept_mcp_server_configuration_on_session_create( } session = await ctx.client.create_session( - PermissionHandler.approve_all, mcp_servers=mcp_servers + on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers ) assert session.session_id is not None @@ -50,7 +50,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( ): """Test that MCP server configuration is accepted on session resume""" # Create a session first - session1 = await ctx.client.create_session(PermissionHandler.approve_all) + session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -66,7 +66,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( session2 = await ctx.client.resume_session( session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers, ) @@ -94,7 +94,7 @@ async def test_should_pass_literal_env_values_to_mcp_server_subprocess( } session = await ctx.client.create_session( - PermissionHandler.approve_all, mcp_servers=mcp_servers + on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers ) assert session.session_id is not None @@ -125,7 +125,7 @@ async def test_should_accept_custom_agent_configuration_on_session_create( ] session = await ctx.client.create_session( - PermissionHandler.approve_all, custom_agents=custom_agents + on_permission_request=PermissionHandler.approve_all, custom_agents=custom_agents ) assert session.session_id is not None @@ -142,7 +142,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( ): """Test that custom agent configuration is accepted on session resume""" # Create a session first - session1 = await ctx.client.create_session(PermissionHandler.approve_all) + session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -158,7 +158,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( session2 = await ctx.client.resume_session( session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, custom_agents=custom_agents, ) @@ -193,7 +193,7 @@ async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETe ] session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers, custom_agents=custom_agents, ) diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index 5a155abe2..8ab5dac5d 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -67,7 +67,7 @@ async def setup(self): ) # Trigger connection by creating and disconnecting an init session - init_session = await self._client1.create_session(PermissionHandler.approve_all) + init_session = await self._client1.create_session(on_permission_request=PermissionHandler.approve_all) await init_session.disconnect() # Read the actual port from client 1 and create client 2 @@ -197,12 +197,12 @@ def magic_number(params: SeedParams, invocation: ToolInvocation) -> str: # Client 1 creates a session with a custom tool session1 = await mctx.client1.create_session( - PermissionHandler.approve_all, tools=[magic_number] + on_permission_request=PermissionHandler.approve_all, tools=[magic_number] ) # Client 2 resumes with NO tools — should not overwrite client 1's tools session2 = await mctx.client2.resume_session( - session1.session_id, PermissionHandler.approve_all + session1.session_id, on_permission_request=PermissionHandler.approve_all ) client1_events = [] client2_events = [] @@ -236,7 +236,7 @@ async def test_one_client_approves_permission_and_both_see_the_result( # Client 1 creates a session and manually approves permission requests session1 = await mctx.client1.create_session( - lambda request, invocation: ( + on_permission_request=lambda request, invocation: ( permission_requests.append(request) or PermissionRequestResult(kind="approved") ), ) @@ -244,7 +244,7 @@ async def test_one_client_approves_permission_and_both_see_the_result( # Client 2 resumes — its handler never resolves, so only client 1's approval takes effect session2 = await mctx.client2.resume_session( session1.session_id, - lambda request, invocation: asyncio.Future(), + on_permission_request=lambda request, invocation: asyncio.Future(), ) client1_events = [] @@ -282,7 +282,7 @@ async def test_one_client_rejects_permission_and_both_see_the_result( """One client rejects a permission request and both see the result.""" # Client 1 creates a session and denies all permission requests session1 = await mctx.client1.create_session( - lambda request, invocation: PermissionRequestResult( + on_permission_request=lambda request, invocation: PermissionRequestResult( kind="denied-interactively-by-user" ), ) @@ -290,7 +290,7 @@ async def test_one_client_rejects_permission_and_both_see_the_result( # Client 2 resumes — its handler never resolves session2 = await mctx.client2.resume_session( session1.session_id, - lambda request, invocation: asyncio.Future(), + on_permission_request=lambda request, invocation: asyncio.Future(), ) client1_events = [] @@ -347,13 +347,13 @@ def currency_lookup(params: CountryCodeParams, invocation: ToolInvocation) -> st # Client 1 creates a session with tool A session1 = await mctx.client1.create_session( - PermissionHandler.approve_all, tools=[city_lookup] + on_permission_request=PermissionHandler.approve_all, tools=[city_lookup] ) # Client 2 resumes with tool B (different tool, union should have both) session2 = await mctx.client2.resume_session( session1.session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, tools=[currency_lookup], ) @@ -395,13 +395,13 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: # Client 1 creates a session with stable_tool session1 = await mctx.client1.create_session( - PermissionHandler.approve_all, tools=[stable_tool] + on_permission_request=PermissionHandler.approve_all, tools=[stable_tool] ) # Client 2 resumes with ephemeral_tool await mctx.client2.resume_session( session1.session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, tools=[ephemeral_tool], ) diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index e198fe5e9..5b9df8a45 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -26,7 +26,7 @@ def on_permission_request( assert invocation["session_id"] == session.session_id return PermissionRequestResult(kind="approved") - session = await ctx.client.create_session(on_permission_request) + session = await ctx.client.create_session(on_permission_request=on_permission_request) write_file(ctx.work_dir, "test.txt", "original content") @@ -49,7 +49,7 @@ def on_permission_request( ) -> PermissionRequestResult: return PermissionRequestResult(kind="denied-interactively-by-user") - session = await ctx.client.create_session(on_permission_request) + session = await ctx.client.create_session(on_permission_request=on_permission_request) original_content = "protected content" write_file(ctx.work_dir, "protected.txt", original_content) @@ -70,7 +70,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies( def deny_all(request, invocation): return PermissionRequestResult() - session = await ctx.client.create_session(deny_all) + session = await ctx.client.create_session(on_permission_request=deny_all) denied_events = [] done_event = asyncio.Event() @@ -101,14 +101,14 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ self, ctx: E2ETestContext ): """Test that tool operations are denied after resume when handler explicitly denies""" - session1 = await ctx.client.create_session(PermissionHandler.approve_all) + session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") def deny_all(request, invocation): return PermissionRequestResult() - session2 = await ctx.client.resume_session(session_id, deny_all) + session2 = await ctx.client.resume_session(session_id, on_permission_request=deny_all) denied_events = [] done_event = asyncio.Event() @@ -137,7 +137,7 @@ def on_event(event): async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETestContext): """Test that sessions work with approve-all permission handler""" - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) message = await session.send_and_wait("What is 2+2?") @@ -158,7 +158,7 @@ async def on_permission_request( await asyncio.sleep(0.01) return PermissionRequestResult(kind="approved") - session = await ctx.client.create_session(on_permission_request) + session = await ctx.client.create_session(on_permission_request=on_permission_request) await session.send_and_wait("Run 'echo test' and tell me what happens") @@ -171,7 +171,7 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC permission_requests = [] # Create initial session - session1 = await ctx.client.create_session(PermissionHandler.approve_all) + session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -182,7 +182,7 @@ def on_permission_request( permission_requests.append(request) return PermissionRequestResult(kind="approved") - session2 = await ctx.client.resume_session(session_id, on_permission_request) + session2 = await ctx.client.resume_session(session_id, on_permission_request=on_permission_request) await session2.send_and_wait("Run 'echo resumed' for me") @@ -199,7 +199,7 @@ def on_permission_request( ) -> PermissionRequestResult: raise RuntimeError("Handler error") - session = await ctx.client.create_session(on_permission_request) + session = await ctx.client.create_session(on_permission_request=on_permission_request) message = await session.send_and_wait("Run 'echo test'. If you can't, say 'failed'.") @@ -224,7 +224,7 @@ def on_permission_request( assert len(request.tool_call_id) > 0 return PermissionRequestResult(kind="approved") - session = await ctx.client.create_session(on_permission_request) + session = await ctx.client.create_session(on_permission_request=on_permission_request) await session.send_and_wait("Run 'echo test'") diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index 193a4e656..682ed701e 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -78,7 +78,7 @@ class TestSessionRpc: async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): """Test calling session.rpc.model.getCurrent""" session = await ctx.client.create_session( - PermissionHandler.approve_all, "claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" ) result = await session.rpc.model.get_current() @@ -92,7 +92,7 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext from copilot.generated.rpc import SessionModelSwitchToParams session = await ctx.client.create_session( - PermissionHandler.approve_all, "claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" ) # Get initial model @@ -118,7 +118,7 @@ async def test_get_and_set_session_mode(self): try: await client.start() - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) # Get initial mode (default should be interactive) initial = await session.rpc.mode.get() @@ -152,7 +152,7 @@ async def test_read_update_and_delete_plan(self): try: await client.start() - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) # Initially plan should not exist initial = await session.rpc.plan.read() @@ -193,7 +193,7 @@ async def test_create_list_and_read_workspace_files(self): try: await client.start() - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) # Initially no files initial_files = await session.rpc.workspace.list_files() diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index fb49f60b1..77bb2bbb9 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -14,7 +14,7 @@ class TestSessions: async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): - session = await ctx.client.create_session(PermissionHandler.approve_all, "fake-test-model") + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, model="fake-test-model") assert session.session_id messages = await session.get_messages() @@ -29,7 +29,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): await session.get_messages() async def test_should_have_stateful_conversation(self, ctx: E2ETestContext): - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) assistant_message = await session.send_and_wait("What is 1+1?") assert assistant_message is not None @@ -44,7 +44,7 @@ async def test_should_create_a_session_with_appended_systemMessage_config( ): system_message_suffix = "End each response with the phrase 'Have a nice day!'" session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, system_message={"mode": "append", "content": system_message_suffix}, ) @@ -64,7 +64,7 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( ): test_system_message = "You are an assistant called Testy McTestface. Reply succinctly." session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, system_message={"mode": "replace", "content": test_system_message}, ) @@ -80,7 +80,7 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestContext): session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, available_tools=["view", "edit"], ) @@ -97,7 +97,7 @@ async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestCon async def test_should_create_a_session_with_excludedTools(self, ctx: E2ETestContext): session = await ctx.client.create_session( - PermissionHandler.approve_all, excluded_tools=["view"] + on_permission_request=PermissionHandler.approve_all, excluded_tools=["view"] ) await session.send("What is 1+1?") @@ -120,9 +120,9 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont import asyncio s1, s2, s3 = await asyncio.gather( - ctx.client.create_session(PermissionHandler.approve_all), - ctx.client.create_session(PermissionHandler.approve_all), - ctx.client.create_session(PermissionHandler.approve_all), + ctx.client.create_session(on_permission_request=PermissionHandler.approve_all), + ctx.client.create_session(on_permission_request=PermissionHandler.approve_all), + ctx.client.create_session(on_permission_request=PermissionHandler.approve_all), ) # All sessions should have unique IDs @@ -144,14 +144,14 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestContext): # Create initial session - session1 = await ctx.client.create_session(PermissionHandler.approve_all) + session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) session_id = session1.session_id answer = await session1.send_and_wait("What is 1+1?") assert answer is not None assert "2" in answer.data.content # Resume using the same client - session2 = await ctx.client.resume_session(session_id, PermissionHandler.approve_all) + session2 = await ctx.client.resume_session(session_id, on_permission_request=PermissionHandler.approve_all) assert session2.session_id == session_id answer2 = await get_final_assistant_message(session2) assert "2" in answer2.data.content @@ -163,7 +163,7 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestContext): # Create initial session - session1 = await ctx.client.create_session(PermissionHandler.approve_all) + session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) session_id = session1.session_id answer = await session1.send_and_wait("What is 1+1?") assert answer is not None @@ -183,7 +183,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont ) try: - session2 = await new_client.resume_session(session_id, PermissionHandler.approve_all) + session2 = await new_client.resume_session(session_id, on_permission_request=PermissionHandler.approve_all) assert session2.session_id == session_id messages = await session2.get_messages() @@ -201,16 +201,16 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont async def test_should_throw_error_resuming_nonexistent_session(self, ctx: E2ETestContext): with pytest.raises(Exception): await ctx.client.resume_session( - "non-existent-session-id", PermissionHandler.approve_all + "non-existent-session-id", on_permission_request=PermissionHandler.approve_all ) async def test_should_list_sessions(self, ctx: E2ETestContext): import asyncio # Create a couple of sessions and send messages to persist them - session1 = await ctx.client.create_session(PermissionHandler.approve_all) + session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) await session1.send_and_wait("Say hello") - session2 = await ctx.client.create_session(PermissionHandler.approve_all) + session2 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) await session2.send_and_wait("Say goodbye") # Small delay to ensure session files are written to disk @@ -247,7 +247,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): import asyncio # Create a session and send a message to persist it - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) await session.send_and_wait("Hello") session_id = session.session_id @@ -269,13 +269,13 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Verify we cannot resume the deleted session with pytest.raises(Exception): - await ctx.client.resume_session(session_id, PermissionHandler.approve_all) + await ctx.client.resume_session(session_id, on_permission_request=PermissionHandler.approve_all) async def test_should_get_last_session_id(self, ctx: E2ETestContext): import asyncio # Create a session and send a message to persist it - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) await session.send_and_wait("Say hello") # Small delay to ensure session data is flushed to disk @@ -296,7 +296,7 @@ def get_secret_number_handler(invocation): ) session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, tools=[ Tool( name="get_secret_number", @@ -317,7 +317,7 @@ def get_secret_number_handler(invocation): async def test_should_create_session_with_custom_provider(self, ctx: E2ETestContext): session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, provider={ "type": "openai", "base_url": "https://api.openai.com/v1", @@ -328,7 +328,7 @@ async def test_should_create_session_with_custom_provider(self, ctx: E2ETestCont async def test_should_create_session_with_azure_provider(self, ctx: E2ETestContext): session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, provider={ "type": "azure", "base_url": "https://my-resource.openai.azure.com", @@ -341,13 +341,13 @@ async def test_should_create_session_with_azure_provider(self, ctx: E2ETestConte assert session.session_id async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestContext): - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) session_id = session.session_id # Resume the session with a provider session2 = await ctx.client.resume_session( session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, provider={ "type": "openai", "base_url": "https://api.openai.com/v1", @@ -360,7 +360,7 @@ async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestCont async def test_should_abort_a_session(self, ctx: E2ETestContext): import asyncio - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) # Set up event listeners BEFORE sending to avoid race conditions wait_for_tool_start = asyncio.create_task( @@ -408,7 +408,7 @@ def capture_early(event): early_events.append(event) session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, on_event=capture_early, ) @@ -449,7 +449,7 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo custom_config_dir = os.path.join(ctx.home_dir, "custom-config") session = await ctx.client.create_session( - PermissionHandler.approve_all, config_dir=custom_config_dir + on_permission_request=PermissionHandler.approve_all, config_dir=custom_config_dir ) assert session.session_id @@ -462,7 +462,7 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo async def test_session_log_emits_events_at_all_levels(self, ctx: E2ETestContext): import asyncio - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) received_events = [] @@ -504,7 +504,7 @@ async def test_should_set_model_with_reasoning_effort(self, ctx: E2ETestContext) """Test that setModel passes reasoningEffort and it appears in the model_change event.""" import asyncio - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) model_change_event = asyncio.get_event_loop().create_future() diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index b148601c8..395812e69 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -56,7 +56,7 @@ async def test_should_load_and_apply_skill_from_skilldirectories(self, ctx: E2ET """Test that skills are loaded and applied from skillDirectories""" skills_dir = create_skill_dir(ctx.work_dir) session = await ctx.client.create_session( - PermissionHandler.approve_all, skill_directories=[skills_dir] + on_permission_request=PermissionHandler.approve_all, skill_directories=[skills_dir] ) assert session.session_id is not None @@ -74,7 +74,7 @@ async def test_should_not_apply_skill_when_disabled_via_disabledskills( """Test that disabledSkills prevents skill from being applied""" skills_dir = create_skill_dir(ctx.work_dir) session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, skill_directories=[skills_dir], disabled_skills=["test-skill"], ) @@ -99,7 +99,7 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( skills_dir = create_skill_dir(ctx.work_dir) # Create a session without skills first - session1 = await ctx.client.create_session(PermissionHandler.approve_all) + session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) session_id = session1.session_id # First message without skill - marker should not appear @@ -110,7 +110,7 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( # Resume with skillDirectories - skill should now be active session2 = await ctx.client.resume_session( session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, skill_directories=[skills_dir], ) diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index 93015c56a..a43a66e39 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -13,7 +13,7 @@ class TestStreamingFidelity: async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: E2ETestContext): - session = await ctx.client.create_session(PermissionHandler.approve_all, streaming=True) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, streaming=True) events = [] session.on(lambda event: events.append(event)) @@ -43,7 +43,7 @@ async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: await session.disconnect() async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E2ETestContext): - session = await ctx.client.create_session(PermissionHandler.approve_all, streaming=False) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, streaming=False) events = [] session.on(lambda event: events.append(event)) @@ -62,7 +62,7 @@ async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E await session.disconnect() async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestContext): - session = await ctx.client.create_session(PermissionHandler.approve_all, streaming=False) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, streaming=False) await session.send_and_wait("What is 3 + 6?") await session.disconnect() @@ -82,7 +82,7 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont try: session2 = await new_client.resume_session( session.session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, streaming=True, ) events = [] diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index 7e5cad3dd..02fca83fa 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -23,7 +23,7 @@ async def test_invokes_built_in_tools(self, ctx: E2ETestContext): with open(readme_path, "w") as f: f.write("# ELIZA, the only chatbot you'll ever need") - session = await ctx.client.create_session(PermissionHandler.approve_all) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) await session.send("What's the first line of README.md in this directory?") assistant_message = await get_final_assistant_message(session) @@ -38,7 +38,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: return params.input.upper() session = await ctx.client.create_session( - PermissionHandler.approve_all, tools=[encrypt_string] + on_permission_request=PermissionHandler.approve_all, tools=[encrypt_string] ) await session.send("Use encrypt_string to encrypt this string: Hello") @@ -51,7 +51,7 @@ def get_user_location() -> str: raise Exception("Melbourne") session = await ctx.client.create_session( - PermissionHandler.approve_all, tools=[get_user_location] + on_permission_request=PermissionHandler.approve_all, tools=[get_user_location] ) await session.send("What is my location? If you can't find out, just say 'unknown'.") @@ -113,7 +113,7 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]: City(countryId=12, cityName="San Lorenzo", population=204356), ] - session = await ctx.client.create_session(PermissionHandler.approve_all, tools=[db_query]) + session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, tools=[db_query]) expected_session_id = session.session_id await session.send( @@ -149,7 +149,7 @@ def tracking_handler(request, invocation): did_run_permission_request = True return PermissionRequestResult(kind="no-result") - session = await ctx.client.create_session(tracking_handler, tools=[safe_lookup]) + session = await ctx.client.create_session(on_permission_request=tracking_handler, tools=[safe_lookup]) await session.send("Use safe_lookup to look up 'test123'") assistant_message = await get_final_assistant_message(session) @@ -169,7 +169,7 @@ def custom_grep(params: GrepParams, invocation: ToolInvocation) -> str: return f"CUSTOM_GREP_RESULT: {params.query}" session = await ctx.client.create_session( - PermissionHandler.approve_all, tools=[custom_grep] + on_permission_request=PermissionHandler.approve_all, tools=[custom_grep] ) await session.send("Use grep to search for the word 'hello'") @@ -190,7 +190,7 @@ def on_permission_request(request, invocation): permission_requests.append(request) return PermissionRequestResult(kind="approved") - session = await ctx.client.create_session(on_permission_request, tools=[encrypt_string]) + session = await ctx.client.create_session(on_permission_request=on_permission_request, tools=[encrypt_string]) await session.send("Use encrypt_string to encrypt this string: Hello") assistant_message = await get_final_assistant_message(session) @@ -216,7 +216,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: def on_permission_request(request, invocation): return PermissionRequestResult(kind="denied-interactively-by-user") - session = await ctx.client.create_session(on_permission_request, tools=[encrypt_string]) + session = await ctx.client.create_session(on_permission_request=on_permission_request, tools=[encrypt_string]) await session.send("Use encrypt_string to encrypt this string: Hello") await get_final_assistant_message(session) diff --git a/python/samples/chat.py b/python/samples/chat.py index 39945a527..ee94c21fe 100644 --- a/python/samples/chat.py +++ b/python/samples/chat.py @@ -9,7 +9,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) def on_event(event): output = None diff --git a/python/test_client.py b/python/test_client.py index 2841f4e50..f1c9cb160 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -35,7 +35,7 @@ async def test_create_session_raises_with_none_permission_handler(self): await client.start() try: with pytest.raises(ValueError, match="on_permission_request handler is required"): - await client.create_session(None) # type: ignore[arg-type] + await client.create_session(on_permission_request=None) # type: ignore[arg-type] finally: await client.force_stop() @@ -45,7 +45,7 @@ async def test_v2_permission_adapter_rejects_no_result(self): await client.start() try: session = await client.create_session( - lambda request, invocation: PermissionRequestResult(kind="no-result") + on_permission_request=lambda request, invocation: PermissionRequestResult(kind="no-result") ) with pytest.raises(ValueError, match="protocol v2 server"): await client._handle_permission_request_v2( @@ -62,9 +62,9 @@ async def test_resume_session_raises_without_permission_handler(self): client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) with pytest.raises(ValueError, match="on_permission_request.*is required"): - await client.resume_session(session.session_id, None) + await client.resume_session(session.session_id, on_permission_request=None) finally: await client.force_stop() @@ -187,7 +187,7 @@ async def mock_request(method, params): def grep(params) -> str: return "ok" - await client.create_session(PermissionHandler.approve_all, tools=[grep]) + await client.create_session(on_permission_request=PermissionHandler.approve_all, tools=[grep]) tool_defs = captured["session.create"]["tools"] assert len(tool_defs) == 1 assert tool_defs[0]["name"] == "grep" @@ -201,7 +201,7 @@ async def test_resume_session_sends_overrides_built_in_tool(self): await client.start() try: - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) captured = {} original_request = client._client.request @@ -218,7 +218,7 @@ def grep(params) -> str: await client.resume_session( session.session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, tools=[grep], ) tool_defs = captured["session.resume"]["tools"] @@ -365,7 +365,7 @@ async def mock_request(method, params): return await original_request(method, params) client._client.request = mock_request - await client.create_session(PermissionHandler.approve_all, client_name="my-app") + await client.create_session(on_permission_request=PermissionHandler.approve_all, client_name="my-app") assert captured["session.create"]["clientName"] == "my-app" finally: await client.force_stop() @@ -376,7 +376,7 @@ async def test_resume_session_forwards_client_name(self): await client.start() try: - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) captured = {} original_request = client._client.request @@ -391,7 +391,7 @@ async def mock_request(method, params): client._client.request = mock_request await client.resume_session( session.session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, client_name="my-app", ) assert captured["session.resume"]["clientName"] == "my-app" @@ -413,7 +413,7 @@ async def mock_request(method, params): client._client.request = mock_request await client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, agent="test-agent", custom_agents=[{"name": "test-agent", "prompt": "You are a test agent."}], ) @@ -427,7 +427,7 @@ async def test_resume_session_forwards_agent(self): await client.start() try: - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) captured = {} original_request = client._client.request @@ -441,7 +441,7 @@ async def mock_request(method, params): client._client.request = mock_request await client.resume_session( session.session_id, - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, agent="test-agent", custom_agents=[{"name": "test-agent", "prompt": "You are a test agent."}], ) @@ -455,7 +455,7 @@ async def test_set_model_sends_correct_rpc(self): await client.start() try: - session = await client.create_session(PermissionHandler.approve_all) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) captured = {} original_request = client._client.request diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index 40f3ada34..995002070 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -19,8 +19,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - ANTHROPIC_MODEL, + on_permission_request=PermissionHandler.approve_all, + model=ANTHROPIC_MODEL, provider={ "type": "anthropic", "base_url": ANTHROPIC_BASE_URL, diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 875f7a6b6..57a49f2a5 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -20,8 +20,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - AZURE_OPENAI_MODEL, + on_permission_request=PermissionHandler.approve_all, + model=AZURE_OPENAI_MODEL, provider={ "type": "azure", "base_url": AZURE_OPENAI_ENDPOINT, diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index 8c6d138ae..87dad5866 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -18,8 +18,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - OLLAMA_MODEL, + on_permission_request=PermissionHandler.approve_all, + model=OLLAMA_MODEL, provider={ "type": "openai", "base_url": OLLAMA_BASE_URL, diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index e4b7aa0c8..fadd1c79d 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -19,8 +19,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - OPENAI_MODEL, + on_permission_request=PermissionHandler.approve_all, + model=OPENAI_MODEL, provider={ "type": "openai", "base_url": OPENAI_BASE_URL, diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index a8ba8e569..e7f640ae9 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -84,7 +84,7 @@ async def main(): )) try: - session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) 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 ba1c1d0a3..c9ab35bce 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -16,7 +16,7 @@ async def ask_copilot(prompt: str) -> str: client = CopilotClient(ExternalServerConfig(url=CLI_URL)) try: - session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait(prompt) diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index ee92e0c87..07eb74e20 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -9,7 +9,7 @@ async def main(): )) try: - session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index ee92e0c87..07eb74e20 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -9,7 +9,7 @@ async def main(): )) try: - session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index d0bcf1188..382f9c4f9 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -10,7 +10,7 @@ async def main(): )) try: - session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index b40366a6c..bc9782b6b 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -47,8 +47,8 @@ async def main(): try: session = await client.create_session( - auto_approve_permission, - "claude-haiku-4.5", + on_permission_request=auto_approve_permission, + model="claude-haiku-4.5", hooks={ "on_session_start": on_session_start, "on_session_end": on_session_end, diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 2c6d69de6..e4de98a9a 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -23,8 +23,8 @@ async def main(): try: session = await client.create_session( - log_permission, - "claude-haiku-4.5", + on_permission_request=log_permission, + model="claude-haiku-4.5", hooks={"on_pre_tool_use": auto_approve_tool}, ) diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index 9d0991abf..92981861d 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -27,8 +27,8 @@ async def main(): try: session = await client.create_session( - auto_approve_permission, - "claude-haiku-4.5", + on_permission_request=auto_approve_permission, + model="claude-haiku-4.5", on_user_input_request=handle_user_input, hooks={"on_pre_tool_use": auto_approve_tool}, ) diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index 272ba4bfb..55f1cb394 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -10,7 +10,7 @@ async def main(): )) try: - session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") 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: diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 850ab7f0f..22f321b22 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -11,8 +11,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", available_tools=[], system_message={ "mode": "replace", diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index c5c9bfad4..37654e269 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -13,8 +13,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", system_message={"mode": "replace", "content": SYSTEM_PROMPT}, available_tools=[], ) diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index 363e370ac..8baed649d 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -11,8 +11,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - "claude-opus-4.6", + on_permission_request=PermissionHandler.approve_all, + model="claude-opus-4.6", reasoning_effort="low", available_tools=[], system_message={ diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index d9ef68c45..15d354258 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -13,8 +13,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", system_message={"mode": "replace", "content": PIRATE_PROMPT}, available_tools=[], ) diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index ea2411e2d..5c3994c4c 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -15,14 +15,14 @@ async def main(): try: session1, session2 = await asyncio.gather( client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", system_message={"mode": "replace", "content": PIRATE_PROMPT}, available_tools=[], ), client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", system_message={"mode": "replace", "content": ROBOT_PROMPT}, available_tools=[], ), diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 77436e51b..30aa40cd1 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -11,8 +11,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", available_tools=[], system_message={ "mode": "replace", diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index d8691d173..049ca1f83 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -12,8 +12,8 @@ async def main(): try: # 1. Create a session session = await client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", available_tools=[], ) @@ -26,7 +26,7 @@ async def main(): session_id = session.session_id # 4. Resume the session with the same ID - resumed = await client.resume_session(session_id, PermissionHandler.approve_all) + resumed = await client.resume_session(session_id, on_permission_request=PermissionHandler.approve_all) print("Session resumed") # 5. Ask for the secret word diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index ee4ab57da..20fd4902e 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -11,8 +11,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", streaming=True, ) diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index b349913f6..c30107a2f 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -11,8 +11,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", custom_agents=[ { "name": "researcher", diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index 8683033c0..9edd04115 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -33,7 +33,7 @@ async def main(): session_kwargs["mcp_servers"] = mcp_servers session = await client.create_session( - PermissionHandler.approve_all, "claude-haiku-4.5", **session_kwargs + on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5", **session_kwargs ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index 664b5d760..c9a8047ec 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -16,8 +16,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", system_message={"mode": "replace", "content": SYSTEM_PROMPT}, available_tools=[], ) diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 6a7a39f75..afa871d83 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -15,8 +15,8 @@ async def main(): skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") session = await client.create_session( - lambda _, __: {"kind": "approved"}, - "claude-haiku-4.5", + on_permission_request=lambda _, __: {"kind": "approved"}, + model="claude-haiku-4.5", skill_directories=[skills_dir], hooks={ "on_pre_tool_use": lambda _, __: {"permissionDecision": "allow"}, diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 2817dae05..668bca197 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -13,8 +13,8 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, - "claude-haiku-4.5", + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", system_message={"mode": "replace", "content": SYSTEM_PROMPT}, available_tools=["grep", "glob", "view"], ) diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 1e9947b64..73c539fe1 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -23,7 +23,7 @@ async def main(): try: session = await client.create_session( - PermissionHandler.approve_all, "claude-haiku-4.5", tools=[custom_grep] + on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5", tools=[custom_grep] ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index 2bb7c70ab..92f2593a6 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -53,8 +53,8 @@ async def main(): try: session = await client.create_session( - auto_approve_permission, - "claude-haiku-4.5", + on_permission_request=auto_approve_permission, + model="claude-haiku-4.5", available_tools=[], tools=[create_file, read_file, list_files], hooks={"on_pre_tool_use": auto_approve_tool}, diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index 64d309f16..d1b8a5696 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -12,7 +12,7 @@ async def main(): try: # First session print("--- Session 1 ---") - session1 = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session1 = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response1 = await session1.send_and_wait( "What is the capital of France?" @@ -29,7 +29,7 @@ async def main(): # Second session — tests that the server accepts new sessions print("--- Session 2 ---") - session2 = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session2 = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response2 = await session2.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index d0bcf1188..382f9c4f9 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -10,7 +10,7 @@ async def main(): )) try: - session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index ee92e0c87..07eb74e20 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -9,7 +9,7 @@ async def main(): )) try: - session = await client.create_session(PermissionHandler.approve_all, "claude-haiku-4.5") + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" From a71f8b0ecd622de59cc31e40aec0da14c3365934 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 16 Mar 2026 12:13:19 -0700 Subject: [PATCH 09/11] Fix Python E2E tests for keyword-only create_session parameters - Restore accidentally deleted custom_agents dict entries in test_agent_and_compact_rpc.py - Convert positional on_permission_request to keyword arg in test_compaction.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/e2e/test_agent_and_compact_rpc.py | 8 ++++++++ python/e2e/test_compaction.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index 0a3e0ea71..146b57484 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -21,6 +21,14 @@ async def test_should_list_available_custom_agents(self): session = await client.create_session( on_permission_request=PermissionHandler.approve_all, custom_agents=[ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + }, + { + "name": "another-agent", "display_name": "Another Agent", "description": "Another test agent", "prompt": "You are another agent.", diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index d42cbd292..beb51e74b 100644 --- a/python/e2e/test_compaction.py +++ b/python/e2e/test_compaction.py @@ -17,7 +17,7 @@ async def test_should_trigger_compaction_with_low_threshold_and_emit_events( ): # Create session with very low compaction thresholds to trigger compaction quickly session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, infinite_sessions={ "enabled": True, # Trigger background compaction at 0.5% context usage (~1000 tokens) @@ -68,7 +68,7 @@ async def test_should_not_emit_compaction_events_when_infinite_sessions_disabled self, ctx: E2ETestContext ): session = await ctx.client.create_session( - PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, infinite_sessions={"enabled": False}, ) From b6c8fd690f4861b327482cc68e3833762382134e Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 16 Mar 2026 12:29:30 -0700 Subject: [PATCH 10/11] Fix formatting --- python/e2e/test_agent_and_compact_rpc.py | 8 +++- python/e2e/test_client.py | 4 +- python/e2e/test_mcp_and_agents.py | 8 +++- python/e2e/test_multi_client.py | 4 +- python/e2e/test_permissions.py | 16 +++++-- python/e2e/test_rpc.py | 12 +++-- python/e2e/test_session.py | 60 ++++++++++++++++++------ python/e2e/test_skills.py | 4 +- python/e2e/test_streaming_fidelity.py | 12 +++-- python/e2e/test_tools.py | 20 ++++++-- python/test_client.py | 32 +++++++++---- 11 files changed, 135 insertions(+), 45 deletions(-) diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index 146b57484..63d3e7322 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -152,7 +152,9 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): try: await client.start() - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) result = await session.rpc.agent.list() assert result.agents == [] @@ -167,7 +169,9 @@ class TestSessionCompactionRpc: @pytest.mark.asyncio async def test_should_compact_session_history_after_messages(self, ctx: E2ETestContext): """Test compacting session history via RPC.""" - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) # Send a message to create some history await session.send_and_wait("What is 2+2?") diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index 90e12b973..d266991f7 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -209,7 +209,9 @@ async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): # Verify subsequent calls also fail (don't hang) with pytest.raises(Exception) as exc_info2: - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) await session.send("test") # Error message varies by platform (EINVAL on Windows, EPIPE on Linux) error_msg = str(exc_info2.value).lower() diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index 846db36a3..c4bd89414 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -50,7 +50,9 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( ): """Test that MCP server configuration is accepted on session resume""" # Create a session first - session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session1 = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -142,7 +144,9 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( ): """Test that custom agent configuration is accepted on session resume""" # Create a session first - session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session1 = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index 8ab5dac5d..c77ae86e1 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -67,7 +67,9 @@ async def setup(self): ) # Trigger connection by creating and disconnecting an init session - init_session = await self._client1.create_session(on_permission_request=PermissionHandler.approve_all) + init_session = await self._client1.create_session( + on_permission_request=PermissionHandler.approve_all + ) await init_session.disconnect() # Read the actual port from client 1 and create client 2 diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index 5b9df8a45..a673d63b5 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -101,7 +101,9 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ self, ctx: E2ETestContext ): """Test that tool operations are denied after resume when handler explicitly denies""" - session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session1 = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -137,7 +139,9 @@ def on_event(event): async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETestContext): """Test that sessions work with approve-all permission handler""" - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) message = await session.send_and_wait("What is 2+2?") @@ -171,7 +175,9 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC permission_requests = [] # Create initial session - session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session1 = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -182,7 +188,9 @@ def on_permission_request( permission_requests.append(request) return PermissionRequestResult(kind="approved") - session2 = await ctx.client.resume_session(session_id, on_permission_request=on_permission_request) + session2 = await ctx.client.resume_session( + session_id, on_permission_request=on_permission_request + ) await session2.send_and_wait("Run 'echo resumed' for me") diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index 682ed701e..814da067d 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -118,7 +118,9 @@ async def test_get_and_set_session_mode(self): try: await client.start() - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) # Get initial mode (default should be interactive) initial = await session.rpc.mode.get() @@ -152,7 +154,9 @@ async def test_read_update_and_delete_plan(self): try: await client.start() - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) # Initially plan should not exist initial = await session.rpc.plan.read() @@ -193,7 +197,9 @@ async def test_create_list_and_read_workspace_files(self): try: await client.start() - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) # Initially no files initial_files = await session.rpc.workspace.list_files() diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 77bb2bbb9..62fe805dc 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -14,7 +14,9 @@ class TestSessions: async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, model="fake-test-model") + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, model="fake-test-model" + ) assert session.session_id messages = await session.get_messages() @@ -29,7 +31,9 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): await session.get_messages() async def test_should_have_stateful_conversation(self, ctx: E2ETestContext): - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) assistant_message = await session.send_and_wait("What is 1+1?") assert assistant_message is not None @@ -144,14 +148,18 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestContext): # Create initial session - session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session1 = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) session_id = session1.session_id answer = await session1.send_and_wait("What is 1+1?") assert answer is not None assert "2" in answer.data.content # Resume using the same client - session2 = await ctx.client.resume_session(session_id, on_permission_request=PermissionHandler.approve_all) + session2 = await ctx.client.resume_session( + session_id, on_permission_request=PermissionHandler.approve_all + ) assert session2.session_id == session_id answer2 = await get_final_assistant_message(session2) assert "2" in answer2.data.content @@ -163,7 +171,9 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestContext): # Create initial session - session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session1 = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) session_id = session1.session_id answer = await session1.send_and_wait("What is 1+1?") assert answer is not None @@ -183,7 +193,9 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont ) try: - session2 = await new_client.resume_session(session_id, on_permission_request=PermissionHandler.approve_all) + session2 = await new_client.resume_session( + session_id, on_permission_request=PermissionHandler.approve_all + ) assert session2.session_id == session_id messages = await session2.get_messages() @@ -208,9 +220,13 @@ async def test_should_list_sessions(self, ctx: E2ETestContext): import asyncio # Create a couple of sessions and send messages to persist them - session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session1 = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) await session1.send_and_wait("Say hello") - session2 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session2 = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) await session2.send_and_wait("Say goodbye") # Small delay to ensure session files are written to disk @@ -247,7 +263,9 @@ async def test_should_delete_session(self, ctx: E2ETestContext): import asyncio # Create a session and send a message to persist it - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) await session.send_and_wait("Hello") session_id = session.session_id @@ -269,13 +287,17 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Verify we cannot resume the deleted session with pytest.raises(Exception): - await ctx.client.resume_session(session_id, on_permission_request=PermissionHandler.approve_all) + await ctx.client.resume_session( + session_id, on_permission_request=PermissionHandler.approve_all + ) async def test_should_get_last_session_id(self, ctx: E2ETestContext): import asyncio # Create a session and send a message to persist it - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) await session.send_and_wait("Say hello") # Small delay to ensure session data is flushed to disk @@ -341,7 +363,9 @@ async def test_should_create_session_with_azure_provider(self, ctx: E2ETestConte assert session.session_id async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestContext): - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) session_id = session.session_id # Resume the session with a provider @@ -360,7 +384,9 @@ async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestCont async def test_should_abort_a_session(self, ctx: E2ETestContext): import asyncio - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) # Set up event listeners BEFORE sending to avoid race conditions wait_for_tool_start = asyncio.create_task( @@ -462,7 +488,9 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo async def test_session_log_emits_events_at_all_levels(self, ctx: E2ETestContext): import asyncio - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) received_events = [] @@ -504,7 +532,9 @@ async def test_should_set_model_with_reasoning_effort(self, ctx: E2ETestContext) """Test that setModel passes reasoningEffort and it appears in the model_change event.""" import asyncio - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) model_change_event = asyncio.get_event_loop().create_future() diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index 395812e69..9b0599975 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -99,7 +99,9 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( skills_dir = create_skill_dir(ctx.work_dir) # Create a session without skills first - session1 = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session1 = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) session_id = session1.session_id # First message without skill - marker should not appear diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index a43a66e39..05e977e12 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -13,7 +13,9 @@ class TestStreamingFidelity: async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: E2ETestContext): - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, streaming=True) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, streaming=True + ) events = [] session.on(lambda event: events.append(event)) @@ -43,7 +45,9 @@ async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: await session.disconnect() async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E2ETestContext): - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, streaming=False) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, streaming=False + ) events = [] session.on(lambda event: events.append(event)) @@ -62,7 +66,9 @@ async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E await session.disconnect() async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestContext): - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, streaming=False) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, streaming=False + ) await session.send_and_wait("What is 3 + 6?") await session.disconnect() diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index 02fca83fa..458897d49 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -23,7 +23,9 @@ async def test_invokes_built_in_tools(self, ctx: E2ETestContext): with open(readme_path, "w") as f: f.write("# ELIZA, the only chatbot you'll ever need") - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all + ) await session.send("What's the first line of README.md in this directory?") assistant_message = await get_final_assistant_message(session) @@ -113,7 +115,9 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]: City(countryId=12, cityName="San Lorenzo", population=204356), ] - session = await ctx.client.create_session(on_permission_request=PermissionHandler.approve_all, tools=[db_query]) + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, tools=[db_query] + ) expected_session_id = session.session_id await session.send( @@ -149,7 +153,9 @@ def tracking_handler(request, invocation): did_run_permission_request = True return PermissionRequestResult(kind="no-result") - session = await ctx.client.create_session(on_permission_request=tracking_handler, tools=[safe_lookup]) + session = await ctx.client.create_session( + on_permission_request=tracking_handler, tools=[safe_lookup] + ) await session.send("Use safe_lookup to look up 'test123'") assistant_message = await get_final_assistant_message(session) @@ -190,7 +196,9 @@ def on_permission_request(request, invocation): permission_requests.append(request) return PermissionRequestResult(kind="approved") - session = await ctx.client.create_session(on_permission_request=on_permission_request, tools=[encrypt_string]) + session = await ctx.client.create_session( + on_permission_request=on_permission_request, tools=[encrypt_string] + ) await session.send("Use encrypt_string to encrypt this string: Hello") assistant_message = await get_final_assistant_message(session) @@ -216,7 +224,9 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: def on_permission_request(request, invocation): return PermissionRequestResult(kind="denied-interactively-by-user") - session = await ctx.client.create_session(on_permission_request=on_permission_request, tools=[encrypt_string]) + session = await ctx.client.create_session( + on_permission_request=on_permission_request, tools=[encrypt_string] + ) await session.send("Use encrypt_string to encrypt this string: Hello") await get_final_assistant_message(session) diff --git a/python/test_client.py b/python/test_client.py index f1c9cb160..9f8f38423 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -45,7 +45,9 @@ async def test_v2_permission_adapter_rejects_no_result(self): await client.start() try: session = await client.create_session( - on_permission_request=lambda request, invocation: PermissionRequestResult(kind="no-result") + on_permission_request=lambda request, invocation: PermissionRequestResult( + kind="no-result" + ) ) with pytest.raises(ValueError, match="protocol v2 server"): await client._handle_permission_request_v2( @@ -62,7 +64,9 @@ async def test_resume_session_raises_without_permission_handler(self): client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) with pytest.raises(ValueError, match="on_permission_request.*is required"): await client.resume_session(session.session_id, on_permission_request=None) finally: @@ -187,7 +191,9 @@ async def mock_request(method, params): def grep(params) -> str: return "ok" - await client.create_session(on_permission_request=PermissionHandler.approve_all, tools=[grep]) + await client.create_session( + on_permission_request=PermissionHandler.approve_all, tools=[grep] + ) tool_defs = captured["session.create"]["tools"] assert len(tool_defs) == 1 assert tool_defs[0]["name"] == "grep" @@ -201,7 +207,9 @@ async def test_resume_session_sends_overrides_built_in_tool(self): await client.start() try: - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) captured = {} original_request = client._client.request @@ -365,7 +373,9 @@ async def mock_request(method, params): return await original_request(method, params) client._client.request = mock_request - await client.create_session(on_permission_request=PermissionHandler.approve_all, client_name="my-app") + await client.create_session( + on_permission_request=PermissionHandler.approve_all, client_name="my-app" + ) assert captured["session.create"]["clientName"] == "my-app" finally: await client.force_stop() @@ -376,7 +386,9 @@ async def test_resume_session_forwards_client_name(self): await client.start() try: - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) captured = {} original_request = client._client.request @@ -427,7 +439,9 @@ async def test_resume_session_forwards_agent(self): await client.start() try: - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) captured = {} original_request = client._client.request @@ -455,7 +469,9 @@ async def test_set_model_sends_correct_rpc(self): await client.start() try: - session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) captured = {} original_request = client._client.request From d68f2a43269064e898cd720877036da12557bda6 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 16 Mar 2026 12:29:44 -0700 Subject: [PATCH 11/11] Format docstrings --- python/copilot/client.py | 18 ++++++++++-------- python/copilot/session.py | 6 +----- python/pyproject.toml | 1 + 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/python/copilot/client.py b/python/copilot/client.py index 1b9fd1ef1..bd010b633 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -8,7 +8,9 @@ >>> from copilot import CopilotClient, PermissionHandler >>> >>> async with CopilotClient() as client: - ... session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + ... session = await client.create_session( + ... on_permission_request=PermissionHandler.approve_all + ... ) ... await session.send("Hello!") """ @@ -150,10 +152,12 @@ def __init__( >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) >>> >>> # Custom CLI path with specific log level - >>> client = CopilotClient(SubprocessConfig( - ... cli_path="/usr/local/bin/copilot", - ... log_level="debug", - ... )) + >>> client = CopilotClient( + ... SubprocessConfig( + ... cli_path="/usr/local/bin/copilot", + ... log_level="debug", + ... ) + ... ) """ if config is None: config = SubprocessConfig() @@ -1064,9 +1068,7 @@ async def get_last_session_id(self) -> str | None: Example: >>> last_id = await client.get_last_session_id() >>> if last_id: - ... session = await client.resume_session( - ... last_id, PermissionHandler.approve_all - ... ) + ... session = await client.resume_session(last_id, PermissionHandler.approve_all) """ if not self._client: raise RuntimeError("Client not connected") diff --git a/python/copilot/session.py b/python/copilot/session.py index e4a17f2f9..88d18060f 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -245,9 +245,7 @@ def on(self, handler: Callable[[SessionEvent], None]) -> Callable[[], None]: ... print(f"Assistant: {event.data.content}") ... elif event.type == "session.error": ... print(f"Error: {event.data.message}") - ... >>> unsubscribe = session.on(handle_event) - ... >>> # Later, to stop receiving events: >>> unsubscribe() """ @@ -730,9 +728,7 @@ async def abort(self) -> None: >>> import asyncio >>> >>> # Start a long-running request - >>> task = asyncio.create_task( - ... session.send("Write a very long story...") - ... ) + >>> task = asyncio.create_task(session.send("Write a very long story...")) >>> >>> # Abort after 5 seconds >>> await asyncio.sleep(5) diff --git a/python/pyproject.toml b/python/pyproject.toml index ec270f97e..7c1f8bbf2 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -68,6 +68,7 @@ select = [ ] [tool.ruff.format] +docstring-code-format = true quote-style = "double" indent-style = "space"