diff --git a/docs/auth/byok.md b/docs/auth/byok.md index cb2f8cb90..4afb149e8 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -372,8 +372,8 @@ const client = new CopilotClient({ from copilot import CopilotClient from copilot.client import ModelInfo, ModelCapabilities, ModelSupports, ModelLimits -client = CopilotClient({ - "on_list_models": lambda: [ +client = CopilotClient( + on_list_models=lambda: [ ModelInfo( id="my-custom-model", name="My Custom Model", @@ -383,7 +383,7 @@ client = CopilotClient({ ), ) ], -}) +) ``` diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index 3d93f7589..716e80583 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -64,14 +64,13 @@ const session = await client.createSession({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", custom_agents=[ { diff --git a/docs/features/hooks.md b/docs/features/hooks.md index c88c6e605..6ba554a1e 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -60,14 +60,13 @@ const session = await client.createSession({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), hooks={ "on_session_start": on_session_start, "on_pre_tool_use": on_pre_tool_use, @@ -262,7 +261,7 @@ const session = await client.createSession({ Python ```python -from copilot.session import PermissionRequestResult +from copilot import PermissionDecisionApproveOnce READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"] @@ -276,7 +275,7 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "allow"} session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), hooks={"on_pre_tool_use": on_pre_tool_use}, ) ``` @@ -578,7 +577,7 @@ const session = await client.createSession({ ```python import json, aiofiles -from copilot.session import PermissionRequestResult +from copilot import PermissionDecisionApproveOnce audit_log = [] @@ -630,7 +629,7 @@ async def on_session_end(input_data, invocation): return None session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), hooks={ "on_session_start": on_session_start, "on_user_prompt_submitted": on_user_prompt_submitted, @@ -709,7 +708,7 @@ const session = await client.createSession({ ```python import subprocess -from copilot.session import PermissionRequestResult +from copilot import PermissionDecisionApproveOnce async def on_session_end(input_data, invocation): sid = invocation["session_id"][:8] @@ -728,7 +727,7 @@ async def on_error_occurred(input_data, invocation): return None session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), hooks={ "on_session_end": on_session_end, "on_error_occurred": on_error_occurred, @@ -932,7 +931,7 @@ const session = await client.createSession({ Python ```python -from copilot.session import PermissionRequestResult +from copilot import PermissionDecisionApproveOnce session_metrics = {} @@ -963,7 +962,7 @@ async def on_session_end(input_data, invocation): return None session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), 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 4aa564558..6f743f1fa 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -68,14 +68,13 @@ await session.send({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) @@ -286,14 +285,13 @@ await session.send({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) diff --git a/docs/features/remote-sessions.md b/docs/features/remote-sessions.md index 391bb762d..33556e43a 100644 --- a/docs/features/remote-sessions.md +++ b/docs/features/remote-sessions.md @@ -38,9 +38,9 @@ session.on("session.info", (event) => { ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient -client = CopilotClient(SubprocessConfig(remote=True)) +client = CopilotClient(enable_remote_sessions=True) session = await client.create_session( working_directory="/path/to/github-repo", on_permission_request=lambda req: {"allowed": True}, diff --git a/docs/features/skills.md b/docs/features/skills.md index 516c11762..05c5a97a9 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -42,15 +42,14 @@ await session.sendAndWait({ prompt: "Review this code for security issues" }); Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce async def main(): client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", skill_directories=[ "./skills/code-review", diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md index ce4f4fba2..237457585 100644 --- a/docs/features/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -69,15 +69,14 @@ await session.send({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce async def main(): client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) @@ -261,15 +260,14 @@ await session.send({ Python ```python -from copilot import CopilotClient -from copilot.session import PermissionRequestResult +from copilot import CopilotClient, PermissionDecisionApproveOnce async def main(): client = CopilotClient() await client.start() session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) @@ -502,7 +500,7 @@ await session.send({ ```python session = await client.create_session( - on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(), model="gpt-4.1", ) diff --git a/docs/getting-started.md b/docs/getting-started.md index 0d5e5887e..f451fa69c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -665,13 +665,12 @@ unsubscribeIdle(); ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionDecisionApproveOnce from copilot.generated.session_events import SessionEvent, SessionEventType -from copilot.session import PermissionRequestResult client = CopilotClient() -session = await client.create_session(on_permission_request=lambda req, inv: PermissionRequestResult(kind="approve-once")) +session = await client.create_session(on_permission_request=lambda req, inv: PermissionDecisionApproveOnce()) # Subscribe to all events unsubscribe = session.on(lambda event: print(f"Event: {event.type}")) @@ -1968,12 +1967,12 @@ const session = await client.createSession({ onPermissionRequest: approveAll }); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, CopilotClientOptions, RuntimeConnection from copilot.session import PermissionHandler -client = CopilotClient({ - "cli_url": "localhost:4321" -}) +client = CopilotClient(CopilotClientOptions( + connection=RuntimeConnection.for_uri("localhost:4321"), +)) await client.start() # Use the client normally @@ -2138,9 +2137,9 @@ Optional peer dependency: `@opentelemetry/api` ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, CopilotClientOptions -client = CopilotClient(SubprocessConfig( +client = CopilotClient(CopilotClientOptions( telemetry={ "otlp_endpoint": "http://localhost:4318", }, diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md index 42a5c6e96..1f9581ba5 100644 --- a/docs/observability/opentelemetry.md +++ b/docs/observability/opentelemetry.md @@ -27,13 +27,13 @@ const client = new CopilotClient({ ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient -client = CopilotClient(SubprocessConfig( +client = CopilotClient( telemetry={ "otlp_endpoint": "http://localhost:4318", }, -)) +) ``` diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index 0552ee36e..dfe9c19af 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -144,10 +144,12 @@ res.json({ content: response?.data.content }); Python ```python -from copilot import CopilotClient, ExternalServerConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler -client = CopilotClient(ExternalServerConfig(url="localhost:4321")) +client = CopilotClient( + connection=RuntimeConnection.for_uri("localhost:4321"), +) await client.start() session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-{int(time.time())}") diff --git a/docs/troubleshooting/debugging.md b/docs/troubleshooting/debugging.md index 3beadb99f..95c9b3a7d 100644 --- a/docs/troubleshooting/debugging.md +++ b/docs/troubleshooting/debugging.md @@ -34,7 +34,7 @@ const client = new CopilotClient({ ```python from copilot import CopilotClient -client = CopilotClient({"log_level": "debug"}) +client = CopilotClient(log_level="debug") ``` @@ -131,7 +131,7 @@ const client = new CopilotClient({ ``` > [!NOTE] -> Python SDK logging configuration is limited. For advanced logging, run the CLI manually with `--log-dir` and connect via `cli_url`. +> Python SDK logging configuration is limited. For advanced logging, run the CLI manually with `--log-dir` and connect via `RuntimeConnection.for_uri(...)`. diff --git a/python/README.md b/python/README.md index 3cee037cc..4e415e320 100644 --- a/python/README.md +++ b/python/README.md @@ -103,7 +103,7 @@ asyncio.run(main()) - ✅ Full JSON-RPC protocol support - ✅ stdio and TCP transports - ✅ Real-time streaming events -- ✅ Session history with `get_messages()` +- ✅ Session history with `get_events()` - ✅ Type hints throughout - ✅ Async/await native - ✅ Async context manager support for automatic resource cleanup @@ -113,7 +113,7 @@ asyncio.run(main()) ### CopilotClient ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient from copilot.session import PermissionHandler async with CopilotClient() as client: @@ -133,40 +133,39 @@ async with CopilotClient() as client: > **Note:** For manual lifecycle management, see [Manual Resource Management](#manual-resource-management) above. ```python -from copilot import CopilotClient, ExternalServerConfig +from copilot import CopilotClient, RuntimeConnection # Connect to an existing CLI server -client = CopilotClient(ExternalServerConfig(url="localhost:3000")) +client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:3000")) ``` **CopilotClient Constructor:** ```python -CopilotClient( - config=None, # SubprocessConfig | ExternalServerConfig | None - *, - auto_start=True, # auto-start server on first use - on_list_models=None, # custom handler for list_models() -) +CopilotClient() # spawn the bundled runtime with defaults +CopilotClient(connection=..., log_level="debug", github_token=..., ...) ``` -**SubprocessConfig** — spawn a local CLI process: +All options are kw-only parameters: -- `cli_path` (str | None): Path to CLI executable (default: `COPILOT_CLI_PATH` env var, or bundled binary) -- `cli_args` (list[str]): Extra arguments for the CLI executable -- `cwd` (str | None): Working directory for CLI process (default: current dir) -- `use_stdio` (bool): Use stdio transport instead of TCP (default: True) -- `port` (int): Server port for TCP mode (default: 0 for random) -- `log_level` (str): Log level (default: "info") -- `env` (dict | None): Environment variables for the CLI process +- `connection` (RuntimeConnection | None): How to reach the runtime. Use + `RuntimeConnection.for_stdio(...)`, `RuntimeConnection.for_tcp(...)`, or + `RuntimeConnection.for_uri(...)`. Defaults to a stdio connection with the bundled binary. +- `working_directory` (str | None): Working directory for the CLI process (default: current dir). +- `log_level` (str): Log level (default: "info"). +- `env` (dict | None): Environment variables for the CLI process. - `github_token` (str | None): GitHub token for authentication. When provided, takes priority over other auth methods. -- `copilot_home` (str | None): Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned CLI process. When `None`, the CLI defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when using `ExternalServerConfig`. +- `base_directory` (str | None): Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned CLI process. When `None`, the CLI defaults to `~/.copilot`. Useful in restricted environments where only specific directories are writable. Ignored when using a `UriRuntimeConnection`. - `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). - `telemetry` (dict | None): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. +- `enable_remote_sessions` (bool): Enable remote/cloud session support (default: False). +- `on_list_models` (callable | None): Custom handler for `list_models()`. When provided, the handler is called instead of querying the runtime. -**ExternalServerConfig** — connect to an existing CLI server: +**RuntimeConnection variants:** -- `url` (str): Server URL (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). +- `RuntimeConnection.for_stdio(path=None, args=None)` — spawn a local CLI process and talk over stdio. +- `RuntimeConnection.for_tcp(port=0, connection_token=None, path=None, args=None)` — spawn a local CLI in TCP mode. +- `RuntimeConnection.for_uri(url, connection_token=None)` — connect to an existing CLI server (e.g. `"localhost:8080"`). **`CopilotClient.create_session()`:** @@ -195,12 +194,12 @@ await client.set_foreground_session_id("session-123") # Subscribe to all lifecycle events def on_lifecycle(event): - print(f"{event.type}: {event.sessionId}") + print(f"{event.type}: {event.session_id}") -unsubscribe = client.on(on_lifecycle) +unsubscribe = client.on_lifecycle(on_lifecycle) # Subscribe to specific event type -unsubscribe = client.on("session.foreground", lambda e: print(f"Foreground: {e.sessionId}")) +unsubscribe = client.on_lifecycle("session.foreground", lambda e: print(f"Foreground: {e.session_id}")) # Later, to stop receiving events: unsubscribe() @@ -531,13 +530,13 @@ async with await client.create_session( The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` config to enable trace export and automatic W3C Trace Context propagation. ```python -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient -client = CopilotClient(SubprocessConfig( +client = CopilotClient( telemetry={ "otlp_endpoint": "http://localhost:4318", }, -)) +) ``` **TelemetryConfig options:** @@ -575,31 +574,27 @@ session = await client.create_session( Provide your own function to inspect each request and apply custom logic (sync or async): ```python -from copilot.session import PermissionRequestResult -from copilot.generated.session_events import PermissionRequest +from copilot import ( + PermissionDecisionApproveOnce, + PermissionDecisionReject, + PermissionRequest, + PermissionRequestResult, + PermissionRequestShell, +) + def on_permission_request( request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: - # request.kind — what type of operation is being requested: - # "shell" — executing a shell command - # "write" — writing or editing a file - # "read" — reading a file - # "mcp" — calling an MCP tool - # "custom-tool" — calling one of your registered tools - # "url" — fetching a URL - # "memory" — accessing or updating session/workspace memory - # "hook" — invoking a registered hook - # request.tool_call_id — the tool call that triggered this request - # request.tool_name — name of the tool (for custom-tool / mcp) - # request.file_name — file being written (for write) - # request.full_command_text — full shell command (for shell) - - if request.kind.value == "shell": - # Deny shell commands - return PermissionRequestResult(kind="reject") - - return PermissionRequestResult(kind="approve-once") + # ``PermissionRequest`` is a discriminated union — pattern-match on + # the variant class to access the per-kind fields. + match request: + case PermissionRequestShell(full_command_text=cmd): + # Deny shell commands + return PermissionDecisionReject(feedback=f"Shell denied: {cmd}") + case _: + return PermissionDecisionApproveOnce() + session = await client.create_session( on_permission_request=on_permission_request, @@ -615,19 +610,29 @@ async def on_permission_request( ) -> PermissionRequestResult: # Simulate an async approval check (e.g., prompting a user over a network) await asyncio.sleep(0) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() ``` ### Permission Result Kinds -The handler must return a `PermissionRequestResult` with one of the kinds declared by the `PermissionRequestResultKind` type. Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. - -| `kind` value | Meaning | -| ---------------------- | ------------------------------------------------------------------------------------------- | -| `"approve-once"` | Allow this single request | -| `"reject"` | Deny the request | -| `"user-not-available"` | Deny the request because no user is available to confirm it (the default) | -| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | +The handler returns a ``PermissionRequestResult``, which is an alias for +``PermissionDecision | PermissionNoResult`` (the generated wire-level +union of every decision variant, plus a small sentinel for v1 servers). +Approval decisions are present-tense — they describe the decision to +apply, not the past-tense outcome reported back on `permission.completed` +session events. + +| Variant | Meaning | +| --------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `PermissionDecisionApproveOnce()` | Allow this single request | +| `PermissionDecisionReject(feedback="…")` | Deny the request (optional feedback string forwarded to the LLM) | +| `PermissionDecisionUserNotAvailable()` | Deny the request because no user is available to confirm it (the default) | +| `PermissionNoResult()` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | + +Several richer variants (``PermissionDecisionApproveForSession``, +``PermissionDecisionApproveForLocation``, ``PermissionDecisionApprovePermanently``, +…) are available for granting longer-lived approvals; see the generated +``copilot.generated.rpc`` module for the full list. ### Resuming Sessions diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index c7a37ea0b..ee53264d2 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -5,16 +5,50 @@ """ from .client import ( + ChildProcessRuntimeConnection, CloudSessionOptions, CloudSessionRepository, CopilotClient, - ExternalServerConfig, + GetAuthStatusResponse, + GetStatusResponse, + LogLevel, + ModelBilling, + ModelCapabilities, ModelCapabilitiesOverride, + ModelInfo, + ModelLimits, ModelLimitsOverride, + ModelPolicy, + ModelSupports, ModelSupportsOverride, + ModelVisionLimits, ModelVisionLimitsOverride, + PingResponse, RemoteSessionMode, - SubprocessConfig, + RuntimeConnection, + SessionBackgroundEvent, + SessionContext, + SessionCreatedEvent, + SessionDeletedEvent, + SessionForegroundEvent, + SessionLifecycleEvent, + SessionLifecycleEventBase, + SessionLifecycleEventMetadata, + SessionLifecycleEventType, + SessionLifecycleHandler, + SessionListFilter, + SessionMetadata, + SessionUpdatedEvent, + StdioRuntimeConnection, + StopError, + TcpRuntimeConnection, + TelemetryConfig, + UriRuntimeConnection, +) +from .generated.session_events import ( + PermissionRequest, + SessionEvent, + SessionEventType, ) from .session import ( AutoModeSwitchHandler, @@ -28,16 +62,50 @@ ElicitationHandler, ElicitationParams, ElicitationResult, + ErrorOccurredHandler, + ErrorOccurredHookInput, + ErrorOccurredHookOutput, ExitPlanModeHandler, ExitPlanModeRequest, ExitPlanModeResult, + InfiniteSessionConfig, InputOptions, + MCPHTTPServerConfig, + MCPServerConfig, + MCPStdioServerConfig, + PermissionHandler, + PermissionNoResult, + PermissionRequestResult, + PostToolUseHandler, + PostToolUseHookInput, + PostToolUseHookOutput, + PreMcpToolCallHandler, + PreMcpToolCallHookInput, + PreMcpToolCallHookOutput, + PreToolUseHandler, + PreToolUseHookInput, + PreToolUseHookOutput, ProviderConfig, SessionCapabilities, + SessionEndHandler, + SessionEndHookInput, + SessionEndHookOutput, + SessionEventHandler, SessionFsCapabilities, SessionFsConfig, + SessionHooks, + SessionStartHandler, + SessionStartHookInput, + SessionStartHookOutput, SessionUiApi, SessionUiCapabilities, + SystemMessageConfig, + UserInputHandler, + UserInputRequest, + UserInputResponse, + UserPromptSubmittedHandler, + UserPromptSubmittedHookInput, + UserPromptSubmittedHookOutput, ) from .session_fs_provider import ( SessionFsFileInfo, @@ -51,6 +119,7 @@ ToolBinaryResult, ToolInvocation, ToolResult, + ToolResultType, convert_mcp_call_tool_result, define_tool, ) @@ -58,46 +127,113 @@ __version__ = "0.1.0" __all__ = [ - "CommandContext", "AutoModeSwitchHandler", "AutoModeSwitchRequest", "AutoModeSwitchResponse", - "CommandDefinition", + "ChildProcessRuntimeConnection", "CloudSessionOptions", "CloudSessionRepository", + "CommandContext", + "CommandDefinition", "CopilotClient", "CopilotSession", "CreateSessionFsHandler", + "ElicitationContext", "ElicitationHandler", "ElicitationParams", - "ElicitationContext", "ElicitationResult", + "ErrorOccurredHandler", + "ErrorOccurredHookInput", + "ErrorOccurredHookOutput", "ExitPlanModeHandler", "ExitPlanModeRequest", "ExitPlanModeResult", - "ExternalServerConfig", + "GetAuthStatusResponse", + "GetStatusResponse", + "InfiniteSessionConfig", "InputOptions", + "LogLevel", + "MCPHTTPServerConfig", + "MCPServerConfig", + "MCPStdioServerConfig", + "ModelBilling", + "ModelCapabilities", "ModelCapabilitiesOverride", + "ModelInfo", + "ModelLimits", "ModelLimitsOverride", + "ModelPolicy", + "ModelSupports", "ModelSupportsOverride", + "ModelVisionLimits", "ModelVisionLimitsOverride", + "PermissionHandler", + "PermissionNoResult", + "PermissionRequest", + "PermissionRequestResult", + "PingResponse", + "PostToolUseHandler", + "PostToolUseHookInput", + "PostToolUseHookOutput", + "PreMcpToolCallHandler", + "PreMcpToolCallHookInput", + "PreMcpToolCallHookOutput", + "PreToolUseHandler", + "PreToolUseHookInput", + "PreToolUseHookOutput", "ProviderConfig", "RemoteSessionMode", + "RuntimeConnection", + "SessionBackgroundEvent", "SessionCapabilities", + "SessionContext", + "SessionCreatedEvent", + "SessionDeletedEvent", + "SessionEndHandler", + "SessionEndHookInput", + "SessionEndHookOutput", + "SessionEvent", + "SessionEventHandler", + "SessionEventType", + "SessionForegroundEvent", "SessionFsCapabilities", "SessionFsConfig", "SessionFsFileInfo", "SessionFsProvider", "SessionFsSqliteProvider", "SessionFsSqliteQueryResult", - "create_session_fs_adapter", + "SessionHooks", + "SessionLifecycleEvent", + "SessionLifecycleEventBase", + "SessionLifecycleEventMetadata", + "SessionLifecycleEventType", + "SessionLifecycleHandler", + "SessionListFilter", + "SessionMetadata", + "SessionStartHandler", + "SessionStartHookInput", + "SessionStartHookOutput", "SessionUiApi", "SessionUiCapabilities", - "SubprocessConfig", + "SessionUpdatedEvent", + "StdioRuntimeConnection", + "StopError", + "SystemMessageConfig", + "TcpRuntimeConnection", + "TelemetryConfig", "Tool", "ToolBinaryResult", "ToolInvocation", "ToolResult", + "ToolResultType", + "UriRuntimeConnection", + "UserInputHandler", + "UserInputRequest", + "UserInputResponse", + "UserPromptSubmittedHandler", + "UserPromptSubmittedHookInput", + "UserPromptSubmittedHookOutput", "convert_mcp_call_tool_result", + "create_session_fs_adapter", "define_tool", ] diff --git a/python/copilot/client.py b/python/copilot/client.py index 7af3fb39f..b9f67ab9d 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -25,12 +25,12 @@ import threading import time import uuid -from collections.abc import Awaitable, Callable -from dataclasses import KW_ONLY, dataclass, field +from collections.abc import Awaitable, Callable, Sequence +from dataclasses import dataclass from datetime import UTC, datetime from pathlib import Path from types import TracebackType -from typing import Any, Literal, TypedDict, cast, overload +from typing import Any, ClassVar, Literal, TypedDict, cast, overload from ._diagnostics import log_timing from ._jsonrpc import JsonRpcClient, JsonRpcError, ProcessExitedError @@ -39,6 +39,7 @@ from .generated.rpc import ( ClientSessionApiHandlers, ConnectRequest, + PermissionDecisionUserNotAvailable, RemoteSessionMode, ServerRpc, _InternalServerRpc, @@ -46,8 +47,8 @@ register_client_session_api_handlers, ) from .generated.session_events import ( - PermissionRequest, SessionEvent, + _load_PermissionRequest, session_event_from_dict, ) from .session import ( @@ -61,6 +62,7 @@ ExitPlanModeHandler, InfiniteSessionConfig, MCPServerConfig, + PermissionNoResult, ProviderConfig, ReasoningEffort, SectionTransformFn, @@ -79,7 +81,7 @@ # Connection Types # ============================================================================ -ConnectionState = Literal["disconnected", "connecting", "connected", "error"] +_ConnectionState = Literal["disconnected", "connecting", "connected", "error"] LogLevel = Literal["none", "error", "warning", "info", "debug", "all"] @@ -154,112 +156,149 @@ class TelemetryConfig(TypedDict, total=False): @dataclass -class SubprocessConfig: - """Config for spawning a local Copilot CLI subprocess. +class RuntimeConnection: + """Discriminated config describing how to reach the Copilot runtime. + + Construct via the static factories :meth:`stdio`, :meth:`tcp`, or + :meth:`uri`. Each factory returns the matching subclass; pattern-match + on the subclass (or :func:`isinstance`) to branch on the transport. Example: - >>> config = SubprocessConfig(github_token="ghp_...") - >>> client = CopilotClient(config) - - >>> # Custom CLI path with TCP transport - >>> config = SubprocessConfig( - ... cli_path="/usr/local/bin/copilot", - ... use_stdio=False, - ... log_level="debug", - ... ) + >>> CopilotClient() # default: stdio with the bundled runtime + >>> CopilotClient(connection=RuntimeConnection.for_uri("localhost:3000")) """ - cli_path: str | None = None - """Path to the Copilot CLI executable. ``None`` uses the bundled binary.""" + @staticmethod + def for_stdio( + *, + path: str | None = None, + args: Sequence[str] = (), + ) -> StdioRuntimeConnection: + """Spawn a runtime child process and communicate over its stdin/stdout. - cli_args: list[str] = field(default_factory=list) - """Extra arguments passed to the CLI executable (inserted before SDK-managed args).""" + This is the default when no :attr:`CopilotClientOptions.connection` + is supplied. - _: KW_ONLY + Args: + path: Path to the runtime executable. When ``None``, uses the + bundled binary. + args: Extra command-line arguments passed to the runtime process. + """ + return StdioRuntimeConnection(path=path, args=tuple(args)) - working_directory: str | None = None - """Working directory for the CLI process. ``None`` uses the current directory.""" + @staticmethod + def for_tcp( + *, + port: int = 0, + connection_token: str | None = None, + path: str | None = None, + args: Sequence[str] = (), + ) -> TcpRuntimeConnection: + """Spawn a runtime child process listening on a TCP socket. - use_stdio: bool = True - """Use stdio transport (``True``, default) or TCP (``False``).""" + Args: + port: TCP port to listen on. ``0`` (the default) auto-allocates + a free port. If the chosen port is already in use, startup + fails. + connection_token: Optional shared secret the SDK sends to the + spawned runtime to authenticate the TCP connection. When + ``None``, a UUID is generated automatically so the loopback + listener is safe by default. + path: Path to the runtime executable. When ``None``, uses the + bundled binary. + args: Extra command-line arguments passed to the runtime process. + """ + return TcpRuntimeConnection( + path=path, + args=tuple(args), + port=port, + connection_token=connection_token, + ) - tcp_connection_token: str | None = None - """Connection token for the headless CLI server (TCP only). + @staticmethod + def for_uri(url: str, *, connection_token: str | None = None) -> UriRuntimeConnection: + """Connect to an already-running runtime at the given URL. - Only meaningful when ``use_stdio=False``. When the SDK spawns the CLI in TCP mode and - this is omitted, a UUID is generated automatically so the loopback listener is safe by - default. Combining this with ``use_stdio=True`` raises :class:`ValueError`. - """ + Args: + url: URL of the runtime to connect to. Accepts ``"port"``, + ``"host:port"``, or a full URL. + connection_token: Optional shared secret to authenticate the + connection. Required when the server was started with a + token; ignored by legacy servers without ``connect`` support. + """ + return UriRuntimeConnection(url=url, connection_token=connection_token) - port: int = 0 - """TCP port for the CLI server (only when ``use_stdio=False``). 0 means random.""" - log_level: LogLevel = "info" - """Log level for the CLI process.""" +@dataclass +class ChildProcessRuntimeConnection(RuntimeConnection): + """Base for :class:`RuntimeConnection` variants that spawn a runtime child process. - env: dict[str, str] | None = None - """Environment variables for the CLI process. ``None`` inherits the current env.""" + Construct via :meth:`RuntimeConnection.stdio` or :meth:`RuntimeConnection.tcp`. + """ - github_token: str | None = None - """GitHub token for authentication. Takes priority over other auth methods.""" + path: str | None = None + """Path to the runtime executable. ``None`` uses the bundled binary.""" - copilot_home: str | None = None - """Base directory for Copilot data (session state, config, etc.). + args: Sequence[str] = () + """Extra command-line arguments passed to the runtime process.""" - Sets the ``COPILOT_HOME`` environment variable on the spawned CLI process. - When ``None``, the CLI defaults to ``~/.copilot``. - This option is only used when the SDK spawns the CLI process. - """ - use_logged_in_user: bool | None = None - """Use the logged-in user for authentication. +@dataclass +class StdioRuntimeConnection(ChildProcessRuntimeConnection): + """Spawns a runtime child process and communicates over its stdin/stdout. - ``None`` (default) resolves to ``True`` unless ``github_token`` is set. + Construct via :meth:`RuntimeConnection.stdio`. """ - telemetry: TelemetryConfig | None = None - """OpenTelemetry configuration. Providing this enables telemetry — no separate flag needed.""" - session_fs: SessionFsConfig | None = None - """Connection-level session filesystem provider configuration.""" - - session_idle_timeout_seconds: int | None = None - """Server-wide session idle timeout in seconds. +@dataclass +class TcpRuntimeConnection(ChildProcessRuntimeConnection): + """Spawns a runtime child process listening on a TCP socket. - Sessions without activity for this duration are automatically cleaned up. - Set to ``None`` or ``0`` to disable (sessions live indefinitely). - This option is only used when the SDK spawns the CLI process. + Construct via :meth:`RuntimeConnection.tcp`. """ - remote: bool = False - """Enable remote session support (Mission Control integration). + port: int = 0 + """TCP port to listen on. ``0`` (the default) auto-allocates a free port.""" - When ``True``, sessions in a GitHub repository working directory are - accessible from GitHub web and mobile. - This option is only used when the SDK spawns the CLI process. - """ + connection_token: str | None = None + """Shared secret the SDK sends to the spawned runtime. ``None`` auto-generates one.""" @dataclass -class ExternalServerConfig: - """Config for connecting to an existing Copilot CLI server over TCP. +class UriRuntimeConnection(RuntimeConnection): + """Connects to an already-running runtime at the specified URL. - Example: - >>> config = ExternalServerConfig(url="localhost:3000") - >>> client = CopilotClient(config) + Construct via :meth:`RuntimeConnection.uri`. """ - url: str - """Server URL. Supports ``"host:port"``, ``"http://host:port"``, or just ``"port"``.""" + url: str = "" + """URL of the runtime to connect to. Accepts ``"port"``, ``"host:port"``, or a full URL.""" + + connection_token: str | None = None + """Shared secret to authenticate the connection.""" - _: KW_ONLY - tcp_connection_token: str | None = None - """Connection token sent in the ``connect`` handshake. Required when the server was - started with a token; ignored by legacy servers without ``connect`` support.""" +@dataclass +class _CopilotClientOptions: + """Internal configuration carrier used by :class:`CopilotClient`. + + This is not part of the public API: ``CopilotClient`` accepts all of + these options as keyword arguments directly. + """ + connection: RuntimeConnection | None = None + working_directory: str | None = None + log_level: LogLevel = "info" + env: dict[str, str] | None = None + github_token: str | None = None + base_directory: str | None = None + use_logged_in_user: bool | None = None + telemetry: TelemetryConfig | None = None session_fs: SessionFsConfig | None = None - """Connection-level session filesystem provider configuration.""" + session_idle_timeout_seconds: int | None = None + enable_remote_sessions: bool = False + on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None # ============================================================================ @@ -272,32 +311,32 @@ class PingResponse: """Response from ping""" message: str # Echo message with "pong: " prefix - timestamp: datetime # ISO 8601 timestamp when the ping was processed - protocolVersion: int # Protocol version for SDK compatibility + timestamp: datetime # Timestamp when the ping was processed + protocol_version: int # Protocol version for SDK compatibility @staticmethod def from_dict(obj: Any) -> PingResponse: assert isinstance(obj, dict) message = obj.get("message") timestamp = obj.get("timestamp") - protocolVersion = obj.get("protocolVersion") - if message is None or timestamp is None or protocolVersion is None: + protocol_version = obj.get("protocolVersion") + if message is None or timestamp is None or protocol_version is None: raise ValueError( f"Missing required fields in PingResponse: message={message}, " - f"timestamp={timestamp}, protocolVersion={protocolVersion}" + f"timestamp={timestamp}, protocolVersion={protocol_version}" ) timestamp_value = ( datetime.fromtimestamp(timestamp / 1000, tz=UTC) if isinstance(timestamp, (int, float)) else from_datetime(timestamp) ) - return PingResponse(str(message), timestamp_value, int(protocolVersion)) + return PingResponse(str(message), timestamp_value, int(protocol_version)) def to_dict(self) -> dict: result: dict = {} result["message"] = self.message result["timestamp"] = self.timestamp.isoformat() - result["protocolVersion"] = self.protocolVersion + result["protocolVersion"] = self.protocol_version return result @@ -329,24 +368,24 @@ class GetStatusResponse: """Response from status.get""" version: str # Package version (e.g., "1.0.0") - protocolVersion: int # Protocol version for SDK compatibility + protocol_version: int # Protocol version for SDK compatibility @staticmethod def from_dict(obj: Any) -> GetStatusResponse: assert isinstance(obj, dict) version = obj.get("version") - protocolVersion = obj.get("protocolVersion") - if version is None or protocolVersion is None: + protocol_version = obj.get("protocolVersion") + if version is None or protocol_version is None: raise ValueError( f"Missing required fields in GetStatusResponse: version={version}, " - f"protocolVersion={protocolVersion}" + f"protocolVersion={protocol_version}" ) - return GetStatusResponse(str(version), int(protocolVersion)) + return GetStatusResponse(str(version), int(protocol_version)) def to_dict(self) -> dict: result: dict = {} result["version"] = self.version - result["protocolVersion"] = self.protocolVersion + result["protocolVersion"] = self.protocol_version return result @@ -678,7 +717,7 @@ class SessionContext: """Working directory context for a session""" working_directory: str # Working directory where the session was created - gitRoot: str | None = None # Git repository root (if in a git repo) + git_root: str | None = None # Git repository root (if in a git repo) repository: str | None = None # GitHub repository in "owner/repo" format branch: str | None = None # Current git branch @@ -690,15 +729,15 @@ def from_dict(obj: Any) -> SessionContext: raise ValueError("Missing required field 'cwd' in SessionContext") return SessionContext( working_directory=str(cwd), - gitRoot=obj.get("gitRoot"), + git_root=obj.get("gitRoot"), repository=obj.get("repository"), branch=obj.get("branch"), ) def to_dict(self) -> dict: result: dict = {"cwd": self.working_directory} - if self.gitRoot is not None: - result["gitRoot"] = self.gitRoot + if self.git_root is not None: + result["gitRoot"] = self.git_root if self.repository is not None: result["repository"] = self.repository if self.branch is not None: @@ -711,7 +750,7 @@ class SessionListFilter: """Filter options for listing sessions""" working_directory: str | None = None # Filter by exact working directory match - gitRoot: str | None = None # Filter by git root + git_root: str | None = None # Filter by git root repository: str | None = None # Filter by repository (owner/repo format) branch: str | None = None # Filter by branch @@ -719,8 +758,8 @@ def to_dict(self) -> dict: result: dict = {} if self.working_directory is not None: result["cwd"] = self.working_directory - if self.gitRoot is not None: - result["gitRoot"] = self.gitRoot + if self.git_root is not None: + result["gitRoot"] = self.git_root if self.repository is not None: result["repository"] = self.repository if self.branch is not None: @@ -732,43 +771,43 @@ def to_dict(self) -> dict: class SessionMetadata: """Metadata about a session""" - sessionId: str # Session identifier - startTime: str # ISO 8601 timestamp when session was created - modifiedTime: str # ISO 8601 timestamp when session was last modified - isRemote: bool # Whether the session is remote + session_id: str # Session identifier + start_time: datetime # Timestamp when session was created + modified_time: datetime # Timestamp when session was last modified + is_remote: bool # Whether the session is remote summary: str | None = None # Optional summary of the session context: SessionContext | None = None # Working directory context @staticmethod def from_dict(obj: Any) -> SessionMetadata: assert isinstance(obj, dict) - sessionId = obj.get("sessionId") - startTime = obj.get("startTime") - modifiedTime = obj.get("modifiedTime") - isRemote = obj.get("isRemote") - if sessionId is None or startTime is None or modifiedTime is None or isRemote is None: + session_id = obj.get("sessionId") + start_time = obj.get("startTime") + modified_time = obj.get("modifiedTime") + is_remote = obj.get("isRemote") + if session_id is None or start_time is None or modified_time is None or is_remote is None: raise ValueError( - f"Missing required fields in SessionMetadata: sessionId={sessionId}, " - f"startTime={startTime}, modifiedTime={modifiedTime}, isRemote={isRemote}" + f"Missing required fields in SessionMetadata: sessionId={session_id}, " + f"startTime={start_time}, modifiedTime={modified_time}, isRemote={is_remote}" ) summary = obj.get("summary") context_dict = obj.get("context") context = SessionContext.from_dict(context_dict) if context_dict else None return SessionMetadata( - sessionId=str(sessionId), - startTime=str(startTime), - modifiedTime=str(modifiedTime), - isRemote=bool(isRemote), + session_id=str(session_id), + start_time=_parse_session_timestamp(start_time), + modified_time=_parse_session_timestamp(modified_time), + is_remote=bool(is_remote), summary=summary, context=context, ) def to_dict(self) -> dict: result: dict = {} - result["sessionId"] = self.sessionId - result["startTime"] = self.startTime - result["modifiedTime"] = self.modifiedTime - result["isRemote"] = self.isRemote + result["sessionId"] = self.session_id + result["startTime"] = self.start_time.isoformat() + result["modifiedTime"] = self.modified_time.isoformat() + result["isRemote"] = self.is_remote if self.summary is not None: result["summary"] = self.summary if self.context is not None: @@ -776,6 +815,18 @@ def to_dict(self) -> dict: return result +def _parse_session_timestamp(value: Any) -> datetime: + """Parse a wire-format timestamp into ``datetime``. + + Accepts either an ISO-8601 string (server-sent JSON) or an existing + ``datetime`` (round-tripped from a previous parse). Returns the value + as-is if it's already a ``datetime``. + """ + if isinstance(value, datetime): + return value + return from_datetime(value) + + # ============================================================================ # Session Lifecycle Types (for TUI+server mode) # ============================================================================ @@ -793,50 +844,107 @@ def to_dict(self) -> dict: class SessionLifecycleEventMetadata: """Metadata for session lifecycle events.""" - startTime: str - modifiedTime: str + start_time: datetime + modified_time: datetime summary: str | None = None @staticmethod def from_dict(data: dict) -> SessionLifecycleEventMetadata: return SessionLifecycleEventMetadata( - startTime=data.get("startTime", ""), - modifiedTime=data.get("modifiedTime", ""), + start_time=_parse_session_timestamp(data.get("startTime", "")), + modified_time=_parse_session_timestamp(data.get("modifiedTime", "")), summary=data.get("summary"), ) @dataclass -class SessionLifecycleEvent: - """Session lifecycle event notification.""" +class SessionLifecycleEventBase: + """Base for session lifecycle event variants. - type: SessionLifecycleEventType - sessionId: str + Construct concrete variants directly (e.g. :class:`SessionCreatedEvent`, + :class:`SessionDeletedEvent`); pattern-match on the variant class to + branch on the event kind. + """ + + session_id: str metadata: SessionLifecycleEventMetadata | None = None - @staticmethod - def from_dict(data: dict) -> SessionLifecycleEvent: - metadata = None - if "metadata" in data and data["metadata"]: - metadata = SessionLifecycleEventMetadata.from_dict(data["metadata"]) - return SessionLifecycleEvent( - type=data.get("type", "session.updated"), - sessionId=data.get("sessionId", ""), - metadata=metadata, - ) + +@dataclass +class SessionCreatedEvent(SessionLifecycleEventBase): + """Emitted when a session is created.""" + + type: ClassVar[Literal["session.created"]] = "session.created" + + +@dataclass +class SessionDeletedEvent(SessionLifecycleEventBase): + """Emitted when a session is deleted.""" + + type: ClassVar[Literal["session.deleted"]] = "session.deleted" + + +@dataclass +class SessionUpdatedEvent(SessionLifecycleEventBase): + """Emitted when a session is updated (summary/title/etc. changed).""" + + type: ClassVar[Literal["session.updated"]] = "session.updated" + + +@dataclass +class SessionForegroundEvent(SessionLifecycleEventBase): + """Emitted when a session moves to the foreground (TUI+server mode).""" + + type: ClassVar[Literal["session.foreground"]] = "session.foreground" + + +@dataclass +class SessionBackgroundEvent(SessionLifecycleEventBase): + """Emitted when a session moves to the background (TUI+server mode).""" + + type: ClassVar[Literal["session.background"]] = "session.background" + + +SessionLifecycleEvent = ( + SessionCreatedEvent + | SessionDeletedEvent + | SessionUpdatedEvent + | SessionForegroundEvent + | SessionBackgroundEvent +) + + +def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent: + """Construct the correct :class:`SessionLifecycleEvent` variant from a wire dict.""" + metadata = None + if "metadata" in data and data["metadata"]: + metadata = SessionLifecycleEventMetadata.from_dict(data["metadata"]) + session_id = data.get("sessionId", "") + event_type = data.get("type") + if event_type == "session.created": + return SessionCreatedEvent(session_id=session_id, metadata=metadata) + if event_type == "session.deleted": + return SessionDeletedEvent(session_id=session_id, metadata=metadata) + if event_type == "session.foreground": + return SessionForegroundEvent(session_id=session_id, metadata=metadata) + if event_type == "session.background": + return SessionBackgroundEvent(session_id=session_id, metadata=metadata) + # Default to ``session.updated`` for unknown event types so consumers + # keep working across server upgrades. + return SessionUpdatedEvent(session_id=session_id, metadata=metadata) SessionLifecycleHandler = Callable[[SessionLifecycleEvent], None] HandlerUnsubcribe = Callable[[], None] -NO_RESULT_PERMISSION_V2_ERROR = ( +_NO_RESULT_PERMISSION_V2_ERROR = ( "Permission handlers cannot return 'no-result' when connected to a protocol v2 server." ) # Minimum protocol version this SDK can communicate with. # Servers reporting a version below this are rejected. -MIN_PROTOCOL_VERSION = 2 +_MIN_PROTOCOL_VERSION = 2 def _get_bundled_cli_path() -> str | None: @@ -923,99 +1031,160 @@ class CopilotClient: >>> await client.stop() >>> # Or connect to an existing server - >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) + >>> client = CopilotClient( + ... connection=RuntimeConnection.for_uri("localhost:3000"), + ... ) """ def __init__( self, - config: SubprocessConfig | ExternalServerConfig | None = None, *, - auto_start: bool = True, + connection: RuntimeConnection | None = None, + working_directory: str | None = None, + log_level: LogLevel = "info", + env: dict[str, str] | None = None, + github_token: str | None = None, + base_directory: str | None = None, + use_logged_in_user: bool | None = None, + telemetry: TelemetryConfig | None = None, + session_fs: SessionFsConfig | None = None, + session_idle_timeout_seconds: int | None = None, + enable_remote_sessions: bool = False, on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None, ): """ Initialize a new CopilotClient. + All process-management options (``working_directory``, ``log_level``, + ``env``, ``github_token``, …) apply only when the SDK spawns the runtime + (stdio / tcp connections). They are ignored when connecting to an + existing runtime via :meth:`RuntimeConnection.for_uri`. + Args: - config: Connection configuration. Pass a :class:`SubprocessConfig` to - spawn a local CLI process, or an :class:`ExternalServerConfig` to - connect to an existing server. Defaults to ``SubprocessConfig()``. - auto_start: Automatically start the connection on first use - (default: ``True``). - on_list_models: Custom handler for :meth:`list_models`. When provided, - the handler is called instead of querying the CLI server. + connection: How to reach the runtime. Defaults to + :meth:`RuntimeConnection.for_stdio` with the bundled binary. + working_directory: Working directory for the runtime process. + ``None`` uses the current directory. + log_level: Log level for the runtime process. Defaults to ``"info"``. + env: Environment variables for the runtime process. ``None`` inherits + the current env. + github_token: GitHub token for authentication. Takes priority over + other auth methods. + base_directory: Base directory for Copilot data (session state, + config, etc.). Sets the ``COPILOT_HOME`` environment variable on + the spawned runtime. When ``None``, the runtime defaults to + ``~/.copilot``. + use_logged_in_user: Use the logged-in user for authentication. + ``None`` (default) resolves to ``True`` unless ``github_token`` + is set. + telemetry: OpenTelemetry configuration. Providing this enables + telemetry. + session_fs: Connection-level session filesystem provider + configuration. + session_idle_timeout_seconds: Server-wide session idle timeout in + seconds. Sessions without activity for this duration are + automatically cleaned up. Set to ``None`` or ``0`` to disable. + enable_remote_sessions: Enable remote session support (Mission + Control integration). When ``True``, sessions in a GitHub + repository working directory are accessible from GitHub web + and mobile. + on_list_models: Custom handler for :meth:`list_models`. When + provided, the handler is called instead of querying the runtime + server. Example: - >>> # Default — spawns CLI server using stdio + >>> # Default — spawns runtime using stdio with the bundled binary >>> client = CopilotClient() >>> - >>> # Connect to an existing server - >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) + >>> # Connect to an existing runtime + >>> client = CopilotClient( + ... connection=RuntimeConnection.for_uri("localhost:3000"), + ... ) >>> - >>> # Custom CLI path with specific log level + >>> # Custom runtime path with specific log level >>> client = CopilotClient( - ... SubprocessConfig( - ... cli_path="/usr/local/bin/copilot", - ... log_level="debug", - ... ) + ... connection=RuntimeConnection.for_stdio(path="/usr/local/bin/copilot"), + ... log_level="debug", ... ) """ - if config is None: - config = SubprocessConfig() + options = _CopilotClientOptions( + connection=connection, + working_directory=working_directory, + log_level=log_level, + env=env, + github_token=github_token, + base_directory=base_directory, + use_logged_in_user=use_logged_in_user, + telemetry=telemetry, + session_fs=session_fs, + session_idle_timeout_seconds=session_idle_timeout_seconds, + enable_remote_sessions=enable_remote_sessions, + on_list_models=on_list_models, + ) + connection = ( + options.connection if options.connection is not None else RuntimeConnection.for_stdio() + ) - self._config: SubprocessConfig | ExternalServerConfig = config - self._auto_start = auto_start - self._on_list_models = on_list_models + self._options: _CopilotClientOptions = options + self._connection: RuntimeConnection = connection + self._on_list_models = options.on_list_models - # Resolve connection-mode-specific state + # Resolve connection-mode-specific state. self._actual_host: str = "localhost" - self._is_external_server: bool = isinstance(config, ExternalServerConfig) - - if config.tcp_connection_token is not None and len(config.tcp_connection_token) == 0: - raise ValueError("tcp_connection_token must be a non-empty string") - - if isinstance(config, ExternalServerConfig): - self._actual_host, actual_port = self._parse_cli_url(config.url) - self._actual_port: int | None = actual_port - self._effective_connection_token: str | None = config.tcp_connection_token + self._is_external_server: bool = isinstance(connection, UriRuntimeConnection) + + if isinstance(connection, UriRuntimeConnection): + if connection.connection_token is not None and len(connection.connection_token) == 0: + raise ValueError("connection_token must be a non-empty string") + self._actual_host, actual_port = self._parse_cli_url(connection.url) + self._runtime_port: int | None = actual_port + self._effective_connection_token: str | None = connection.connection_token else: - self._actual_port = None - - if config.tcp_connection_token is not None and config.use_stdio: - raise ValueError("tcp_connection_token cannot be used with use_stdio=True") - if config.use_stdio: - self._effective_connection_token = None - elif config.tcp_connection_token is not None: - self._effective_connection_token = config.tcp_connection_token + assert isinstance(connection, ChildProcessRuntimeConnection) + self._runtime_port = None + + if isinstance(connection, TcpRuntimeConnection): + if ( + connection.connection_token is not None + and len(connection.connection_token) == 0 + ): + raise ValueError("connection_token must be a non-empty string") + self._effective_connection_token = ( + connection.connection_token + if connection.connection_token is not None + else str(uuid.uuid4()) + ) else: - self._effective_connection_token = str(uuid.uuid4()) + self._effective_connection_token = None - # Resolve CLI path: explicit > COPILOT_CLI_PATH env var > bundled binary - effective_env = config.env if config.env is not None else os.environ + # Resolve CLI path: explicit > COPILOT_CLI_PATH env var > bundled binary. + effective_env = options.env if options.env is not None else os.environ self._cli_path_source: str | None = "explicit" - if config.cli_path is None: + if connection.path is None: env_cli_path = effective_env.get("COPILOT_CLI_PATH") if env_cli_path: - config.cli_path = env_cli_path + connection.path = env_cli_path self._cli_path_source = "environment" else: bundled_path = _get_bundled_cli_path() if bundled_path: - config.cli_path = bundled_path + connection.path = bundled_path self._cli_path_source = "bundled" else: raise RuntimeError( "Copilot CLI not found. The bundled CLI binary is not available. " - "Ensure you installed a platform-specific wheel, or provide cli_path." + "Ensure you installed a platform-specific wheel, or set " + "RuntimeConnection.for_stdio(path=...) / " + "RuntimeConnection.for_tcp(path=...)." ) # Resolve use_logged_in_user default - if config.use_logged_in_user is None: - config.use_logged_in_user = not bool(config.github_token) + if options.use_logged_in_user is None: + options.use_logged_in_user = not bool(options.github_token) self._process: subprocess.Popen | None = None self._client: JsonRpcClient | None = None - self._state: ConnectionState = "disconnected" + self._state: _ConnectionState = "disconnected" self._sessions: dict[str, CopilotSession] = {} self._sessions_lock = threading.Lock() self._models_cache: list[ModelInfo] | None = None @@ -1027,9 +1196,9 @@ def __init__( self._lifecycle_handlers_lock = threading.Lock() self._rpc: ServerRpc | None = None self._negotiated_protocol_version: int | None = None - if config.session_fs is not None: - _validate_session_fs_config(config.session_fs) - self._session_fs_config = config.session_fs + if options.session_fs is not None: + _validate_session_fs_config(options.session_fs) + self._session_fs_config = options.session_fs @property def rpc(self) -> ServerRpc: @@ -1039,14 +1208,14 @@ def rpc(self) -> ServerRpc: return self._rpc @property - def actual_port(self) -> int | None: - """The actual TCP port the CLI server is listening on, if using TCP transport. + def runtime_port(self) -> int | None: + """TCP port the runtime is listening on, when using TCP transport. Useful for multi-client scenarios where a second client needs to connect - to the same server. Only available after :meth:`start` completes and + to the same runtime. Only available after :meth:`start` completes and only when not using stdio transport. """ - return self._actual_port + return self._runtime_port def _parse_cli_url(self, url: str) -> tuple[str, int]: """ @@ -1128,18 +1297,18 @@ async def start(self) -> None: """ Start the CLI server and establish a connection. - If connecting to an external server (via :class:`ExternalServerConfig`), + If connecting to an already-running runtime (via :meth:`RuntimeConnection.for_uri`), only establishes the connection. Otherwise, spawns the CLI server process and then connects. - This method is called automatically when creating a session if ``auto_start`` - is True (default). + This method is called automatically when creating a session, so most + callers do not need to call it explicitly. Raises: RuntimeError: If the server fails to start or the connection fails. Example: - >>> client = CopilotClient(auto_start=False) + >>> client = CopilotClient() >>> await client.start() >>> # Now ready to create sessions """ @@ -1285,7 +1454,7 @@ async def stop(self) -> None: self._state = "disconnected" if not self._is_external_server: - self._actual_port = None + self._runtime_port = None if errors: raise ExceptionGroup("errors during CopilotClient.stop()", errors) @@ -1340,7 +1509,7 @@ async def force_stop(self) -> None: self._state = "disconnected" if not self._is_external_server: - self._actual_port = None + self._runtime_port = None async def create_session( self, @@ -1375,8 +1544,8 @@ async def create_session( on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, - on_exit_plan_mode: ExitPlanModeHandler | None = None, - on_auto_mode_switch: AutoModeSwitchHandler | None = None, + on_exit_plan_mode_request: ExitPlanModeHandler | None = None, + on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, github_token: str | None = None, remote_session: RemoteSessionMode | None = None, @@ -1386,8 +1555,8 @@ async def create_session( Create a new conversation session with the Copilot CLI. Sessions maintain conversation state, handle events, and manage tool execution. - If the client is not connected and ``auto_start`` is enabled, this will - automatically start the connection. + If the client is not yet connected, this will automatically start the + connection. Args: on_permission_request: Optional handler for permission requests. When @@ -1452,7 +1621,6 @@ async def create_session( A :class:`CopilotSession` instance for the new session. Raises: - RuntimeError: If the client is not connected and auto_start is disabled. ValueError: If ``on_permission_request`` is provided but not callable. Example: @@ -1470,10 +1638,7 @@ async def create_session( if on_permission_request is not None and not callable(on_permission_request): raise ValueError("on_permission_request must be callable when provided.") if not self._client: - if self._auto_start: - await self.start() - else: - raise RuntimeError("Client not connected. Call start() first.") + await self.start() tool_defs = [] if tools: @@ -1518,8 +1683,8 @@ async def create_session( # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) - payload["requestExitPlanMode"] = bool(on_exit_plan_mode) - payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch) + payload["requestExitPlanMode"] = bool(on_exit_plan_mode_request) + payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch_request) # Serialize commands (name + description only) into payload if commands: @@ -1662,10 +1827,10 @@ async def create_session( session._register_user_input_handler(on_user_input_request) if on_elicitation_request: session._register_elicitation_handler(on_elicitation_request) - if on_exit_plan_mode: - session._register_exit_plan_mode_handler(on_exit_plan_mode) - if on_auto_mode_switch: - session._register_auto_mode_switch_handler(on_auto_mode_switch) + if on_exit_plan_mode_request: + session._register_exit_plan_mode_handler(on_exit_plan_mode_request) + if on_auto_mode_switch_request: + session._register_auto_mode_switch_handler(on_auto_mode_switch_request) if hooks: session._register_hooks(hooks) if transform_callbacks: @@ -1754,8 +1919,8 @@ async def resume_session( on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, - on_exit_plan_mode: ExitPlanModeHandler | None = None, - on_auto_mode_switch: AutoModeSwitchHandler | None = None, + on_exit_plan_mode_request: ExitPlanModeHandler | None = None, + on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, github_token: str | None = None, remote_session: RemoteSessionMode | None = None, @@ -1851,10 +2016,7 @@ async def resume_session( if on_permission_request is not None and not callable(on_permission_request): raise ValueError("on_permission_request must be callable when provided.") if not self._client: - if self._auto_start: - await self.start() - else: - raise RuntimeError("Client not connected. Call start() first.") + await self.start() tool_defs = [] if tools: @@ -1912,8 +2074,8 @@ async def resume_session( # Enable elicitation request callback if handler provided payload["requestElicitation"] = bool(on_elicitation_request) - payload["requestExitPlanMode"] = bool(on_exit_plan_mode) - payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch) + payload["requestExitPlanMode"] = bool(on_exit_plan_mode_request) + payload["requestAutoModeSwitch"] = bool(on_auto_mode_switch_request) # Serialize commands (name + description only) into payload if commands: @@ -2015,10 +2177,10 @@ async def resume_session( session._register_user_input_handler(on_user_input_request) if on_elicitation_request: session._register_elicitation_handler(on_elicitation_request) - if on_exit_plan_mode: - session._register_exit_plan_mode_handler(on_exit_plan_mode) - if on_auto_mode_switch: - session._register_auto_mode_switch_handler(on_auto_mode_switch) + if on_exit_plan_mode_request: + session._register_exit_plan_mode_handler(on_exit_plan_mode_request) + if on_auto_mode_switch_request: + session._register_auto_mode_switch_handler(on_auto_mode_switch_request) if hooks: session._register_hooks(hooks) if transform_callbacks: @@ -2074,20 +2236,6 @@ async def resume_session( ) return session - def get_state(self) -> ConnectionState: - """ - Get the current connection state of the client. - - Returns: - The current connection state: "disconnected", "connecting", - "connected", or "error". - - Example: - >>> if client.get_state() == "connected": - ... session = await client.create_session() - """ - return self._state - async def ping(self, message: str | None = None) -> PingResponse: """ Send a ping request to the server to verify connectivity. @@ -2256,7 +2404,7 @@ async def get_session_metadata(self, session_id: str) -> SessionMetadata | None: Example: >>> metadata = await client.get_session_metadata("session-123") >>> if metadata: - ... print(f"Session started at: {metadata.startTime}") + ... print(f"Session started at: {metadata.start_time}") """ if not self._client: raise RuntimeError("Client not connected") @@ -2376,14 +2524,16 @@ async def set_foreground_session_id(self, session_id: str) -> None: raise RuntimeError(f"Failed to set foreground session: {error}") @overload - def on(self, handler: SessionLifecycleHandler, /) -> HandlerUnsubcribe: ... + def on_lifecycle(self, handler: SessionLifecycleHandler, /) -> HandlerUnsubcribe: + pass @overload - def on( + def on_lifecycle( self, event_type: SessionLifecycleEventType, /, handler: SessionLifecycleHandler - ) -> HandlerUnsubcribe: ... + ) -> HandlerUnsubcribe: + pass - def on( + def on_lifecycle( self, event_type_or_handler: SessionLifecycleEventType | SessionLifecycleHandler, /, @@ -2396,8 +2546,8 @@ def on( or change foreground/background state (in TUI+server mode). Can be called in two ways: - - on(handler): Subscribe to all lifecycle events - - on(event_type, handler): Subscribe to a specific event type + - on_lifecycle(handler): Subscribe to all lifecycle events + - on_lifecycle(event_type, handler): Subscribe to a specific event type Args: event_type_or_handler: Either a specific event type to listen for, @@ -2409,10 +2559,12 @@ def on( Example: >>> # Subscribe to specific event type - >>> unsubscribe = client.on("session.foreground", lambda e: print(e.sessionId)) + >>> unsubscribe = client.on_lifecycle( + ... "session.foreground", lambda e: print(e.session_id) + ... ) >>> >>> # Subscribe to all events - >>> unsubscribe = client.on(lambda e: print(f"{e.type}: {e.sessionId}")) + >>> unsubscribe = client.on_lifecycle(lambda e: print(f"{e.type}: {e.session_id}")) >>> >>> # Later, to stop receiving events: >>> unsubscribe() @@ -2444,7 +2596,10 @@ def unsubscribe_typed() -> None: return unsubscribe_typed else: - raise ValueError("Invalid arguments: use on(handler) or on(event_type, handler)") + raise ValueError( + "Invalid arguments: use on_lifecycle(handler) " + "or on_lifecycle(event_type, handler)" + ) def _dispatch_lifecycle_event(self, event: SessionLifecycleEvent) -> None: """Dispatch a lifecycle event to all registered handlers.""" @@ -2489,22 +2644,22 @@ async def _verify_protocol_version(self) -> None: # is silently dropped — the legacy server can't enforce one. used_fallback_ping = True ping_result = await self.ping() - server_version = ping_result.protocolVersion + server_version = ping_result.protocol_version else: raise if server_version is None: raise RuntimeError( "SDK protocol version mismatch: " - f"SDK supports versions {MIN_PROTOCOL_VERSION}-{max_version}" + f"SDK supports versions {_MIN_PROTOCOL_VERSION}-{max_version}" ", but server does not report a protocol version. " "Please update your server to ensure compatibility." ) - if server_version < MIN_PROTOCOL_VERSION or server_version > max_version: + if server_version < _MIN_PROTOCOL_VERSION or server_version > max_version: raise RuntimeError( "SDK protocol version mismatch: " - f"SDK supports versions {MIN_PROTOCOL_VERSION}-{max_version}" + f"SDK supports versions {_MIN_PROTOCOL_VERSION}-{max_version}" f", but server reports version {server_version}. " "Please update your SDK or server to ensure compatibility." ) @@ -2546,8 +2701,8 @@ def _convert_provider_to_wire_format( wire_provider["modelId"] = provider["model_id"] if "wire_model" in provider: wire_provider["wireModel"] = provider["wire_model"] - if "max_input_tokens" in provider: - wire_provider["maxPromptTokens"] = provider["max_input_tokens"] + if "max_prompt_tokens" in provider: + wire_provider["maxPromptTokens"] = provider["max_prompt_tokens"] if "max_output_tokens" in provider: wire_provider["maxOutputTokens"] = provider["max_output_tokens"] if "azure" in provider: @@ -2606,19 +2761,21 @@ def _convert_default_agent_to_wire_format( return wire async def _start_cli_server(self) -> None: - """ - Start the CLI server process. + """Start the runtime process. - This spawns the CLI server as a subprocess using the configured transport + This spawns the runtime as a subprocess using the configured transport mode (stdio or TCP). Raises: RuntimeError: If the server fails to start or times out. """ - assert isinstance(self._config, SubprocessConfig) - cfg = self._config + assert isinstance(self._connection, ChildProcessRuntimeConnection) + conn = self._connection + opts = self._options + use_stdio = isinstance(conn, StdioRuntimeConnection) + tcp_port = conn.port if isinstance(conn, TcpRuntimeConnection) else 0 - cli_path = cfg.cli_path + cli_path = conn.path assert cli_path is not None # resolved in __init__ # Verify CLI exists @@ -2627,24 +2784,24 @@ async def _start_cli_server(self) -> None: if (cli_path := shutil.which(cli_path)) is None: raise RuntimeError(f"Copilot CLI not found at {original_path}") - # Start with user-provided cli_args, then add SDK-managed args - args = list(cfg.cli_args) + [ + # Start with user-provided args, then add SDK-managed args + args = list(conn.args) + [ "--headless", "--no-auto-update", "--log-level", - cfg.log_level, + opts.log_level, ] # Add auth-related flags - if cfg.github_token: + if opts.github_token: args.extend(["--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"]) - if not cfg.use_logged_in_user: + if not opts.use_logged_in_user: args.append("--no-auto-login") - if cfg.session_idle_timeout_seconds is not None and cfg.session_idle_timeout_seconds > 0: - args.extend(["--session-idle-timeout", str(cfg.session_idle_timeout_seconds)]) + if opts.session_idle_timeout_seconds is not None and opts.session_idle_timeout_seconds > 0: + args.extend(["--session-idle-timeout", str(opts.session_idle_timeout_seconds)]) - if cfg.remote: + if opts.enable_remote_sessions: args.append("--remote") # If cli_path is a .js file, run it with node @@ -2659,28 +2816,28 @@ async def _start_cli_server(self) -> None: "cli_path": cli_path, "executable": args[0], "cli_path_source": self._cli_path_source, - "use_stdio": cfg.use_stdio, - "port": None if cfg.use_stdio else cfg.port, + "use_stdio": use_stdio, + "port": None if use_stdio else tcp_port, }, ) # Get environment variables - if cfg.env is None: + if opts.env is None: env = dict(os.environ) else: - env = dict(cfg.env) + env = dict(opts.env) # Set auth token in environment if provided - if cfg.github_token: - env["COPILOT_SDK_AUTH_TOKEN"] = cfg.github_token + if opts.github_token: + env["COPILOT_SDK_AUTH_TOKEN"] = opts.github_token if self._effective_connection_token: env["COPILOT_CONNECTION_TOKEN"] = self._effective_connection_token - if cfg.copilot_home: - env["COPILOT_HOME"] = cfg.copilot_home + if opts.base_directory: + env["COPILOT_HOME"] = opts.base_directory # Set OpenTelemetry environment variables if telemetry config is provided - telemetry = cfg.telemetry + telemetry = opts.telemetry if telemetry is not None: env["COPILOT_OTEL_ENABLED"] = "true" if "otlp_endpoint" in telemetry: @@ -2699,11 +2856,11 @@ async def _start_cli_server(self) -> None: # On Windows, hide the console window to avoid distracting users in GUI apps creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 - cwd = cfg.working_directory or os.getcwd() + cwd = opts.working_directory or os.getcwd() # Choose transport mode spawn_start = time.perf_counter() - if cfg.use_stdio: + if use_stdio: args.append("--stdio") # Use regular Popen with pipes (buffering=0 for unbuffered) self._process = subprocess.Popen( @@ -2717,8 +2874,8 @@ async def _start_cli_server(self) -> None: creationflags=creationflags, ) else: - if cfg.port > 0: - args.extend(["--port", str(cfg.port)]) + if tcp_port > 0: + args.extend(["--port", str(tcp_port)]) self._process = subprocess.Popen( args, stdin=subprocess.DEVNULL, @@ -2736,7 +2893,7 @@ async def _start_cli_server(self) -> None: ) # For stdio mode, we're ready immediately - if cfg.use_stdio: + if use_stdio: return # For TCP mode, wait for port announcement @@ -2755,7 +2912,7 @@ async def read_port(): logger.debug("[CLI] %s", line_str.rstrip()) match = re.search(r"listening on port (\d+)", line_str, re.IGNORECASE) if match: - self._actual_port = int(match.group(1)) + self._runtime_port = int(match.group(1)) return try: @@ -2766,14 +2923,13 @@ async def read_port(): logging.DEBUG, "CopilotClient._start_cli_server TCP port wait complete", port_wait_start, - port=self._actual_port, + port=self._runtime_port, ) except TimeoutError: raise RuntimeError("Timeout waiting for CLI server to start") async def _connect_to_server(self) -> None: - """ - Connect to the CLI server via the configured transport. + """Connect to the runtime via the configured transport. Uses either stdio or TCP based on the client configuration. @@ -2781,8 +2937,7 @@ async def _connect_to_server(self) -> None: RuntimeError: If the connection fails. """ setup_start = time.perf_counter() - use_stdio = isinstance(self._config, SubprocessConfig) and self._config.use_stdio - if use_stdio: + if isinstance(self._connection, StdioRuntimeConnection): await self._connect_via_stdio() else: await self._connect_via_tcp() @@ -2824,7 +2979,7 @@ def handle_notification(method: str, params: dict): session._dispatch_event(event) elif method == "session.lifecycle": # Handle session lifecycle events - lifecycle_event = SessionLifecycleEvent.from_dict(params) + lifecycle_event = _session_lifecycle_event_from_dict(params) self._dispatch_lifecycle_event(lifecycle_event) self._client.set_notification_handler(handle_notification) @@ -2860,7 +3015,7 @@ async def _connect_via_tcp(self) -> None: Raises: RuntimeError: If the server port is not available or connection fails. """ - if not self._actual_port: + if not self._runtime_port: raise RuntimeError("Server port not available") # Create a TCP socket connection with timeout @@ -2876,9 +3031,9 @@ async def _connect_via_tcp(self) -> None: tcp_connect_start = time.perf_counter() logger.info( "CopilotClient._connect_via_tcp connecting to CLI server", - extra={"host": self._actual_host, "port": self._actual_port}, + extra={"host": self._actual_host, "port": self._runtime_port}, ) - sock.connect((self._actual_host, self._actual_port)) + sock.connect((self._actual_host, self._runtime_port)) sock.settimeout(None) # Remove timeout after connection log_timing( logger, @@ -2886,11 +3041,11 @@ async def _connect_via_tcp(self) -> None: "CopilotClient._connect_via_tcp TCP connect complete", tcp_connect_start, host=self._actual_host, - port=self._actual_port, + port=self._runtime_port, ) except OSError as e: raise RuntimeError( - f"Failed to connect to CLI server at {self._actual_host}:{self._actual_port}: {e}" + f"Failed to connect to CLI server at {self._actual_host}:{self._runtime_port}: {e}" ) # Create a file-like wrapper for the socket @@ -2949,7 +3104,7 @@ def handle_notification(method: str, params: dict): session._dispatch_event(event) elif method == "session.lifecycle": # Handle session lifecycle events - lifecycle_event = SessionLifecycleEvent.from_dict(params) + lifecycle_event = _session_lifecycle_event_from_dict(params) self._dispatch_lifecycle_event(lifecycle_event) self._client.set_notification_handler(handle_notification) @@ -3193,22 +3348,14 @@ async def _handle_permission_request_v2(self, params: dict) -> dict: raise ValueError(f"unknown session {session_id}") try: - perm_request = PermissionRequest.from_dict(permission_request) + perm_request = _load_PermissionRequest(permission_request) result = await session._handle_permission_request(perm_request) - if result.kind == "no-result": - raise ValueError(NO_RESULT_PERMISSION_V2_ERROR) - return {"result": {"kind": result.kind}} + if isinstance(result, PermissionNoResult): + raise ValueError(_NO_RESULT_PERMISSION_V2_ERROR) + return {"result": result.to_dict()} except ValueError as exc: - if str(exc) == NO_RESULT_PERMISSION_V2_ERROR: + if str(exc) == _NO_RESULT_PERMISSION_V2_ERROR: raise - return { - "result": { - "kind": "user-not-available", - } - } + return {"result": PermissionDecisionUserNotAvailable().to_dict()} except Exception: # pylint: disable=broad-except - return { - "result": { - "kind": "user-not-available", - } - } + return {"result": PermissionDecisionUserNotAvailable().to_dict()} diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index ce9f98f2a..929aa79e6 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -2,8 +2,9 @@ AUTO-GENERATED FILE - DO NOT EDIT Generated from: api.schema.json """ +from __future__ import annotations -from typing import TYPE_CHECKING +from typing import ClassVar, TYPE_CHECKING from .session_events import AbortReason, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval @@ -384,6 +385,31 @@ def to_dict(self) -> dict: result["includeSkills"] = from_union([from_bool, from_none], self.include_skills) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CommandsRespondToQueuedCommandRequest: + """Queued-command request ID and the result indicating whether the host executed it (and + whether to stop processing further queued commands). + """ + request_id: str + """Request ID from the `command.queued` event the host is responding to.""" + + result: QueuedCommandResult + """Result of the queued command execution.""" + + @staticmethod + def from_dict(obj: Any) -> 'CommandsRespondToQueuedCommandRequest': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + result = _load_QueuedCommandResult(obj.get("result")) + return CommandsRespondToQueuedCommandRequest(request_id, result) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = (self.result).to_dict() + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CommandsRespondToQueuedCommandResult: @@ -2446,6 +2472,30 @@ class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUserKind(Enum) class PermissionDecisionRejectKind(Enum): REJECT = "reject" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class PermissionDecisionRequest: + """Pending permission request ID and the decision to apply (approve/reject and scope).""" + + request_id: str + """Request ID of the pending permission request""" + + result: PermissionDecision + """The client's response to the pending permission prompt""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionRequest': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + result = _load_PermissionDecision(obj.get("result")) + return PermissionDecisionRequest(request_id, result) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = (self.result).to_dict() + return result + class PermissionDecisionUserNotAvailableKind(Enum): USER_NOT_AVAILABLE = "user-not-available" @@ -3224,7 +3274,7 @@ def to_dict(self) -> dict: class QueuedCommandHandled: """Schema for the `QueuedCommandHandled` type.""" - handled: bool + handled: ClassVar[str] = "true" """The host actually executed the queued command.""" stop_processing_queue: bool | None = None @@ -3235,13 +3285,12 @@ class QueuedCommandHandled: @staticmethod def from_dict(obj: Any) -> 'QueuedCommandHandled': assert isinstance(obj, dict) - handled = from_bool(obj.get("handled")) stop_processing_queue = from_union([from_bool, from_none], obj.get("stopProcessingQueue")) - return QueuedCommandHandled(handled, stop_processing_queue) + return QueuedCommandHandled(stop_processing_queue) def to_dict(self) -> dict: result: dict = {} - result["handled"] = from_bool(self.handled) + result["handled"] = self.handled if self.stop_processing_queue is not None: result["stopProcessingQueue"] = from_union([from_bool, from_none], self.stop_processing_queue) return result @@ -3251,7 +3300,7 @@ def to_dict(self) -> dict: class QueuedCommandNotHandled: """Schema for the `QueuedCommandNotHandled` type.""" - handled: bool + handled: ClassVar[str] = "false" """The host did not execute the queued command. Unblocks the queue without claiming the command was processed (e.g. when the handler threw before completing). """ @@ -3259,12 +3308,11 @@ class QueuedCommandNotHandled: @staticmethod def from_dict(obj: Any) -> 'QueuedCommandNotHandled': assert isinstance(obj, dict) - handled = from_bool(obj.get("handled")) - return QueuedCommandNotHandled(handled) + return QueuedCommandNotHandled() def to_dict(self) -> dict: result: dict = {} - result["handled"] = from_bool(self.handled) + result["handled"] = self.handled return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -4237,6 +4285,31 @@ def to_dict(self) -> dict: result["skipped"] = from_list(from_str, self.skipped) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionSetCredentialsParams: + """New auth credentials to install on the session. Omit to leave credentials unchanged.""" + + credentials: AuthInfo | None = None + """The new auth credentials to install on the session. When omitted or `undefined`, the call + is a no-op and the session's existing credentials are preserved. The runtime stores the + value verbatim and uses it for outbound model/API requests; it does NOT re-validate or + re-fetch the associated Copilot user response. Several variants carry secret material; + treat this method's params as containing secrets at rest and in transit. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SessionSetCredentialsParams': + assert isinstance(obj, dict) + credentials = from_union([_load_AuthInfo, from_none], obj.get("credentials")) + return SessionSetCredentialsParams(credentials) + + def to_dict(self) -> dict: + result: dict = {} + if self.credentials is not None: + result["credentials"] = from_union([lambda x: (x).to_dict(), from_none], self.credentials) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionSetCredentialsResult: @@ -5196,6 +5269,25 @@ class TaskInfoType(Enum): AGENT = "agent" SHELL = "shell" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskList: + """Background tasks currently tracked by the session.""" + + tasks: list[TaskInfo] + """Currently tracked tasks""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskList': + assert isinstance(obj, dict) + tasks = from_list(_load_TaskInfo, obj.get("tasks")) + return TaskList(tasks) + + def to_dict(self) -> dict: + result: dict = {} + result["tasks"] = from_list(lambda x: (x).to_dict(), self.tasks) + return result + class TaskShellInfoType(Enum): SHELL = "shell" @@ -5237,6 +5329,29 @@ def to_dict(self) -> dict: result["cancelled"] = from_bool(self.cancelled) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksGetCurrentPromotableResult: + """The first sync-waiting task that can currently be promoted to background mode.""" + + task: TaskInfo | None = None + """The first sync-waiting task (agent first, then shell) that can currently be promoted to + background mode. Omitted if no such task exists. The returned task is guaranteed to have + executionMode='sync' and canPromoteToBackground=true at the time of the call. + """ + + @staticmethod + def from_dict(obj: Any) -> 'TasksGetCurrentPromotableResult': + assert isinstance(obj, dict) + task = from_union([_load_TaskInfo, from_none], obj.get("task")) + return TasksGetCurrentPromotableResult(task) + + def to_dict(self) -> dict: + result: dict = {} + if self.task is not None: + result["task"] = from_union([lambda x: (x).to_dict(), from_none], self.task) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TasksGetProgressRequest: @@ -5256,6 +5371,30 @@ def to_dict(self) -> dict: result["id"] = from_str(self.id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksPromoteCurrentToBackgroundResult: + """The promoted task as it now exists in background mode, omitted if no promotable task was + waiting. + """ + task: TaskInfo | None = None + """The promoted task as it now exists in background mode, omitted if no promotable task was + waiting. Atomic operation: avoids the race window of getCurrentPromotable + + promoteToBackground. + """ + + @staticmethod + def from_dict(obj: Any) -> 'TasksPromoteCurrentToBackgroundResult': + assert isinstance(obj, dict) + task = from_union([_load_TaskInfo, from_none], obj.get("task")) + return TasksPromoteCurrentToBackgroundResult(task) + + def to_dict(self) -> dict: + result: dict = {} + if self.task is not None: + result["task"] = from_union([lambda x: (x).to_dict(), from_none], self.task) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TasksPromoteToBackgroundRequest: @@ -6312,7 +6451,7 @@ class SendAttachmentDirectory: path: str """Absolute directory path""" - type: SlashCommandInputCompletion + type: ClassVar[str] = "directory" """Attachment type discriminator""" @staticmethod @@ -6320,14 +6459,13 @@ def from_dict(obj: Any) -> 'SendAttachmentDirectory': assert isinstance(obj, dict) display_name = from_str(obj.get("displayName")) path = from_str(obj.get("path")) - type = SlashCommandInputCompletion(obj.get("type")) - return SendAttachmentDirectory(display_name, path, type) + return SendAttachmentDirectory(display_name, path) def to_dict(self) -> dict: result: dict = {} result["displayName"] = from_str(self.display_name) result["path"] = from_str(self.path) - result["type"] = to_enum(SlashCommandInputCompletion, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6790,7 +6928,7 @@ class ExternalToolTextResultForLlmContentAudio: mime_type: str """MIME type of the audio (e.g., audio/wav, audio/mpeg)""" - type: ExternalToolTextResultForLlmContentAudioType + type: ClassVar[str] = "audio" """Content block type discriminator""" @staticmethod @@ -6798,14 +6936,13 @@ def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentAudio': assert isinstance(obj, dict) data = from_str(obj.get("data")) mime_type = from_str(obj.get("mimeType")) - type = ExternalToolTextResultForLlmContentAudioType(obj.get("type")) - return ExternalToolTextResultForLlmContentAudio(data, mime_type, type) + return ExternalToolTextResultForLlmContentAudio(data, mime_type) def to_dict(self) -> dict: result: dict = {} result["data"] = from_str(self.data) result["mimeType"] = from_str(self.mime_type) - result["type"] = to_enum(ExternalToolTextResultForLlmContentAudioType, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6819,7 +6956,7 @@ class ExternalToolTextResultForLlmContentImage: mime_type: str """MIME type of the image (e.g., image/png, image/jpeg)""" - type: ExternalToolTextResultForLlmContentImageType + type: ClassVar[str] = "image" """Content block type discriminator""" @staticmethod @@ -6827,14 +6964,13 @@ def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentImage': assert isinstance(obj, dict) data = from_str(obj.get("data")) mime_type = from_str(obj.get("mimeType")) - type = ExternalToolTextResultForLlmContentImageType(obj.get("type")) - return ExternalToolTextResultForLlmContentImage(data, mime_type, type) + return ExternalToolTextResultForLlmContentImage(data, mime_type) def to_dict(self) -> dict: result: dict = {} result["data"] = from_str(self.data) result["mimeType"] = from_str(self.mime_type) - result["type"] = to_enum(ExternalToolTextResultForLlmContentImageType, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6845,20 +6981,19 @@ class ExternalToolTextResultForLlmContentResource: resource: ExternalToolTextResultForLlmContentResourceDetails """The embedded resource contents, either text or base64-encoded binary""" - type: ExternalToolTextResultForLlmContentResourceType + type: ClassVar[str] = "resource" """Content block type discriminator""" @staticmethod def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentResource': assert isinstance(obj, dict) resource = (lambda x: from_union([EmbeddedTextResourceContents.from_dict, EmbeddedBlobResourceContents.from_dict], x))(obj.get("resource")) - type = ExternalToolTextResultForLlmContentResourceType(obj.get("type")) - return ExternalToolTextResultForLlmContentResource(resource, type) + return ExternalToolTextResultForLlmContentResource(resource) def to_dict(self) -> dict: result: dict = {} result["resource"] = from_union([lambda x: to_class(EmbeddedTextResourceContents, x), lambda x: to_class(EmbeddedBlobResourceContents, x)], self.resource) - result["type"] = to_enum(ExternalToolTextResultForLlmContentResourceType, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6869,7 +7004,7 @@ class ExternalToolTextResultForLlmContentTerminal: text: str """Terminal/shell output text""" - type: ExternalToolTextResultForLlmContentTerminalType + type: ClassVar[str] = "terminal" """Content block type discriminator""" cwd: str | None = None @@ -6882,15 +7017,14 @@ class ExternalToolTextResultForLlmContentTerminal: def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentTerminal': assert isinstance(obj, dict) text = from_str(obj.get("text")) - type = ExternalToolTextResultForLlmContentTerminalType(obj.get("type")) cwd = from_union([from_str, from_none], obj.get("cwd")) exit_code = from_union([from_int, from_none], obj.get("exitCode")) - return ExternalToolTextResultForLlmContentTerminal(text, type, cwd, exit_code) + return ExternalToolTextResultForLlmContentTerminal(text, cwd, exit_code) def to_dict(self) -> dict: result: dict = {} result["text"] = from_str(self.text) - result["type"] = to_enum(ExternalToolTextResultForLlmContentTerminalType, self.type) + result["type"] = self.type if self.cwd is not None: result["cwd"] = from_union([from_str, from_none], self.cwd) if self.exit_code is not None: @@ -6905,20 +7039,19 @@ class ExternalToolTextResultForLlmContentText: text: str """The text content""" - type: KindEnum + type: ClassVar[str] = "text" """Content block type discriminator""" @staticmethod def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentText': assert isinstance(obj, dict) text = from_str(obj.get("text")) - type = KindEnum(obj.get("type")) - return ExternalToolTextResultForLlmContentText(text, type) + return ExternalToolTextResultForLlmContentText(text) def to_dict(self) -> dict: result: dict = {} result["text"] = from_str(self.text) - result["type"] = to_enum(KindEnum, self.type) + result["type"] = self.type return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6926,7 +7059,7 @@ def to_dict(self) -> dict: class SlashCommandTextResult: """Schema for the `SlashCommandTextResult` type.""" - kind: KindEnum + kind: ClassVar[str] = "text" """Text result discriminator""" text: str @@ -6946,16 +7079,15 @@ class SlashCommandTextResult: @staticmethod def from_dict(obj: Any) -> 'SlashCommandTextResult': assert isinstance(obj, dict) - kind = KindEnum(obj.get("kind")) text = from_str(obj.get("text")) markdown = from_union([from_bool, from_none], obj.get("markdown")) preserve_ansi = from_union([from_bool, from_none], obj.get("preserveAnsi")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandTextResult(kind, text, markdown, preserve_ansi, runtime_settings_changed) + return SlashCommandTextResult(text, markdown, preserve_ansi, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(KindEnum, self.kind) + result["kind"] = self.kind result["text"] = from_str(self.text) if self.markdown is not None: result["markdown"] = from_union([from_bool, from_none], self.markdown) @@ -7977,97 +8109,99 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForLocationApprovalCommands: - """Schema for the `PermissionDecisionApproveForLocationApprovalCommands` type.""" +class PermissionDecisionApproveForLocation: + """Schema for the `PermissionDecisionApproveForLocation` type.""" - command_identifiers: list[str] - """Command identifiers covered by this approval.""" + approval: PermissionDecisionApproveForLocationApproval + """Approval to persist for this location""" - kind: PermissionDecisionApproveForLocationApprovalCommandsKind - """Approval scoped to specific command identifiers.""" + kind: ClassVar[str] = "approve-for-location" + """Approve and persist for this project location""" + + location_key: str + """Location key (git root or cwd) to persist the approval to""" @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalCommands': + def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocation': assert isinstance(obj, dict) - command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) - kind = PermissionDecisionApproveForLocationApprovalCommandsKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalCommands(command_identifiers, kind) + approval = _load_PermissionDecisionApproveForLocationApproval(obj.get("approval")) + location_key = from_str(obj.get("locationKey")) + return PermissionDecisionApproveForLocation(approval, location_key) def to_dict(self) -> dict: result: dict = {} - result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCommandsKind, self.kind) + result["approval"] = (self.approval).to_dict() + result["kind"] = self.kind + result["locationKey"] = from_str(self.location_key) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForSessionApprovalCommands: - """Schema for the `PermissionDecisionApproveForSessionApprovalCommands` type.""" +class PermissionDecisionApproveForLocationApprovalCommands: + """Schema for the `PermissionDecisionApproveForLocationApprovalCommands` type.""" command_identifiers: list[str] """Command identifiers covered by this approval.""" - kind: PermissionDecisionApproveForLocationApprovalCommandsKind + kind: ClassVar[str] = "commands" """Approval scoped to specific command identifiers.""" @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalCommands': + def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalCommands': assert isinstance(obj, dict) command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) - kind = PermissionDecisionApproveForLocationApprovalCommandsKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalCommands(command_identifiers, kind) + return PermissionDecisionApproveForLocationApprovalCommands(command_identifiers) def to_dict(self) -> dict: result: dict = {} result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCommandsKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionsLocationsAddToolApprovalDetailsCommands: - """Schema for the `PermissionsLocationsAddToolApprovalDetailsCommands` type.""" +class PermissionDecisionApproveForSessionApprovalCommands: + """Schema for the `PermissionDecisionApproveForSessionApprovalCommands` type.""" command_identifiers: list[str] """Command identifiers covered by this approval.""" - kind: PermissionDecisionApproveForLocationApprovalCommandsKind + kind: ClassVar[str] = "commands" """Approval scoped to specific command identifiers.""" @staticmethod - def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsCommands': + def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalCommands': assert isinstance(obj, dict) command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) - kind = PermissionDecisionApproveForLocationApprovalCommandsKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsCommands(command_identifiers, kind) + return PermissionDecisionApproveForSessionApprovalCommands(command_identifiers) def to_dict(self) -> dict: result: dict = {} result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCommandsKind, self.kind) + result["kind"] = self.kind return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class UserToolSessionApprovalCommands: - """Schema for the `UserToolSessionApprovalCommands` type.""" +class PermissionsLocationsAddToolApprovalDetailsCommands: + """Schema for the `PermissionsLocationsAddToolApprovalDetailsCommands` type.""" command_identifiers: list[str] - """Command identifiers approved by the user""" + """Command identifiers covered by this approval.""" - kind: PermissionDecisionApproveForLocationApprovalCommandsKind - """Command approval kind""" + kind: ClassVar[str] = "commands" + """Approval scoped to specific command identifiers.""" @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalCommands': + def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsCommands': assert isinstance(obj, dict) command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) - kind = PermissionDecisionApproveForLocationApprovalCommandsKind(obj.get("kind")) - return UserToolSessionApprovalCommands(command_identifiers, kind) + return PermissionsLocationsAddToolApprovalDetailsCommands(command_identifiers) def to_dict(self) -> dict: result: dict = {} result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCommandsKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8075,7 +8209,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalCustomTool: """Schema for the `PermissionDecisionApproveForLocationApprovalCustomTool` type.""" - kind: PermissionDecisionApproveForLocationApprovalCustomToolKind + kind: ClassVar[str] = "custom-tool" """Approval covering a custom tool.""" tool_name: str @@ -8084,13 +8218,12 @@ class PermissionDecisionApproveForLocationApprovalCustomTool: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalCustomTool': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalCustomToolKind(obj.get("kind")) tool_name = from_str(obj.get("toolName")) - return PermissionDecisionApproveForLocationApprovalCustomTool(kind, tool_name) + return PermissionDecisionApproveForLocationApprovalCustomTool(tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCustomToolKind, self.kind) + result["kind"] = self.kind result["toolName"] = from_str(self.tool_name) return result @@ -8099,7 +8232,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalCustomTool: """Schema for the `PermissionDecisionApproveForSessionApprovalCustomTool` type.""" - kind: PermissionDecisionApproveForLocationApprovalCustomToolKind + kind: ClassVar[str] = "custom-tool" """Approval covering a custom tool.""" tool_name: str @@ -8108,13 +8241,12 @@ class PermissionDecisionApproveForSessionApprovalCustomTool: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalCustomTool': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalCustomToolKind(obj.get("kind")) tool_name = from_str(obj.get("toolName")) - return PermissionDecisionApproveForSessionApprovalCustomTool(kind, tool_name) + return PermissionDecisionApproveForSessionApprovalCustomTool(tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCustomToolKind, self.kind) + result["kind"] = self.kind result["toolName"] = from_str(self.tool_name) return result @@ -8123,7 +8255,7 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsCustomTool: """Schema for the `PermissionsLocationsAddToolApprovalDetailsCustomTool` type.""" - kind: PermissionDecisionApproveForLocationApprovalCustomToolKind + kind: ClassVar[str] = "custom-tool" """Approval covering a custom tool.""" tool_name: str @@ -8132,36 +8264,12 @@ class PermissionsLocationsAddToolApprovalDetailsCustomTool: @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsCustomTool': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalCustomToolKind(obj.get("kind")) - tool_name = from_str(obj.get("toolName")) - return PermissionsLocationsAddToolApprovalDetailsCustomTool(kind, tool_name) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCustomToolKind, self.kind) - result["toolName"] = from_str(self.tool_name) - return result - -@dataclass -class UserToolSessionApprovalCustomTool: - """Schema for the `UserToolSessionApprovalCustomTool` type.""" - - kind: PermissionDecisionApproveForLocationApprovalCustomToolKind - """Custom tool approval kind""" - - tool_name: str - """Custom tool name""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalCustomTool': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalCustomToolKind(obj.get("kind")) tool_name = from_str(obj.get("toolName")) - return UserToolSessionApprovalCustomTool(kind, tool_name) + return PermissionsLocationsAddToolApprovalDetailsCustomTool(tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalCustomToolKind, self.kind) + result["kind"] = self.kind result["toolName"] = from_str(self.tool_name) return result @@ -8170,7 +8278,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalExtensionManagement: """Schema for the `PermissionDecisionApproveForLocationApprovalExtensionManagement` type.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind + kind: ClassVar[str] = "extension-management" """Approval covering extension lifecycle operations such as enable, disable, or reload.""" operation: str | None = None @@ -8181,13 +8289,12 @@ class PermissionDecisionApproveForLocationApprovalExtensionManagement: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalExtensionManagement': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalExtensionManagementKind(obj.get("kind")) operation = from_union([from_str, from_none], obj.get("operation")) - return PermissionDecisionApproveForLocationApprovalExtensionManagement(kind, operation) + return PermissionDecisionApproveForLocationApprovalExtensionManagement(operation) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionManagementKind, self.kind) + result["kind"] = self.kind if self.operation is not None: result["operation"] = from_union([from_str, from_none], self.operation) return result @@ -8197,7 +8304,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalExtensionManagement: """Schema for the `PermissionDecisionApproveForSessionApprovalExtensionManagement` type.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind + kind: ClassVar[str] = "extension-management" """Approval covering extension lifecycle operations such as enable, disable, or reload.""" operation: str | None = None @@ -8208,13 +8315,12 @@ class PermissionDecisionApproveForSessionApprovalExtensionManagement: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalExtensionManagement': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalExtensionManagementKind(obj.get("kind")) operation = from_union([from_str, from_none], obj.get("operation")) - return PermissionDecisionApproveForSessionApprovalExtensionManagement(kind, operation) + return PermissionDecisionApproveForSessionApprovalExtensionManagement(operation) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionManagementKind, self.kind) + result["kind"] = self.kind if self.operation is not None: result["operation"] = from_union([from_str, from_none], self.operation) return result @@ -8224,7 +8330,7 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsExtensionManagement: """Schema for the `PermissionsLocationsAddToolApprovalDetailsExtensionManagement` type.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind + kind: ClassVar[str] = "extension-management" """Approval covering extension lifecycle operations such as enable, disable, or reload.""" operation: str | None = None @@ -8235,13 +8341,12 @@ class PermissionsLocationsAddToolApprovalDetailsExtensionManagement: @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsExtensionManagement': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalExtensionManagementKind(obj.get("kind")) operation = from_union([from_str, from_none], obj.get("operation")) - return PermissionsLocationsAddToolApprovalDetailsExtensionManagement(kind, operation) + return PermissionsLocationsAddToolApprovalDetailsExtensionManagement(operation) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionManagementKind, self.kind) + result["kind"] = self.kind if self.operation is not None: result["operation"] = from_union([from_str, from_none], self.operation) return result @@ -8251,7 +8356,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalMCP: """Schema for the `PermissionDecisionApproveForLocationApprovalMcp` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPKind + kind: ClassVar[str] = "mcp" """Approval covering an MCP tool.""" server_name: str @@ -8263,14 +8368,13 @@ class PermissionDecisionApproveForLocationApprovalMCP: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalMCP': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) tool_name = from_union([from_none, from_str], obj.get("toolName")) - return PermissionDecisionApproveForLocationApprovalMCP(kind, server_name, tool_name) + return PermissionDecisionApproveForLocationApprovalMCP(server_name, tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) result["toolName"] = from_union([from_none, from_str], self.tool_name) return result @@ -8280,7 +8384,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalMCP: """Schema for the `PermissionDecisionApproveForSessionApprovalMcp` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPKind + kind: ClassVar[str] = "mcp" """Approval covering an MCP tool.""" server_name: str @@ -8292,14 +8396,13 @@ class PermissionDecisionApproveForSessionApprovalMCP: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalMCP': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) tool_name = from_union([from_none, from_str], obj.get("toolName")) - return PermissionDecisionApproveForSessionApprovalMCP(kind, server_name, tool_name) + return PermissionDecisionApproveForSessionApprovalMCP(server_name, tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) result["toolName"] = from_union([from_none, from_str], self.tool_name) return result @@ -8309,7 +8412,7 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsMCP: """Schema for the `PermissionsLocationsAddToolApprovalDetailsMcp` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPKind + kind: ClassVar[str] = "mcp" """Approval covering an MCP tool.""" server_name: str @@ -8321,42 +8424,13 @@ class PermissionsLocationsAddToolApprovalDetailsMCP: @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsMCP': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPKind(obj.get("kind")) - server_name = from_str(obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - return PermissionsLocationsAddToolApprovalDetailsMCP(kind, server_name, tool_name) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPKind, self.kind) - result["serverName"] = from_str(self.server_name) - result["toolName"] = from_union([from_none, from_str], self.tool_name) - return result - -@dataclass -class UserToolSessionApprovalMCP: - """Schema for the `UserToolSessionApprovalMcp` type.""" - - kind: PermissionDecisionApproveForLocationApprovalMCPKind - """MCP tool approval kind""" - - server_name: str - """MCP server name""" - - tool_name: str | None = None - """Optional MCP tool name, or null for all tools on the server""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalMCP': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) tool_name = from_union([from_none, from_str], obj.get("toolName")) - return UserToolSessionApprovalMCP(kind, server_name, tool_name) + return PermissionsLocationsAddToolApprovalDetailsMCP(server_name, tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) result["toolName"] = from_union([from_none, from_str], self.tool_name) return result @@ -8366,7 +8440,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalMCPSampling: """Schema for the `PermissionDecisionApproveForLocationApprovalMcpSampling` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPSamplingKind + kind: ClassVar[str] = "mcp-sampling" """Approval covering MCP sampling requests for a server.""" server_name: str @@ -8375,13 +8449,12 @@ class PermissionDecisionApproveForLocationApprovalMCPSampling: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalMCPSampling': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPSamplingKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) - return PermissionDecisionApproveForLocationApprovalMCPSampling(kind, server_name) + return PermissionDecisionApproveForLocationApprovalMCPSampling(server_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPSamplingKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) return result @@ -8390,7 +8463,7 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalMCPSampling: """Schema for the `PermissionDecisionApproveForSessionApprovalMcpSampling` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPSamplingKind + kind: ClassVar[str] = "mcp-sampling" """Approval covering MCP sampling requests for a server.""" server_name: str @@ -8399,13 +8472,12 @@ class PermissionDecisionApproveForSessionApprovalMCPSampling: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalMCPSampling': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPSamplingKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) - return PermissionDecisionApproveForSessionApprovalMCPSampling(kind, server_name) + return PermissionDecisionApproveForSessionApprovalMCPSampling(server_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPSamplingKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) return result @@ -8414,7 +8486,7 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsMCPSampling: """Schema for the `PermissionsLocationsAddToolApprovalDetailsMcpSampling` type.""" - kind: PermissionDecisionApproveForLocationApprovalMCPSamplingKind + kind: ClassVar[str] = "mcp-sampling" """Approval covering MCP sampling requests for a server.""" server_name: str @@ -8423,13 +8495,12 @@ class PermissionsLocationsAddToolApprovalDetailsMCPSampling: @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsMCPSampling': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMCPSamplingKind(obj.get("kind")) server_name = from_str(obj.get("serverName")) - return PermissionsLocationsAddToolApprovalDetailsMCPSampling(kind, server_name) + return PermissionsLocationsAddToolApprovalDetailsMCPSampling(server_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMCPSamplingKind, self.kind) + result["kind"] = self.kind result["serverName"] = from_str(self.server_name) return result @@ -8438,18 +8509,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalMemory: """Schema for the `PermissionDecisionApproveForLocationApprovalMemory` type.""" - kind: PermissionDecisionApproveForLocationApprovalMemoryKind + kind: ClassVar[str] = "memory" """Approval covering writes to long-term memory.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalMemory': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMemoryKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalMemory(kind) + return PermissionDecisionApproveForLocationApprovalMemory() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMemoryKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8457,18 +8527,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalMemory: """Schema for the `PermissionDecisionApproveForSessionApprovalMemory` type.""" - kind: PermissionDecisionApproveForLocationApprovalMemoryKind + kind: ClassVar[str] = "memory" """Approval covering writes to long-term memory.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalMemory': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMemoryKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalMemory(kind) + return PermissionDecisionApproveForSessionApprovalMemory() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMemoryKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8476,36 +8545,17 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsMemory: """Schema for the `PermissionsLocationsAddToolApprovalDetailsMemory` type.""" - kind: PermissionDecisionApproveForLocationApprovalMemoryKind + kind: ClassVar[str] = "memory" """Approval covering writes to long-term memory.""" @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsMemory': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMemoryKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsMemory(kind) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMemoryKind, self.kind) - return result - -@dataclass -class UserToolSessionApprovalMemory: - """Schema for the `UserToolSessionApprovalMemory` type.""" - - kind: PermissionDecisionApproveForLocationApprovalMemoryKind - """Memory approval kind""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalMemory': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalMemoryKind(obj.get("kind")) - return UserToolSessionApprovalMemory(kind) + return PermissionsLocationsAddToolApprovalDetailsMemory() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalMemoryKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8513,18 +8563,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalRead: """Schema for the `PermissionDecisionApproveForLocationApprovalRead` type.""" - kind: PermissionDecisionApproveForLocationApprovalReadKind + kind: ClassVar[str] = "read" """Approval covering read-only filesystem operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalRead': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalReadKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalRead(kind) + return PermissionDecisionApproveForLocationApprovalRead() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalReadKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8532,18 +8581,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalRead: """Schema for the `PermissionDecisionApproveForSessionApprovalRead` type.""" - kind: PermissionDecisionApproveForLocationApprovalReadKind + kind: ClassVar[str] = "read" """Approval covering read-only filesystem operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalRead': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalReadKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalRead(kind) + return PermissionDecisionApproveForSessionApprovalRead() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalReadKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8551,36 +8599,17 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsRead: """Schema for the `PermissionsLocationsAddToolApprovalDetailsRead` type.""" - kind: PermissionDecisionApproveForLocationApprovalReadKind + kind: ClassVar[str] = "read" """Approval covering read-only filesystem operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsRead': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalReadKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsRead(kind) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalReadKind, self.kind) - return result - -@dataclass -class UserToolSessionApprovalRead: - """Schema for the `UserToolSessionApprovalRead` type.""" - - kind: PermissionDecisionApproveForLocationApprovalReadKind - """Read approval kind""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalRead': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalReadKind(obj.get("kind")) - return UserToolSessionApprovalRead(kind) + return PermissionsLocationsAddToolApprovalDetailsRead() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalReadKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8588,18 +8617,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForLocationApprovalWrite: """Schema for the `PermissionDecisionApproveForLocationApprovalWrite` type.""" - kind: PermissionDecisionApproveForLocationApprovalWriteKind + kind: ClassVar[str] = "write" """Approval covering filesystem write operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalWrite': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalWriteKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalWrite(kind) + return PermissionDecisionApproveForLocationApprovalWrite() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalWriteKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8607,18 +8635,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveForSessionApprovalWrite: """Schema for the `PermissionDecisionApproveForSessionApprovalWrite` type.""" - kind: PermissionDecisionApproveForLocationApprovalWriteKind + kind: ClassVar[str] = "write" """Approval covering filesystem write operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalWrite': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalWriteKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalWrite(kind) + return PermissionDecisionApproveForSessionApprovalWrite() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalWriteKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8626,36 +8653,47 @@ def to_dict(self) -> dict: class PermissionsLocationsAddToolApprovalDetailsWrite: """Schema for the `PermissionsLocationsAddToolApprovalDetailsWrite` type.""" - kind: PermissionDecisionApproveForLocationApprovalWriteKind + kind: ClassVar[str] = "write" """Approval covering filesystem write operations.""" @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsWrite': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalWriteKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsWrite(kind) + return PermissionsLocationsAddToolApprovalDetailsWrite() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalWriteKind, self.kind) + result["kind"] = self.kind return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class UserToolSessionApprovalWrite: - """Schema for the `UserToolSessionApprovalWrite` type.""" +class PermissionDecisionApproveForSession: + """Schema for the `PermissionDecisionApproveForSession` type.""" + + kind: ClassVar[str] = "approve-for-session" + """Approve and remember for the rest of the session""" + + approval: PermissionDecisionApproveForSessionApproval | None = None + """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts)""" - kind: PermissionDecisionApproveForLocationApprovalWriteKind - """Write approval kind""" + domain: str | None = None + """URL domain to approve for the rest of the session (URL prompts only)""" @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalWrite': + def from_dict(obj: Any) -> 'PermissionDecisionApproveForSession': assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalWriteKind(obj.get("kind")) - return UserToolSessionApprovalWrite(kind) + approval = from_union([_load_PermissionDecisionApproveForSessionApproval, from_none], obj.get("approval")) + domain = from_union([from_str, from_none], obj.get("domain")) + return PermissionDecisionApproveForSession(approval, domain) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalWriteKind, self.kind) + result["kind"] = self.kind + if self.approval is not None: + result["approval"] = from_union([lambda x: (x).to_dict(), from_none], self.approval) + if self.domain is not None: + result["domain"] = from_union([from_str, from_none], self.domain) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8663,18 +8701,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproveOnce: """Schema for the `PermissionDecisionApproveOnce` type.""" - kind: PermissionDecisionApproveOnceKind + kind: ClassVar[str] = "approve-once" """Approve this single request only""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveOnce': assert isinstance(obj, dict) - kind = PermissionDecisionApproveOnceKind(obj.get("kind")) - return PermissionDecisionApproveOnce(kind) + return PermissionDecisionApproveOnce() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveOnceKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8685,20 +8722,19 @@ class PermissionDecisionApprovePermanently: domain: str """URL domain to approve permanently""" - kind: PermissionDecisionApprovePermanentlyKind + kind: ClassVar[str] = "approve-permanently" """Approve and persist across sessions (URL prompts only)""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApprovePermanently': assert isinstance(obj, dict) domain = from_str(obj.get("domain")) - kind = PermissionDecisionApprovePermanentlyKind(obj.get("kind")) - return PermissionDecisionApprovePermanently(domain, kind) + return PermissionDecisionApprovePermanently(domain) def to_dict(self) -> dict: result: dict = {} result["domain"] = from_str(self.domain) - result["kind"] = to_enum(PermissionDecisionApprovePermanentlyKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8706,18 +8742,17 @@ def to_dict(self) -> dict: class PermissionDecisionApproved: """Schema for the `PermissionDecisionApproved` type.""" - kind: PermissionDecisionApprovedKind + kind: ClassVar[str] = "approved" """The permission request was approved""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproved': assert isinstance(obj, dict) - kind = PermissionDecisionApprovedKind(obj.get("kind")) - return PermissionDecisionApproved(kind) + return PermissionDecisionApproved() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionApprovedKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8728,7 +8763,7 @@ class PermissionDecisionApprovedForLocation: approval: UserToolSessionApproval """The approval to persist for this location""" - kind: PermissionDecisionApprovedForLocationKind + kind: ClassVar[str] = "approved-for-location" """Approved and persisted for this project location""" location_key: str @@ -8738,14 +8773,13 @@ class PermissionDecisionApprovedForLocation: def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocation': assert isinstance(obj, dict) approval = UserToolSessionApproval.from_dict(obj.get("approval")) - kind = PermissionDecisionApprovedForLocationKind(obj.get("kind")) location_key = from_str(obj.get("locationKey")) - return PermissionDecisionApprovedForLocation(approval, kind, location_key) + return PermissionDecisionApprovedForLocation(approval, location_key) def to_dict(self) -> dict: result: dict = {} result["approval"] = to_class(UserToolSessionApproval, self.approval) - result["kind"] = to_enum(PermissionDecisionApprovedForLocationKind, self.kind) + result["kind"] = self.kind result["locationKey"] = from_str(self.location_key) return result @@ -8757,20 +8791,19 @@ class PermissionDecisionApprovedForSession: approval: UserToolSessionApproval """The approval to add as a session-scoped rule""" - kind: PermissionDecisionApprovedForSessionKind + kind: ClassVar[str] = "approved-for-session" """Approved and remembered for the rest of the session""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSession': assert isinstance(obj, dict) approval = UserToolSessionApproval.from_dict(obj.get("approval")) - kind = PermissionDecisionApprovedForSessionKind(obj.get("kind")) - return PermissionDecisionApprovedForSession(approval, kind) + return PermissionDecisionApprovedForSession(approval) def to_dict(self) -> dict: result: dict = {} result["approval"] = to_class(UserToolSessionApproval, self.approval) - result["kind"] = to_enum(PermissionDecisionApprovedForSessionKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8778,7 +8811,7 @@ def to_dict(self) -> dict: class PermissionDecisionCancelled: """Schema for the `PermissionDecisionCancelled` type.""" - kind: PermissionDecisionCancelledKind + kind: ClassVar[str] = "cancelled" """The permission request was cancelled before a response was used""" reason: str | None = None @@ -8787,13 +8820,12 @@ class PermissionDecisionCancelled: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionCancelled': assert isinstance(obj, dict) - kind = PermissionDecisionCancelledKind(obj.get("kind")) reason = from_union([from_str, from_none], obj.get("reason")) - return PermissionDecisionCancelled(kind, reason) + return PermissionDecisionCancelled(reason) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionCancelledKind, self.kind) + result["kind"] = self.kind if self.reason is not None: result["reason"] = from_union([from_str, from_none], self.reason) return result @@ -8803,7 +8835,7 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedByContentExclusionPolicy: """Schema for the `PermissionDecisionDeniedByContentExclusionPolicy` type.""" - kind: PermissionDecisionDeniedByContentExclusionPolicyKind + kind: ClassVar[str] = "denied-by-content-exclusion-policy" """Denied by the organization's content exclusion policy""" message: str @@ -8815,14 +8847,13 @@ class PermissionDecisionDeniedByContentExclusionPolicy: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedByContentExclusionPolicy': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedByContentExclusionPolicyKind(obj.get("kind")) message = from_str(obj.get("message")) path = from_str(obj.get("path")) - return PermissionDecisionDeniedByContentExclusionPolicy(kind, message, path) + return PermissionDecisionDeniedByContentExclusionPolicy(message, path) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedByContentExclusionPolicyKind, self.kind) + result["kind"] = self.kind result["message"] = from_str(self.message) result["path"] = from_str(self.path) return result @@ -8832,7 +8863,7 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedByPermissionRequestHook: """Schema for the `PermissionDecisionDeniedByPermissionRequestHook` type.""" - kind: PermissionDecisionDeniedByPermissionRequestHookKind + kind: ClassVar[str] = "denied-by-permission-request-hook" """Denied by a permission request hook registered by an extension or plugin""" interrupt: bool | None = None @@ -8844,14 +8875,13 @@ class PermissionDecisionDeniedByPermissionRequestHook: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedByPermissionRequestHook': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedByPermissionRequestHookKind(obj.get("kind")) interrupt = from_union([from_bool, from_none], obj.get("interrupt")) message = from_union([from_str, from_none], obj.get("message")) - return PermissionDecisionDeniedByPermissionRequestHook(kind, interrupt, message) + return PermissionDecisionDeniedByPermissionRequestHook(interrupt, message) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedByPermissionRequestHookKind, self.kind) + result["kind"] = self.kind if self.interrupt is not None: result["interrupt"] = from_union([from_bool, from_none], self.interrupt) if self.message is not None: @@ -8863,7 +8893,7 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedByRules: """Schema for the `PermissionDecisionDeniedByRules` type.""" - kind: PermissionDecisionDeniedByRulesKind + kind: ClassVar[str] = "denied-by-rules" """Denied because approval rules explicitly blocked it""" rules: list[PermissionRule] @@ -8872,13 +8902,12 @@ class PermissionDecisionDeniedByRules: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedByRules': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedByRulesKind(obj.get("kind")) rules = from_list(PermissionRule.from_dict, obj.get("rules")) - return PermissionDecisionDeniedByRules(kind, rules) + return PermissionDecisionDeniedByRules(rules) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedByRulesKind, self.kind) + result["kind"] = self.kind result["rules"] = from_list(lambda x: to_class(PermissionRule, x), self.rules) return result @@ -8887,7 +8916,7 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedInteractivelyByUser: """Schema for the `PermissionDecisionDeniedInteractivelyByUser` type.""" - kind: PermissionDecisionDeniedInteractivelyByUserKind + kind: ClassVar[str] = "denied-interactively-by-user" """Denied by the user during an interactive prompt""" feedback: str | None = None @@ -8899,14 +8928,13 @@ class PermissionDecisionDeniedInteractivelyByUser: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedInteractivelyByUser': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedInteractivelyByUserKind(obj.get("kind")) feedback = from_union([from_str, from_none], obj.get("feedback")) force_reject = from_union([from_bool, from_none], obj.get("forceReject")) - return PermissionDecisionDeniedInteractivelyByUser(kind, feedback, force_reject) + return PermissionDecisionDeniedInteractivelyByUser(feedback, force_reject) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedInteractivelyByUserKind, self.kind) + result["kind"] = self.kind if self.feedback is not None: result["feedback"] = from_union([from_str, from_none], self.feedback) if self.force_reject is not None: @@ -8918,18 +8946,17 @@ def to_dict(self) -> dict: class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser: """Schema for the `PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser` type.""" - kind: PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUserKind + kind: ClassVar[str] = "denied-no-approval-rule-and-could-not-request-from-user" """Denied because no approval rule matched and user confirmation was unavailable""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser': assert isinstance(obj, dict) - kind = PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUserKind(obj.get("kind")) - return PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser(kind) + return PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUserKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -8937,7 +8964,7 @@ def to_dict(self) -> dict: class PermissionDecisionReject: """Schema for the `PermissionDecisionReject` type.""" - kind: PermissionDecisionRejectKind + kind: ClassVar[str] = "reject" """Reject the request""" feedback: str | None = None @@ -8946,13 +8973,12 @@ class PermissionDecisionReject: @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionReject': assert isinstance(obj, dict) - kind = PermissionDecisionRejectKind(obj.get("kind")) feedback = from_union([from_str, from_none], obj.get("feedback")) - return PermissionDecisionReject(kind, feedback) + return PermissionDecisionReject(feedback) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionRejectKind, self.kind) + result["kind"] = self.kind if self.feedback is not None: result["feedback"] = from_union([from_str, from_none], self.feedback) return result @@ -8962,18 +8988,17 @@ def to_dict(self) -> dict: class PermissionDecisionUserNotAvailable: """Schema for the `PermissionDecisionUserNotAvailable` type.""" - kind: PermissionDecisionUserNotAvailableKind + kind: ClassVar[str] = "user-not-available" """No user is available to confirm the request""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionUserNotAvailable': assert isinstance(obj, dict) - kind = PermissionDecisionUserNotAvailableKind(obj.get("kind")) - return PermissionDecisionUserNotAvailable(kind) + return PermissionDecisionUserNotAvailable() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionUserNotAvailableKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -9159,40 +9184,6 @@ def to_dict(self) -> dict: result["kind"] = to_enum(QueuePendingItemsKind, self.kind) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class QueuedCommandResult: - """Result of the queued command execution. - - Schema for the `QueuedCommandHandled` type. - - Schema for the `QueuedCommandNotHandled` type. - """ - handled: bool - """The host actually executed the queued command. - - The host did not execute the queued command. Unblocks the queue without claiming the - command was processed (e.g. when the handler threw before completing). - """ - stop_processing_queue: bool | None = None - """When true, the runtime will not process subsequent queued commands until a new request - comes in. - """ - - @staticmethod - def from_dict(obj: Any) -> 'QueuedCommandResult': - assert isinstance(obj, dict) - handled = from_bool(obj.get("handled")) - stop_processing_queue = from_union([from_bool, from_none], obj.get("stopProcessingQueue")) - return QueuedCommandResult(handled, stop_processing_queue) - - def to_dict(self) -> dict: - result: dict = {} - result["handled"] = from_bool(self.handled) - if self.stop_processing_queue is not None: - result["stopProcessingQueue"] = from_union([from_bool, from_none], self.stop_processing_queue) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class RemoteEnableRequest: @@ -9290,7 +9281,7 @@ class SendAttachmentBlob: mime_type: str """MIME type of the inline data""" - type: SendAttachmentBlobType + type: ClassVar[str] = "blob" """Attachment type discriminator""" display_name: str | None = None @@ -9301,15 +9292,14 @@ def from_dict(obj: Any) -> 'SendAttachmentBlob': assert isinstance(obj, dict) data = from_str(obj.get("data")) mime_type = from_str(obj.get("mimeType")) - type = SendAttachmentBlobType(obj.get("type")) display_name = from_union([from_str, from_none], obj.get("displayName")) - return SendAttachmentBlob(data, mime_type, type, display_name) + return SendAttachmentBlob(data, mime_type, display_name) def to_dict(self) -> dict: result: dict = {} result["data"] = from_str(self.data) result["mimeType"] = from_str(self.mime_type) - result["type"] = to_enum(SendAttachmentBlobType, self.type) + result["type"] = self.type if self.display_name is not None: result["displayName"] = from_union([from_str, from_none], self.display_name) return result @@ -9325,7 +9315,7 @@ class SendAttachmentFile: path: str """Absolute file path""" - type: SendAttachmentFileType + type: ClassVar[str] = "file" """Attachment type discriminator""" line_range: SendAttachmentFileLineRange | None = None @@ -9336,15 +9326,14 @@ def from_dict(obj: Any) -> 'SendAttachmentFile': assert isinstance(obj, dict) display_name = from_str(obj.get("displayName")) path = from_str(obj.get("path")) - type = SendAttachmentFileType(obj.get("type")) line_range = from_union([SendAttachmentFileLineRange.from_dict, from_none], obj.get("lineRange")) - return SendAttachmentFile(display_name, path, type, line_range) + return SendAttachmentFile(display_name, path, line_range) def to_dict(self) -> dict: result: dict = {} result["displayName"] = from_str(self.display_name) result["path"] = from_str(self.path) - result["type"] = to_enum(SendAttachmentFileType, self.type) + result["type"] = self.type if self.line_range is not None: result["lineRange"] = from_union([lambda x: to_class(SendAttachmentFileLineRange, x), from_none], self.line_range) return result @@ -9366,7 +9355,7 @@ class SendAttachmentGithubReference: title: str """Title of the referenced item""" - type: SendAttachmentGithubReferenceType + type: ClassVar[str] = "github_reference" """Attachment type discriminator""" url: str @@ -9379,9 +9368,8 @@ def from_dict(obj: Any) -> 'SendAttachmentGithubReference': reference_type = SendAttachmentGithubReferenceTypeEnum(obj.get("referenceType")) state = from_str(obj.get("state")) title = from_str(obj.get("title")) - type = SendAttachmentGithubReferenceType(obj.get("type")) url = from_str(obj.get("url")) - return SendAttachmentGithubReference(number, reference_type, state, title, type, url) + return SendAttachmentGithubReference(number, reference_type, state, title, url) def to_dict(self) -> dict: result: dict = {} @@ -9389,10 +9377,112 @@ def to_dict(self) -> dict: result["referenceType"] = to_enum(SendAttachmentGithubReferenceTypeEnum, self.reference_type) result["state"] = from_str(self.state) result["title"] = from_str(self.title) - result["type"] = to_enum(SendAttachmentGithubReferenceType, self.type) + result["type"] = self.type result["url"] = from_str(self.url) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SendRequest: + """Parameters for sending a user message to the session""" + + prompt: str + """The user message text""" + + agent_mode: SendAgentMode | None = None + """The UI mode the agent was in when this message was sent. Defaults to the session's + current mode. + """ + attachments: list[SendAttachment] | None = None + """Optional attachments (files, directories, selections, blobs, GitHub references) to + include with the message + """ + billable: bool | None = None + """If false, this message will not trigger a Premium Request Unit charge. User messages + default to billable. + """ + display_prompt: str | None = None + """If provided, this is shown in the timeline instead of `prompt`""" + + mode: SendMode | None = None + """How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` + interjects during an in-progress turn. + """ + prepend: bool | None = None + """If true, adds the message to the front of the queue instead of the end""" + + request_headers: dict[str, str] | None = None + """Custom HTTP headers to include in outbound model requests for this turn. Merged with + session-level provider headers; per-turn headers augment and overwrite session-level + headers with the same key. + """ + required_tool: str | None = None + """If set, the request will fail if the named tool is not available when this message is + among the user messages at the start of the current exchange + """ + source: Any = None + """Optional provenance tag copied to the resulting user.message event. Supported values are + `system`, `command-*`, and `schedule-*`. + """ + traceparent: str | None = None + """W3C Trace Context traceparent header for distributed tracing of this agent turn""" + + tracestate: str | None = None + """W3C Trace Context tracestate header for distributed tracing""" + + wait: bool | None = None + """If true, await completion of the agentic loop for this message before returning. Defaults + to false (fire-and-forget). When true, the result still contains the same `messageId`; + the caller can rely on the agent having processed the message before the call resolves. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SendRequest': + assert isinstance(obj, dict) + prompt = from_str(obj.get("prompt")) + agent_mode = from_union([SendAgentMode, from_none], obj.get("agentMode")) + attachments = from_union([lambda x: from_list(_load_SendAttachment, x), from_none], obj.get("attachments")) + billable = from_union([from_bool, from_none], obj.get("billable")) + display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) + mode = from_union([SendMode, from_none], obj.get("mode")) + prepend = from_union([from_bool, from_none], obj.get("prepend")) + request_headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("requestHeaders")) + required_tool = from_union([from_str, from_none], obj.get("requiredTool")) + source = obj.get("source") + traceparent = from_union([from_str, from_none], obj.get("traceparent")) + tracestate = from_union([from_str, from_none], obj.get("tracestate")) + wait = from_union([from_bool, from_none], obj.get("wait")) + return SendRequest(prompt, agent_mode, attachments, billable, display_prompt, mode, prepend, request_headers, required_tool, source, traceparent, tracestate, wait) + + def to_dict(self) -> dict: + result: dict = {} + result["prompt"] = from_str(self.prompt) + if self.agent_mode is not None: + result["agentMode"] = from_union([lambda x: to_enum(SendAgentMode, x), from_none], self.agent_mode) + if self.attachments is not None: + result["attachments"] = from_union([lambda x: from_list(lambda x: (x).to_dict(), x), from_none], self.attachments) + if self.billable is not None: + result["billable"] = from_union([from_bool, from_none], self.billable) + if self.display_prompt is not None: + result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(SendMode, x), from_none], self.mode) + if self.prepend is not None: + result["prepend"] = from_union([from_bool, from_none], self.prepend) + if self.request_headers is not None: + result["requestHeaders"] = from_union([lambda x: from_dict(from_str, x), from_none], self.request_headers) + if self.required_tool is not None: + result["requiredTool"] = from_union([from_str, from_none], self.required_tool) + if self.source is not None: + result["source"] = self.source + if self.traceparent is not None: + result["traceparent"] = from_union([from_str, from_none], self.traceparent) + if self.tracestate is not None: + result["tracestate"] = from_union([from_str, from_none], self.tracestate) + if self.wait is not None: + result["wait"] = from_union([from_bool, from_none], self.wait) + return result + @dataclass class ServerSkillList: """Skills discovered across global and project sources.""" @@ -9731,7 +9821,7 @@ class SlashCommandAgentPromptResult: display_prompt: str """Prompt text to display to the user""" - kind: SlashCommandAgentPromptResultKind + kind: ClassVar[str] = "agent-prompt" """Agent prompt result discriminator""" prompt: str @@ -9749,16 +9839,15 @@ class SlashCommandAgentPromptResult: def from_dict(obj: Any) -> 'SlashCommandAgentPromptResult': assert isinstance(obj, dict) display_prompt = from_str(obj.get("displayPrompt")) - kind = SlashCommandAgentPromptResultKind(obj.get("kind")) prompt = from_str(obj.get("prompt")) mode = from_union([SessionMode, from_none], obj.get("mode")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandAgentPromptResult(display_prompt, kind, prompt, mode, runtime_settings_changed) + return SlashCommandAgentPromptResult(display_prompt, prompt, mode, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} result["displayPrompt"] = from_str(self.display_prompt) - result["kind"] = to_enum(SlashCommandAgentPromptResultKind, self.kind) + result["kind"] = self.kind result["prompt"] = from_str(self.prompt) if self.mode is not None: result["mode"] = from_union([lambda x: to_enum(SessionMode, x), from_none], self.mode) @@ -9771,7 +9860,7 @@ def to_dict(self) -> dict: class SlashCommandCompletedResult: """Schema for the `SlashCommandCompletedResult` type.""" - kind: SlashCommandCompletedResultKind + kind: ClassVar[str] = "completed" """Completed result discriminator""" message: str | None = None @@ -9785,14 +9874,13 @@ class SlashCommandCompletedResult: @staticmethod def from_dict(obj: Any) -> 'SlashCommandCompletedResult': assert isinstance(obj, dict) - kind = SlashCommandCompletedResultKind(obj.get("kind")) message = from_union([from_str, from_none], obj.get("message")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandCompletedResult(kind, message, runtime_settings_changed) + return SlashCommandCompletedResult(message, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(SlashCommandCompletedResultKind, self.kind) + result["kind"] = self.kind if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) if self.runtime_settings_changed is not None: @@ -9807,7 +9895,7 @@ class SlashCommandSelectSubcommandResult: command: str """Parent command name that requires subcommand selection""" - kind: SlashCommandSelectSubcommandResultKind + kind: ClassVar[str] = "select-subcommand" """Select subcommand result discriminator""" options: list[SlashCommandSelectSubcommandOption] @@ -9825,16 +9913,15 @@ class SlashCommandSelectSubcommandResult: def from_dict(obj: Any) -> 'SlashCommandSelectSubcommandResult': assert isinstance(obj, dict) command = from_str(obj.get("command")) - kind = SlashCommandSelectSubcommandResultKind(obj.get("kind")) options = from_list(SlashCommandSelectSubcommandOption.from_dict, obj.get("options")) title = from_str(obj.get("title")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandSelectSubcommandResult(command, kind, options, title, runtime_settings_changed) + return SlashCommandSelectSubcommandResult(command, options, title, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} result["command"] = from_str(self.command) - result["kind"] = to_enum(SlashCommandSelectSubcommandResultKind, self.kind) + result["kind"] = self.kind result["options"] = from_list(lambda x: to_class(SlashCommandSelectSubcommandOption, x), self.options) result["title"] = from_str(self.title) if self.runtime_settings_changed is not None: @@ -9895,7 +9982,7 @@ class TaskShellInfo: status: TaskStatus """Current lifecycle status of the task""" - type: TaskShellInfoType + type: ClassVar[str] = "shell" """Task kind""" can_promote_to_background: bool | None = None @@ -9922,13 +10009,12 @@ def from_dict(obj: Any) -> 'TaskShellInfo': id = from_str(obj.get("id")) started_at = from_datetime(obj.get("startedAt")) status = TaskStatus(obj.get("status")) - type = TaskShellInfoType(obj.get("type")) can_promote_to_background = from_union([from_bool, from_none], obj.get("canPromoteToBackground")) completed_at = from_union([from_datetime, from_none], obj.get("completedAt")) execution_mode = from_union([TaskExecutionMode, from_none], obj.get("executionMode")) log_path = from_union([from_str, from_none], obj.get("logPath")) pid = from_union([from_int, from_none], obj.get("pid")) - return TaskShellInfo(attachment_mode, command, description, id, started_at, status, type, can_promote_to_background, completed_at, execution_mode, log_path, pid) + return TaskShellInfo(attachment_mode, command, description, id, started_at, status, can_promote_to_background, completed_at, execution_mode, log_path, pid) def to_dict(self) -> dict: result: dict = {} @@ -9938,7 +10024,7 @@ def to_dict(self) -> dict: result["id"] = from_str(self.id) result["startedAt"] = self.started_at.isoformat() result["status"] = to_enum(TaskStatus, self.status) - result["type"] = to_enum(TaskShellInfoType, self.type) + result["type"] = self.type if self.can_promote_to_background is not None: result["canPromoteToBackground"] = from_union([from_bool, from_none], self.can_promote_to_background) if self.completed_at is not None: @@ -9981,6 +10067,30 @@ def to_dict(self) -> dict: result["pid"] = from_union([from_int, from_none], self.pid) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class PermissionLocationAddToolApprovalParams: + """Location-scoped tool approval to persist.""" + + approval: PermissionsLocationsAddToolApprovalDetails + """Tool approval to persist and apply""" + + location_key: str + """Location key (git root or cwd) to persist the approval to""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionLocationAddToolApprovalParams': + assert isinstance(obj, dict) + approval = _load_PermissionsLocationsAddToolApprovalDetails(obj.get("approval")) + location_key = from_str(obj.get("locationKey")) + return PermissionLocationAddToolApprovalParams(approval, location_key) + + def to_dict(self) -> dict: + result: dict = {} + result["approval"] = (self.approval).to_dict() + result["locationKey"] = from_str(self.location_key) + return result + @dataclass class ToolList: """Built-in tools available for the requested model, with their parameters and instructions.""" @@ -10711,20 +10821,19 @@ class PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess: extension_name: str """Extension name.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind + kind: ClassVar[str] = "extension-permission-access" """Approval covering an extension's request to access a permission-gated capability.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess': assert isinstance(obj, dict) extension_name = from_str(obj.get("extensionName")) - kind = PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind(obj.get("kind")) - return PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess(extension_name, kind) + return PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess(extension_name) def to_dict(self) -> dict: result: dict = {} result["extensionName"] = from_str(self.extension_name) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -10736,20 +10845,19 @@ class PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess: extension_name: str """Extension name.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind + kind: ClassVar[str] = "extension-permission-access" """Approval covering an extension's request to access a permission-gated capability.""" @staticmethod def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess': assert isinstance(obj, dict) extension_name = from_str(obj.get("extensionName")) - kind = PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind(obj.get("kind")) - return PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess(extension_name, kind) + return PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess(extension_name) def to_dict(self) -> dict: result: dict = {} result["extensionName"] = from_str(self.extension_name) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -10760,179 +10868,75 @@ class PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess: extension_name: str """Extension name.""" - kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind + kind: ClassVar[str] = "extension-permission-access" """Approval covering an extension's request to access a permission-gated capability.""" @staticmethod def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess': assert isinstance(obj, dict) extension_name = from_str(obj.get("extensionName")) - kind = PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind(obj.get("kind")) - return PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess(extension_name, kind) - - def to_dict(self) -> dict: - result: dict = {} - result["extensionName"] = from_str(self.extension_name) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, self.kind) - return result - -@dataclass -class UserToolSessionApprovalExtensionManagement: - """Schema for the `UserToolSessionApprovalExtensionManagement` type.""" - - kind: PermissionDecisionApproveForLocationApprovalExtensionManagementKind - """Extension management approval kind""" - - operation: str | None = None - """Optional operation identifier""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalExtensionManagement': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForLocationApprovalExtensionManagementKind(obj.get("kind")) - operation = from_union([from_str, from_none], obj.get("operation")) - return UserToolSessionApprovalExtensionManagement(kind, operation) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionManagementKind, self.kind) - if self.operation is not None: - result["operation"] = from_union([from_str, from_none], self.operation) - return result - -@dataclass -class UserToolSessionApprovalExtensionPermissionAccess: - """Schema for the `UserToolSessionApprovalExtensionPermissionAccess` type.""" - - extension_name: str - """Extension name""" - - kind: PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind - """Extension permission access approval kind""" - - @staticmethod - def from_dict(obj: Any) -> 'UserToolSessionApprovalExtensionPermissionAccess': - assert isinstance(obj, dict) - extension_name = from_str(obj.get("extensionName")) - kind = PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind(obj.get("kind")) - return UserToolSessionApprovalExtensionPermissionAccess(extension_name, kind) + return PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess(extension_name) def to_dict(self) -> dict: result: dict = {} result["extensionName"] = from_str(self.extension_name) - result["kind"] = to_enum(PermissionDecisionApproveForLocationApprovalExtensionPermissionAccessKind, self.kind) + result["kind"] = self.kind return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class ExternalToolTextResultForLlmContent: - """A content block within a tool result, which may be text, terminal output, image, audio, - or a resource - - Plain text content block - - Terminal/shell output content block with optional exit code and working directory - - Image content block with base64-encoded data - - Audio content block with base64-encoded data - - Resource link content block referencing an external resource - - Embedded resource content block with inline text or binary data - """ - type: ExternalToolTextResultForLlmContentType - """Content block type discriminator""" - - text: str | None = None - """The text content - - Terminal/shell output text - """ - cwd: str | None = None - """Working directory where the command was executed""" +class ExternalToolTextResultForLlm: + """Expanded external tool result payload""" - exit_code: int | None = None - """Process exit code, if the command has completed""" + text_result_for_llm: str + """Text result returned to the model""" - data: str | None = None - """Base64-encoded image data + binary_results_for_llm: list[ExternalToolTextResultForLlmBinaryResultsForLlm] | None = None + """Base64-encoded binary results returned to the model""" - Base64-encoded audio data - """ - mime_type: str | None = None - """MIME type of the image (e.g., image/png, image/jpeg) + contents: list[ExternalToolTextResultForLlmContent] | None = None + """Structured content blocks from the tool""" - MIME type of the audio (e.g., audio/wav, audio/mpeg) + error: str | None = None + """Optional error message for failed executions""" - MIME type of the resource content + result_type: str | None = None + """Execution outcome classification. Optional for back-compat; normalized to 'success' (or + 'failure' when error is present) when missing or unrecognized. """ - description: str | None = None - """Human-readable description of the resource""" - - icons: list[ExternalToolTextResultForLlmContentResourceLinkIcon] | None = None - """Icons associated with this resource""" - - name: str | None = None - """Resource name identifier""" - - size: int | None = None - """Size of the resource in bytes""" - - title: str | None = None - """Human-readable display title for the resource""" - - uri: str | None = None - """URI identifying the resource""" + session_log: str | None = None + """Detailed log content for timeline display""" - resource: ExternalToolTextResultForLlmContentResourceDetails | None = None - """The embedded resource contents, either text or base64-encoded binary""" + tool_telemetry: dict[str, Any] | None = None + """Optional tool-specific telemetry""" @staticmethod - def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContent': + def from_dict(obj: Any) -> 'ExternalToolTextResultForLlm': assert isinstance(obj, dict) - type = ExternalToolTextResultForLlmContentType(obj.get("type")) - text = from_union([from_str, from_none], obj.get("text")) - cwd = from_union([from_str, from_none], obj.get("cwd")) - exit_code = from_union([from_int, from_none], obj.get("exitCode")) - data = from_union([from_str, from_none], obj.get("data")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - description = from_union([from_str, from_none], obj.get("description")) - icons = from_union([lambda x: from_list(ExternalToolTextResultForLlmContentResourceLinkIcon.from_dict, x), from_none], obj.get("icons")) - name = from_union([from_str, from_none], obj.get("name")) - size = from_union([from_int, from_none], obj.get("size")) - title = from_union([from_str, from_none], obj.get("title")) - uri = from_union([from_str, from_none], obj.get("uri")) - resource = from_union([(lambda x: from_union([EmbeddedTextResourceContents.from_dict, EmbeddedBlobResourceContents.from_dict], x)), from_none], obj.get("resource")) - return ExternalToolTextResultForLlmContent(type, text, cwd, exit_code, data, mime_type, description, icons, name, size, title, uri, resource) + text_result_for_llm = from_str(obj.get("textResultForLlm")) + binary_results_for_llm = from_union([lambda x: from_list(ExternalToolTextResultForLlmBinaryResultsForLlm.from_dict, x), from_none], obj.get("binaryResultsForLlm")) + contents = from_union([lambda x: from_list(_load_ExternalToolTextResultForLlmContent, x), from_none], obj.get("contents")) + error = from_union([from_str, from_none], obj.get("error")) + result_type = from_union([from_str, from_none], obj.get("resultType")) + session_log = from_union([from_str, from_none], obj.get("sessionLog")) + tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry")) + return ExternalToolTextResultForLlm(text_result_for_llm, binary_results_for_llm, contents, error, result_type, session_log, tool_telemetry) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(ExternalToolTextResultForLlmContentType, self.type) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - if self.cwd is not None: - result["cwd"] = from_union([from_str, from_none], self.cwd) - if self.exit_code is not None: - result["exitCode"] = from_union([from_int, from_none], self.exit_code) - if self.data is not None: - result["data"] = from_union([from_str, from_none], self.data) - if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) - if self.description is not None: - result["description"] = from_union([from_str, from_none], self.description) - if self.icons is not None: - result["icons"] = from_union([lambda x: from_list(lambda x: to_class(ExternalToolTextResultForLlmContentResourceLinkIcon, x), x), from_none], self.icons) - if self.name is not None: - result["name"] = from_union([from_str, from_none], self.name) - if self.size is not None: - result["size"] = from_union([from_int, from_none], self.size) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) - if self.uri is not None: - result["uri"] = from_union([from_str, from_none], self.uri) - if self.resource is not None: - result["resource"] = from_union([lambda x: from_union([lambda x: to_class(EmbeddedTextResourceContents, x), lambda x: to_class(EmbeddedBlobResourceContents, x)], x), from_none], self.resource) + result["textResultForLlm"] = from_str(self.text_result_for_llm) + if self.binary_results_for_llm is not None: + result["binaryResultsForLlm"] = from_union([lambda x: from_list(lambda x: to_class(ExternalToolTextResultForLlmBinaryResultsForLlm, x), x), from_none], self.binary_results_for_llm) + if self.contents is not None: + result["contents"] = from_union([lambda x: from_list(lambda x: (x).to_dict(), x), from_none], self.contents) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.result_type is not None: + result["resultType"] = from_union([from_str, from_none], self.result_type) + if self.session_log is not None: + result["sessionLog"] = from_union([from_str, from_none], self.session_log) + if self.tool_telemetry is not None: + result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -10943,7 +10947,7 @@ class ExternalToolTextResultForLlmContentResourceLink: name: str """Resource name identifier""" - type: ExternalToolTextResultForLlmContentResourceLinkType + type: ClassVar[str] = "resource_link" """Content block type discriminator""" uri: str @@ -10968,19 +10972,18 @@ class ExternalToolTextResultForLlmContentResourceLink: def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentResourceLink': assert isinstance(obj, dict) name = from_str(obj.get("name")) - type = ExternalToolTextResultForLlmContentResourceLinkType(obj.get("type")) uri = from_str(obj.get("uri")) description = from_union([from_str, from_none], obj.get("description")) icons = from_union([lambda x: from_list(ExternalToolTextResultForLlmContentResourceLinkIcon.from_dict, x), from_none], obj.get("icons")) mime_type = from_union([from_str, from_none], obj.get("mimeType")) size = from_union([from_int, from_none], obj.get("size")) title = from_union([from_str, from_none], obj.get("title")) - return ExternalToolTextResultForLlmContentResourceLink(name, type, uri, description, icons, mime_type, size, title) + return ExternalToolTextResultForLlmContentResourceLink(name, uri, description, icons, mime_type, size, title) def to_dict(self) -> dict: result: dict = {} result["name"] = from_str(self.name) - result["type"] = to_enum(ExternalToolTextResultForLlmContentResourceLinkType, self.type) + result["type"] = self.type result["uri"] = from_str(self.uri) if self.description is not None: result["description"] = from_union([from_str, from_none], self.description) @@ -11509,144 +11512,8 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class CommandsRespondToQueuedCommandRequest: - """Queued-command request ID and the result indicating whether the host executed it (and - whether to stop processing further queued commands). - """ - request_id: str - """Request ID from the `command.queued` event the host is responding to.""" - - result: QueuedCommandResult - """Result of the queued command execution.""" - - @staticmethod - def from_dict(obj: Any) -> 'CommandsRespondToQueuedCommandRequest': - assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - result = QueuedCommandResult.from_dict(obj.get("result")) - return CommandsRespondToQueuedCommandRequest(request_id, result) - - def to_dict(self) -> dict: - result: dict = {} - result["requestId"] = from_str(self.request_id) - result["result"] = to_class(QueuedCommandResult, self.result) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SendAttachment: - """A user message attachment — a file, directory, code selection, blob, or GitHub reference - - File attachment - - Directory attachment - - Code selection attachment from an editor - - GitHub issue, pull request, or discussion reference - - Blob attachment with inline base64-encoded data - """ - type: SendAttachmentType - """Attachment type discriminator""" - - display_name: str | None = None - """User-facing display name for the attachment - - User-facing display name for the selection - """ - line_range: SendAttachmentFileLineRange | None = None - """Optional line range to scope the attachment to a specific section of the file""" - - path: str | None = None - """Absolute file path - - Absolute directory path - """ - file_path: str | None = None - """Absolute path to the file containing the selection""" - - selection: SendAttachmentSelectionDetails | None = None - """Position range of the selection within the file""" - - text: str | None = None - """The selected text content""" - - number: int | None = None - """Issue, pull request, or discussion number""" - - reference_type: SendAttachmentGithubReferenceTypeEnum | None = None - """Type of GitHub reference""" - - state: str | None = None - """Current state of the referenced item (e.g., open, closed, merged)""" - - title: str | None = None - """Title of the referenced item""" - - url: str | None = None - """URL to the referenced item on GitHub""" - - data: str | None = None - """Base64-encoded content""" - - mime_type: str | None = None - """MIME type of the inline data""" - - @staticmethod - def from_dict(obj: Any) -> 'SendAttachment': - assert isinstance(obj, dict) - type = SendAttachmentType(obj.get("type")) - display_name = from_union([from_str, from_none], obj.get("displayName")) - line_range = from_union([SendAttachmentFileLineRange.from_dict, from_none], obj.get("lineRange")) - path = from_union([from_str, from_none], obj.get("path")) - file_path = from_union([from_str, from_none], obj.get("filePath")) - selection = from_union([SendAttachmentSelectionDetails.from_dict, from_none], obj.get("selection")) - text = from_union([from_str, from_none], obj.get("text")) - number = from_union([from_int, from_none], obj.get("number")) - reference_type = from_union([SendAttachmentGithubReferenceTypeEnum, from_none], obj.get("referenceType")) - state = from_union([from_str, from_none], obj.get("state")) - title = from_union([from_str, from_none], obj.get("title")) - url = from_union([from_str, from_none], obj.get("url")) - data = from_union([from_str, from_none], obj.get("data")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - return SendAttachment(type, display_name, line_range, path, file_path, selection, text, number, reference_type, state, title, url, data, mime_type) - - def to_dict(self) -> dict: - result: dict = {} - result["type"] = to_enum(SendAttachmentType, self.type) - if self.display_name is not None: - result["displayName"] = from_union([from_str, from_none], self.display_name) - if self.line_range is not None: - result["lineRange"] = from_union([lambda x: to_class(SendAttachmentFileLineRange, x), from_none], self.line_range) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.file_path is not None: - result["filePath"] = from_union([from_str, from_none], self.file_path) - if self.selection is not None: - result["selection"] = from_union([lambda x: to_class(SendAttachmentSelectionDetails, x), from_none], self.selection) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - if self.number is not None: - result["number"] = from_union([from_int, from_none], self.number) - if self.reference_type is not None: - result["referenceType"] = from_union([lambda x: to_enum(SendAttachmentGithubReferenceTypeEnum, x), from_none], self.reference_type) - if self.state is not None: - result["state"] = from_union([from_str, from_none], self.state) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) - if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) - if self.data is not None: - result["data"] = from_union([from_str, from_none], self.data) - if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SendAttachmentSelection: - """Code selection attachment from an editor""" +class SendAttachmentSelection: + """Code selection attachment from an editor""" display_name: str """User-facing display name for the selection""" @@ -11660,7 +11527,7 @@ class SendAttachmentSelection: text: str """The selected text content""" - type: SendAttachmentSelectionType + type: ClassVar[str] = "selection" """Attachment type discriminator""" @staticmethod @@ -11670,8 +11537,7 @@ def from_dict(obj: Any) -> 'SendAttachmentSelection': file_path = from_str(obj.get("filePath")) selection = SendAttachmentSelectionDetails.from_dict(obj.get("selection")) text = from_str(obj.get("text")) - type = SendAttachmentSelectionType(obj.get("type")) - return SendAttachmentSelection(display_name, file_path, selection, text, type) + return SendAttachmentSelection(display_name, file_path, selection, text) def to_dict(self) -> dict: result: dict = {} @@ -11679,7 +11545,7 @@ def to_dict(self) -> dict: result["filePath"] = from_str(self.file_path) result["selection"] = to_class(SendAttachmentSelectionDetails, self.selection) result["text"] = from_str(self.text) - result["type"] = to_enum(SendAttachmentSelectionType, self.type) + result["type"] = self.type return result @dataclass @@ -11917,107 +11783,6 @@ def to_dict(self) -> dict: result["agent"] = to_class(AgentInfo, self.agent) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SlashCommandInvocationResult: - """Result of invoking the slash command (text output, prompt to send to the agent, or - completion). - - Schema for the `SlashCommandTextResult` type. - - Schema for the `SlashCommandAgentPromptResult` type. - - Schema for the `SlashCommandCompletedResult` type. - - Schema for the `SlashCommandSelectSubcommandResult` type. - """ - kind: SlashCommandInvocationResultKind - """Text result discriminator - - Agent prompt result discriminator - - Completed result discriminator - - Select subcommand result discriminator - """ - markdown: bool | None = None - """Whether text contains Markdown""" - - preserve_ansi: bool | None = None - """Whether ANSI sequences should be preserved""" - - runtime_settings_changed: bool | None = None - """True when the invocation mutated user runtime settings; consumers caching settings should - refresh - """ - text: str | None = None - """Text output for the client to render""" - - display_prompt: str | None = None - """Prompt text to display to the user""" - - mode: SessionMode | None = None - """Optional target session mode for the agent prompt""" - - prompt: str | None = None - """Prompt to submit to the agent""" - - message: str | None = None - """Optional user-facing message describing the completed command""" - - command: str | None = None - """Parent command name that requires subcommand selection""" - - options: list[SlashCommandSelectSubcommandOption] | None = None - """Available subcommand options for the client to present""" - - title: str | None = None - """Human-readable title for the selection UI""" - - @staticmethod - def from_dict(obj: Any) -> 'SlashCommandInvocationResult': - assert isinstance(obj, dict) - kind = SlashCommandInvocationResultKind(obj.get("kind")) - markdown = from_union([from_bool, from_none], obj.get("markdown")) - preserve_ansi = from_union([from_bool, from_none], obj.get("preserveAnsi")) - runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - text = from_union([from_str, from_none], obj.get("text")) - display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) - mode = from_union([SessionMode, from_none], obj.get("mode")) - prompt = from_union([from_str, from_none], obj.get("prompt")) - message = from_union([from_str, from_none], obj.get("message")) - command = from_union([from_str, from_none], obj.get("command")) - options = from_union([lambda x: from_list(SlashCommandSelectSubcommandOption.from_dict, x), from_none], obj.get("options")) - title = from_union([from_str, from_none], obj.get("title")) - return SlashCommandInvocationResult(kind, markdown, preserve_ansi, runtime_settings_changed, text, display_prompt, mode, prompt, message, command, options, title) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(SlashCommandInvocationResultKind, self.kind) - if self.markdown is not None: - result["markdown"] = from_union([from_bool, from_none], self.markdown) - if self.preserve_ansi is not None: - result["preserveAnsi"] = from_union([from_bool, from_none], self.preserve_ansi) - if self.runtime_settings_changed is not None: - result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - if self.display_prompt is not None: - result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) - if self.mode is not None: - result["mode"] = from_union([lambda x: to_enum(SessionMode, x), from_none], self.mode) - if self.prompt is not None: - result["prompt"] = from_union([from_str, from_none], self.prompt) - if self.message is not None: - result["message"] = from_union([from_str, from_none], self.message) - if self.command is not None: - result["command"] = from_union([from_str, from_none], self.command) - if self.options is not None: - result["options"] = from_union([lambda x: from_list(lambda x: to_class(SlashCommandSelectSubcommandOption, x), x), from_none], self.options) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TaskProgress: @@ -12451,7 +12216,7 @@ class APIKeyAuthInfo: host: str """Authentication host.""" - type: APIKeyAuthInfoType + type: ClassVar[str] = "api-key" """API-key authentication for non-GitHub LLM providers (e.g. when running BYOM-style).""" copilot_user: CopilotUserResponse | None = None @@ -12465,15 +12230,14 @@ def from_dict(obj: Any) -> 'APIKeyAuthInfo': assert isinstance(obj, dict) api_key = from_str(obj.get("apiKey")) host = from_str(obj.get("host")) - type = APIKeyAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return APIKeyAuthInfo(api_key, host, type, copilot_user) + return APIKeyAuthInfo(api_key, host, copilot_user) def to_dict(self) -> dict: result: dict = {} result["apiKey"] = from_str(self.api_key) result["host"] = from_str(self.host) - result["type"] = to_enum(APIKeyAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12486,7 +12250,7 @@ class CopilotAPITokenAuthInfo: host: Host """Authentication host (always the public GitHub host).""" - type: CopilotAPITokenAuthInfoType + type: ClassVar[str] = "copilot-api-token" """Direct Copilot API authentication via the `GITHUB_COPILOT_API_TOKEN` + `COPILOT_API_URL` environment-variable pair. The token itself is read from the environment by the runtime, not carried in this struct. @@ -12501,14 +12265,13 @@ class CopilotAPITokenAuthInfo: def from_dict(obj: Any) -> 'CopilotAPITokenAuthInfo': assert isinstance(obj, dict) host = Host(obj.get("host")) - type = CopilotAPITokenAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return CopilotAPITokenAuthInfo(host, type, copilot_user) + return CopilotAPITokenAuthInfo(host, copilot_user) def to_dict(self) -> dict: result: dict = {} result["host"] = to_enum(Host, self.host) - result["type"] = to_enum(CopilotAPITokenAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12527,7 +12290,7 @@ class EnvAuthInfo: token: str """The token value itself. Treat as a secret.""" - type: EnvAuthInfoType + type: ClassVar[str] = "env" """Personal access token (PAT) or server-to-server token sourced from an environment variable. """ @@ -12547,17 +12310,16 @@ def from_dict(obj: Any) -> 'EnvAuthInfo': env_var = from_str(obj.get("envVar")) host = from_str(obj.get("host")) token = from_str(obj.get("token")) - type = EnvAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) login = from_union([from_str, from_none], obj.get("login")) - return EnvAuthInfo(env_var, host, token, type, copilot_user, login) + return EnvAuthInfo(env_var, host, token, copilot_user, login) def to_dict(self) -> dict: result: dict = {} result["envVar"] = from_str(self.env_var) result["host"] = from_str(self.host) result["token"] = from_str(self.token) - result["type"] = to_enum(EnvAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) if self.login is not None: @@ -12578,7 +12340,7 @@ class GhCLIAuthInfo: token: str """The token returned by `gh auth token`. Treat as a secret.""" - type: GhCLIAuthInfoType + type: ClassVar[str] = "gh-cli" """Authentication via the `gh` CLI's saved credentials.""" copilot_user: CopilotUserResponse | None = None @@ -12593,16 +12355,15 @@ def from_dict(obj: Any) -> 'GhCLIAuthInfo': host = from_str(obj.get("host")) login = from_str(obj.get("login")) token = from_str(obj.get("token")) - type = GhCLIAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return GhCLIAuthInfo(host, login, token, type, copilot_user) + return GhCLIAuthInfo(host, login, token, copilot_user) def to_dict(self) -> dict: result: dict = {} result["host"] = from_str(self.host) result["login"] = from_str(self.login) result["token"] = from_str(self.token) - result["type"] = to_enum(GhCLIAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12618,7 +12379,7 @@ class HMACAuthInfo: host: Host """Authentication host. HMAC auth always targets the public GitHub host.""" - type: HMACAuthInfoType + type: ClassVar[str] = "hmac" """HMAC-based authentication used by GitHub-internal services.""" copilot_user: CopilotUserResponse | None = None @@ -12632,15 +12393,14 @@ def from_dict(obj: Any) -> 'HMACAuthInfo': assert isinstance(obj, dict) hmac = from_str(obj.get("hmac")) host = Host(obj.get("host")) - type = HMACAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return HMACAuthInfo(hmac, host, type, copilot_user) + return HMACAuthInfo(hmac, host, copilot_user) def to_dict(self) -> dict: result: dict = {} result["hmac"] = from_str(self.hmac) result["host"] = to_enum(Host, self.host) - result["type"] = to_enum(HMACAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12656,7 +12416,7 @@ class TokenAuthInfo: token: str """The token value itself. Treat as a secret.""" - type: TokenAuthInfoType + type: ClassVar[str] = "token" """SDK-side token authentication; the host configured the token directly via the SDK.""" copilot_user: CopilotUserResponse | None = None @@ -12670,15 +12430,14 @@ def from_dict(obj: Any) -> 'TokenAuthInfo': assert isinstance(obj, dict) host = from_str(obj.get("host")) token = from_str(obj.get("token")) - type = TokenAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return TokenAuthInfo(host, token, type, copilot_user) + return TokenAuthInfo(host, token, copilot_user) def to_dict(self) -> dict: result: dict = {} result["host"] = from_str(self.host) result["token"] = from_str(self.token) - result["type"] = to_enum(TokenAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result @@ -12694,7 +12453,7 @@ class UserAuthInfo: login: str """OAuth user login.""" - type: UserAuthInfoType + type: ClassVar[str] = "user" """OAuth user authentication. The token itself is held in the runtime's secret token store (keyed by host+login) and is NOT carried in this struct. """ @@ -12709,106 +12468,18 @@ def from_dict(obj: Any) -> 'UserAuthInfo': assert isinstance(obj, dict) host = from_str(obj.get("host")) login = from_str(obj.get("login")) - type = UserAuthInfoType(obj.get("type")) copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - return UserAuthInfo(host, login, type, copilot_user) + return UserAuthInfo(host, login, copilot_user) def to_dict(self) -> dict: result: dict = {} result["host"] = from_str(self.host) result["login"] = from_str(self.login) - result["type"] = to_enum(UserAuthInfoType, self.type) + result["type"] = self.type if self.copilot_user is not None: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionDecisionApproveForLocationApproval: - """Approval to persist for this location - - Schema for the `PermissionDecisionApproveForLocationApprovalCommands` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalRead` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalWrite` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalMcp` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalMcpSampling` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalMemory` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalCustomTool` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalExtensionManagement` type. - - Schema for the `PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess` - type. - """ - kind: ApprovalKind - """Approval scoped to specific command identifiers. - - Approval covering read-only filesystem operations. - - Approval covering filesystem write operations. - - Approval covering an MCP tool. - - Approval covering MCP sampling requests for a server. - - Approval covering writes to long-term memory. - - Approval covering a custom tool. - - Approval covering extension lifecycle operations such as enable, disable, or reload. - - Approval covering an extension's request to access a permission-gated capability. - """ - command_identifiers: list[str] | None = None - """Command identifiers covered by this approval.""" - - server_name: str | None = None - """MCP server name.""" - - tool_name: str | None = None - """MCP tool name, or null to cover every tool on the server. - - Custom tool name. - """ - operation: str | None = None - """Optional operation identifier; when omitted, the approval covers all extension management - operations. - """ - extension_name: str | None = None - """Extension name.""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocationApproval': - assert isinstance(obj, dict) - kind = ApprovalKind(obj.get("kind")) - command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) - server_name = from_union([from_str, from_none], obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - operation = from_union([from_str, from_none], obj.get("operation")) - extension_name = from_union([from_str, from_none], obj.get("extensionName")) - return PermissionDecisionApproveForLocationApproval(kind, command_identifiers, server_name, tool_name, operation, extension_name) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(ApprovalKind, self.kind) - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) - if self.server_name is not None: - result["serverName"] = from_union([from_str, from_none], self.server_name) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.operation is not None: - result["operation"] = from_union([from_str, from_none], self.operation) - if self.extension_name is not None: - result["extensionName"] = from_union([from_str, from_none], self.extension_name) - return result - @dataclass class PermissionDecisionApproveForIonApproval: """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) @@ -12928,231 +12599,34 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionApproveForSessionApproval: - """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) - - Schema for the `PermissionDecisionApproveForSessionApprovalCommands` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalRead` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalWrite` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalMcp` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalMcpSampling` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalMemory` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalCustomTool` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalExtensionManagement` type. - - Schema for the `PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess` - type. - """ - kind: ApprovalKind - """Approval scoped to specific command identifiers. - - Approval covering read-only filesystem operations. - - Approval covering filesystem write operations. - - Approval covering an MCP tool. - - Approval covering MCP sampling requests for a server. - - Approval covering writes to long-term memory. - - Approval covering a custom tool. - - Approval covering extension lifecycle operations such as enable, disable, or reload. - - Approval covering an extension's request to access a permission-gated capability. - """ - command_identifiers: list[str] | None = None - """Command identifiers covered by this approval.""" - - server_name: str | None = None - """MCP server name.""" - - tool_name: str | None = None - """MCP tool name, or null to cover every tool on the server. - - Custom tool name. - """ - operation: str | None = None - """Optional operation identifier; when omitted, the approval covers all extension management - operations. - """ - extension_name: str | None = None - """Extension name.""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForSessionApproval': - assert isinstance(obj, dict) - kind = ApprovalKind(obj.get("kind")) - command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) - server_name = from_union([from_str, from_none], obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - operation = from_union([from_str, from_none], obj.get("operation")) - extension_name = from_union([from_str, from_none], obj.get("extensionName")) - return PermissionDecisionApproveForSessionApproval(kind, command_identifiers, server_name, tool_name, operation, extension_name) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(ApprovalKind, self.kind) - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) - if self.server_name is not None: - result["serverName"] = from_union([from_str, from_none], self.server_name) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.operation is not None: - result["operation"] = from_union([from_str, from_none], self.operation) - if self.extension_name is not None: - result["extensionName"] = from_union([from_str, from_none], self.extension_name) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionsLocationsAddToolApprovalDetails: - """Tool approval to persist and apply - - Schema for the `PermissionsLocationsAddToolApprovalDetailsCommands` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsRead` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsWrite` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsMcp` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsMcpSampling` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsMemory` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsCustomTool` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsExtensionManagement` type. - - Schema for the `PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess` type. - """ - kind: ApprovalKind - """Approval scoped to specific command identifiers. - - Approval covering read-only filesystem operations. - - Approval covering filesystem write operations. - - Approval covering an MCP tool. - - Approval covering MCP sampling requests for a server. - - Approval covering writes to long-term memory. - - Approval covering a custom tool. - - Approval covering extension lifecycle operations such as enable, disable, or reload. - - Approval covering an extension's request to access a permission-gated capability. - """ - command_identifiers: list[str] | None = None - """Command identifiers covered by this approval.""" - - server_name: str | None = None - """MCP server name.""" - - tool_name: str | None = None - """MCP tool name, or null to cover every tool on the server. - - Custom tool name. - """ - operation: str | None = None - """Optional operation identifier; when omitted, the approval covers all extension management - operations. - """ - extension_name: str | None = None - """Extension name.""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionsLocationsAddToolApprovalDetails': - assert isinstance(obj, dict) - kind = ApprovalKind(obj.get("kind")) - command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) - server_name = from_union([from_str, from_none], obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - operation = from_union([from_str, from_none], obj.get("operation")) - extension_name = from_union([from_str, from_none], obj.get("extensionName")) - return PermissionsLocationsAddToolApprovalDetails(kind, command_identifiers, server_name, tool_name, operation, extension_name) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(ApprovalKind, self.kind) - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) - if self.server_name is not None: - result["serverName"] = from_union([from_str, from_none], self.server_name) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.operation is not None: - result["operation"] = from_union([from_str, from_none], self.operation) - if self.extension_name is not None: - result["extensionName"] = from_union([from_str, from_none], self.extension_name) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ExternalToolTextResultForLlm: - """Expanded external tool result payload""" - - text_result_for_llm: str - """Text result returned to the model""" - - binary_results_for_llm: list[ExternalToolTextResultForLlmBinaryResultsForLlm] | None = None - """Base64-encoded binary results returned to the model""" - - contents: list[ExternalToolTextResultForLlmContent] | None = None - """Structured content blocks from the tool""" +class HandlePendingToolCallRequest: + """Pending external tool call request ID, with the tool result or an error describing why it + failed. + """ + request_id: str + """Request ID of the pending tool call""" error: str | None = None - """Optional error message for failed executions""" - - result_type: str | None = None - """Execution outcome classification. Optional for back-compat; normalized to 'success' (or - 'failure' when error is present) when missing or unrecognized. - """ - session_log: str | None = None - """Detailed log content for timeline display""" + """Error message if the tool call failed""" - tool_telemetry: dict[str, Any] | None = None - """Optional tool-specific telemetry""" + result: ExternalToolTextResultForLlm | str | None = None + """Tool call result (string or expanded result object)""" @staticmethod - def from_dict(obj: Any) -> 'ExternalToolTextResultForLlm': + def from_dict(obj: Any) -> 'HandlePendingToolCallRequest': assert isinstance(obj, dict) - text_result_for_llm = from_str(obj.get("textResultForLlm")) - binary_results_for_llm = from_union([lambda x: from_list(ExternalToolTextResultForLlmBinaryResultsForLlm.from_dict, x), from_none], obj.get("binaryResultsForLlm")) - contents = from_union([lambda x: from_list(ExternalToolTextResultForLlmContent.from_dict, x), from_none], obj.get("contents")) + request_id = from_str(obj.get("requestId")) error = from_union([from_str, from_none], obj.get("error")) - result_type = from_union([from_str, from_none], obj.get("resultType")) - session_log = from_union([from_str, from_none], obj.get("sessionLog")) - tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry")) - return ExternalToolTextResultForLlm(text_result_for_llm, binary_results_for_llm, contents, error, result_type, session_log, tool_telemetry) + result = from_union([ExternalToolTextResultForLlm.from_dict, from_str, from_none], obj.get("result")) + return HandlePendingToolCallRequest(request_id, error, result) def to_dict(self) -> dict: result: dict = {} - result["textResultForLlm"] = from_str(self.text_result_for_llm) - if self.binary_results_for_llm is not None: - result["binaryResultsForLlm"] = from_union([lambda x: from_list(lambda x: to_class(ExternalToolTextResultForLlmBinaryResultsForLlm, x), x), from_none], self.binary_results_for_llm) - if self.contents is not None: - result["contents"] = from_union([lambda x: from_list(lambda x: to_class(ExternalToolTextResultForLlmContent, x), x), from_none], self.contents) + result["requestId"] = from_str(self.request_id) if self.error is not None: result["error"] = from_union([from_str, from_none], self.error) - if self.result_type is not None: - result["resultType"] = from_union([from_str, from_none], self.result_type) - if self.session_log is not None: - result["sessionLog"] = from_union([from_str, from_none], self.session_log) - if self.tool_telemetry is not None: - result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry) + if self.result is not None: + result["result"] = from_union([lambda x: to_class(ExternalToolTextResultForLlm, x), from_str, from_none], self.result) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -13476,118 +12950,16 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SendRequest: - """Parameters for sending a user message to the session""" - - prompt: str - """The user message text""" - - agent_mode: SendAgentMode | None = None - """The UI mode the agent was in when this message was sent. Defaults to the session's - current mode. - """ - attachments: list[SendAttachment] | None = None - """Optional attachments (files, directories, selections, blobs, GitHub references) to - include with the message - """ - billable: bool | None = None - """If false, this message will not trigger a Premium Request Unit charge. User messages - default to billable. - """ - display_prompt: str | None = None - """If provided, this is shown in the timeline instead of `prompt`""" - - mode: SendMode | None = None - """How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` - interjects during an in-progress turn. - """ - prepend: bool | None = None - """If true, adds the message to the front of the queue instead of the end""" - - request_headers: dict[str, str] | None = None - """Custom HTTP headers to include in outbound model requests for this turn. Merged with - session-level provider headers; per-turn headers augment and overwrite session-level - headers with the same key. - """ - required_tool: str | None = None - """If set, the request will fail if the named tool is not available when this message is - among the user messages at the start of the current exchange - """ - source: Any = None - """Optional provenance tag copied to the resulting user.message event. Supported values are - `system`, `command-*`, and `schedule-*`. - """ - traceparent: str | None = None - """W3C Trace Context traceparent header for distributed tracing of this agent turn""" - - tracestate: str | None = None - """W3C Trace Context tracestate header for distributed tracing""" +class TasksGetProgressResult: + """Progress information for the task, or null when no task with that ID is tracked.""" - wait: bool | None = None - """If true, await completion of the agentic loop for this message before returning. Defaults - to false (fire-and-forget). When true, the result still contains the same `messageId`; - the caller can rely on the agent having processed the message before the call resolves. + progress: TaskProgress | None = None + """Progress information for the task, discriminated by type. Returns null when no task with + this ID is currently tracked. """ @staticmethod - def from_dict(obj: Any) -> 'SendRequest': - assert isinstance(obj, dict) - prompt = from_str(obj.get("prompt")) - agent_mode = from_union([SendAgentMode, from_none], obj.get("agentMode")) - attachments = from_union([lambda x: from_list(SendAttachment.from_dict, x), from_none], obj.get("attachments")) - billable = from_union([from_bool, from_none], obj.get("billable")) - display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) - mode = from_union([SendMode, from_none], obj.get("mode")) - prepend = from_union([from_bool, from_none], obj.get("prepend")) - request_headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("requestHeaders")) - required_tool = from_union([from_str, from_none], obj.get("requiredTool")) - source = obj.get("source") - traceparent = from_union([from_str, from_none], obj.get("traceparent")) - tracestate = from_union([from_str, from_none], obj.get("tracestate")) - wait = from_union([from_bool, from_none], obj.get("wait")) - return SendRequest(prompt, agent_mode, attachments, billable, display_prompt, mode, prepend, request_headers, required_tool, source, traceparent, tracestate, wait) - - def to_dict(self) -> dict: - result: dict = {} - result["prompt"] = from_str(self.prompt) - if self.agent_mode is not None: - result["agentMode"] = from_union([lambda x: to_enum(SendAgentMode, x), from_none], self.agent_mode) - if self.attachments is not None: - result["attachments"] = from_union([lambda x: from_list(lambda x: to_class(SendAttachment, x), x), from_none], self.attachments) - if self.billable is not None: - result["billable"] = from_union([from_bool, from_none], self.billable) - if self.display_prompt is not None: - result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) - if self.mode is not None: - result["mode"] = from_union([lambda x: to_enum(SendMode, x), from_none], self.mode) - if self.prepend is not None: - result["prepend"] = from_union([from_bool, from_none], self.prepend) - if self.request_headers is not None: - result["requestHeaders"] = from_union([lambda x: from_dict(from_str, x), from_none], self.request_headers) - if self.required_tool is not None: - result["requiredTool"] = from_union([from_str, from_none], self.required_tool) - if self.source is not None: - result["source"] = self.source - if self.traceparent is not None: - result["traceparent"] = from_union([from_str, from_none], self.traceparent) - if self.tracestate is not None: - result["tracestate"] = from_union([from_str, from_none], self.tracestate) - if self.wait is not None: - result["wait"] = from_union([from_bool, from_none], self.wait) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class TasksGetProgressResult: - """Progress information for the task, or null when no task with that ID is tracked.""" - - progress: TaskProgress | None = None - """Progress information for the task, discriminated by type. Returns null when no task with - this ID is currently tracked. - """ - - @staticmethod - def from_dict(obj: Any) -> 'TasksGetProgressResult': + def from_dict(obj: Any) -> 'TasksGetProgressResult': assert isinstance(obj, dict) progress = from_union([TaskProgress.from_dict, from_none], obj.get("progress")) return TasksGetProgressResult(progress) @@ -13628,231 +13000,6 @@ def to_dict(self) -> dict: result["required"] = from_union([lambda x: from_list(from_str, x), from_none], self.required) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class AuthInfo: - """The new auth credentials to install on the session. When omitted or `undefined`, the call - is a no-op and the session's existing credentials are preserved. The runtime stores the - value verbatim and uses it for outbound model/API requests; it does NOT re-validate or - re-fetch the associated Copilot user response. Several variants carry secret material; - treat this method's params as containing secrets at rest and in transit. - - Schema for the `HMACAuthInfo` type. - - Schema for the `EnvAuthInfo` type. - - Schema for the `TokenAuthInfo` type. - - Schema for the `CopilotApiTokenAuthInfo` type. - - Schema for the `UserAuthInfo` type. - - Schema for the `GhCliAuthInfo` type. - - Schema for the `ApiKeyAuthInfo` type. - """ - host: str - """Authentication host. HMAC auth always targets the public GitHub host. - - Authentication host (e.g. https://github.com or a GHES host). - - Authentication host. - - Authentication host (always the public GitHub host). - """ - type: AuthInfoType - """HMAC-based authentication used by GitHub-internal services. - - Personal access token (PAT) or server-to-server token sourced from an environment - variable. - - SDK-side token authentication; the host configured the token directly via the SDK. - - Direct Copilot API authentication via the `GITHUB_COPILOT_API_TOKEN` + `COPILOT_API_URL` - environment-variable pair. The token itself is read from the environment by the runtime, - not carried in this struct. - - OAuth user authentication. The token itself is held in the runtime's secret token store - (keyed by host+login) and is NOT carried in this struct. - - Authentication via the `gh` CLI's saved credentials. - - API-key authentication for non-GitHub LLM providers (e.g. when running BYOM-style). - """ - copilot_user: CopilotUserResponse | None = None - """Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the - GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this - verbatim and does not re-fetch when set. - """ - hmac: str | None = None - """HMAC secret used to sign requests.""" - - env_var: str | None = None - """Name of the environment variable the token was sourced from.""" - - login: str | None = None - """User login associated with the token. Undefined for server-to-server tokens (those - starting with `ghs_`). - - OAuth user login. - - User login as reported by `gh auth status`. - """ - token: str | None = None - """The token value itself. Treat as a secret. - - The token returned by `gh auth token`. Treat as a secret. - """ - api_key: str | None = None - """The API key. Treat as a secret.""" - - @staticmethod - def from_dict(obj: Any) -> 'AuthInfo': - assert isinstance(obj, dict) - host = from_str(obj.get("host")) - type = AuthInfoType(obj.get("type")) - copilot_user = from_union([CopilotUserResponse.from_dict, from_none], obj.get("copilotUser")) - hmac = from_union([from_str, from_none], obj.get("hmac")) - env_var = from_union([from_str, from_none], obj.get("envVar")) - login = from_union([from_str, from_none], obj.get("login")) - token = from_union([from_str, from_none], obj.get("token")) - api_key = from_union([from_str, from_none], obj.get("apiKey")) - return AuthInfo(host, type, copilot_user, hmac, env_var, login, token, api_key) - - def to_dict(self) -> dict: - result: dict = {} - result["host"] = from_str(self.host) - result["type"] = to_enum(AuthInfoType, self.type) - if self.copilot_user is not None: - result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) - if self.hmac is not None: - result["hmac"] = from_union([from_str, from_none], self.hmac) - if self.env_var is not None: - result["envVar"] = from_union([from_str, from_none], self.env_var) - if self.login is not None: - result["login"] = from_union([from_str, from_none], self.login) - if self.token is not None: - result["token"] = from_union([from_str, from_none], self.token) - if self.api_key is not None: - result["apiKey"] = from_union([from_str, from_none], self.api_key) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionDecisionApproveForLocation: - """Schema for the `PermissionDecisionApproveForLocation` type.""" - - approval: PermissionDecisionApproveForLocationApproval - """Approval to persist for this location""" - - kind: PermissionDecisionApproveForLocationKind - """Approve and persist for this project location""" - - location_key: str - """Location key (git root or cwd) to persist the approval to""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForLocation': - assert isinstance(obj, dict) - approval = PermissionDecisionApproveForLocationApproval.from_dict(obj.get("approval")) - kind = PermissionDecisionApproveForLocationKind(obj.get("kind")) - location_key = from_str(obj.get("locationKey")) - return PermissionDecisionApproveForLocation(approval, kind, location_key) - - def to_dict(self) -> dict: - result: dict = {} - result["approval"] = to_class(PermissionDecisionApproveForLocationApproval, self.approval) - result["kind"] = to_enum(PermissionDecisionApproveForLocationKind, self.kind) - result["locationKey"] = from_str(self.location_key) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionDecisionApproveForSession: - """Schema for the `PermissionDecisionApproveForSession` type.""" - - kind: PermissionDecisionApproveForSessionKind - """Approve and remember for the rest of the session""" - - approval: PermissionDecisionApproveForSessionApproval | None = None - """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts)""" - - domain: str | None = None - """URL domain to approve for the rest of the session (URL prompts only)""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionApproveForSession': - assert isinstance(obj, dict) - kind = PermissionDecisionApproveForSessionKind(obj.get("kind")) - approval = from_union([PermissionDecisionApproveForSessionApproval.from_dict, from_none], obj.get("approval")) - domain = from_union([from_str, from_none], obj.get("domain")) - return PermissionDecisionApproveForSession(kind, approval, domain) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionApproveForSessionKind, self.kind) - if self.approval is not None: - result["approval"] = from_union([lambda x: to_class(PermissionDecisionApproveForSessionApproval, x), from_none], self.approval) - if self.domain is not None: - result["domain"] = from_union([from_str, from_none], self.domain) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionLocationAddToolApprovalParams: - """Location-scoped tool approval to persist.""" - - approval: PermissionsLocationsAddToolApprovalDetails - """Tool approval to persist and apply""" - - location_key: str - """Location key (git root or cwd) to persist the approval to""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionLocationAddToolApprovalParams': - assert isinstance(obj, dict) - approval = PermissionsLocationsAddToolApprovalDetails.from_dict(obj.get("approval")) - location_key = from_str(obj.get("locationKey")) - return PermissionLocationAddToolApprovalParams(approval, location_key) - - def to_dict(self) -> dict: - result: dict = {} - result["approval"] = to_class(PermissionsLocationsAddToolApprovalDetails, self.approval) - result["locationKey"] = from_str(self.location_key) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class HandlePendingToolCallRequest: - """Pending external tool call request ID, with the tool result or an error describing why it - failed. - """ - request_id: str - """Request ID of the pending tool call""" - - error: str | None = None - """Error message if the tool call failed""" - - result: ExternalToolTextResultForLlm | str | None = None - """Tool call result (string or expanded result object)""" - - @staticmethod - def from_dict(obj: Any) -> 'HandlePendingToolCallRequest': - assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - error = from_union([from_str, from_none], obj.get("error")) - result = from_union([ExternalToolTextResultForLlm.from_dict, from_str, from_none], obj.get("result")) - return HandlePendingToolCallRequest(request_id, error, result) - - def to_dict(self) -> dict: - result: dict = {} - result["requestId"] = from_str(self.request_id) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.result is not None: - result["result"] = from_union([lambda x: to_class(ExternalToolTextResultForLlm, x), from_str, from_none], self.result) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionsSetAdditionalPluginsRequest: @@ -14078,263 +13225,63 @@ def to_dict(self) -> dict: if self.feature_flags is not None: result["featureFlags"] = from_union([lambda x: from_dict(from_bool, x), from_none], self.feature_flags) if self.installed_plugins is not None: - result["installedPlugins"] = from_union([lambda x: from_list(lambda x: to_class(SessionInstalledPlugin, x), x), from_none], self.installed_plugins) - if self.integration_id is not None: - result["integrationId"] = from_union([from_str, from_none], self.integration_id) - if self.is_experimental_mode is not None: - result["isExperimentalMode"] = from_union([from_bool, from_none], self.is_experimental_mode) - if self.log_interactive_shells is not None: - result["logInteractiveShells"] = from_union([from_bool, from_none], self.log_interactive_shells) - if self.lsp_client_name is not None: - result["lspClientName"] = from_union([from_str, from_none], self.lsp_client_name) - if self.manage_schedule_enabled is not None: - result["manageScheduleEnabled"] = from_union([from_bool, from_none], self.manage_schedule_enabled) - if self.model is not None: - result["model"] = from_union([from_str, from_none], self.model) - if self.provider is not None: - result["provider"] = self.provider - if self.reasoning_effort is not None: - result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) - if self.running_in_interactive_mode is not None: - result["runningInInteractiveMode"] = from_union([from_bool, from_none], self.running_in_interactive_mode) - if self.sandbox_config is not None: - result["sandboxConfig"] = self.sandbox_config - if self.shell_init_profile is not None: - result["shellInitProfile"] = from_union([from_str, from_none], self.shell_init_profile) - if self.shell_process_flags is not None: - result["shellProcessFlags"] = from_union([lambda x: from_list(from_str, x), from_none], self.shell_process_flags) - if self.skill_directories is not None: - result["skillDirectories"] = from_union([lambda x: from_list(from_str, x), from_none], self.skill_directories) - if self.skip_custom_instructions is not None: - result["skipCustomInstructions"] = from_union([from_bool, from_none], self.skip_custom_instructions) - if self.trajectory_file is not None: - result["trajectoryFile"] = from_union([from_str, from_none], self.trajectory_file) - if self.working_directory is not None: - result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class UIElicitationRequest: - """Prompt message and JSON schema describing the form fields to elicit from the user.""" - - message: str - """Message describing what information is needed from the user""" - - requested_schema: UIElicitationSchema - """JSON Schema describing the form fields to present to the user""" - - @staticmethod - def from_dict(obj: Any) -> 'UIElicitationRequest': - assert isinstance(obj, dict) - message = from_str(obj.get("message")) - requested_schema = UIElicitationSchema.from_dict(obj.get("requestedSchema")) - return UIElicitationRequest(message, requested_schema) - - def to_dict(self) -> dict: - result: dict = {} - result["message"] = from_str(self.message) - result["requestedSchema"] = to_class(UIElicitationSchema, self.requested_schema) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SessionSetCredentialsParams: - """New auth credentials to install on the session. Omit to leave credentials unchanged.""" - - credentials: AuthInfo | None = None - """The new auth credentials to install on the session. When omitted or `undefined`, the call - is a no-op and the session's existing credentials are preserved. The runtime stores the - value verbatim and uses it for outbound model/API requests; it does NOT re-validate or - re-fetch the associated Copilot user response. Several variants carry secret material; - treat this method's params as containing secrets at rest and in transit. - """ - - @staticmethod - def from_dict(obj: Any) -> 'SessionSetCredentialsParams': - assert isinstance(obj, dict) - credentials = from_union([AuthInfo.from_dict, from_none], obj.get("credentials")) - return SessionSetCredentialsParams(credentials) - - def to_dict(self) -> dict: - result: dict = {} - if self.credentials is not None: - result["credentials"] = from_union([lambda x: to_class(AuthInfo, x), from_none], self.credentials) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class PermissionDecision: - """The client's response to the pending permission prompt - - Schema for the `PermissionDecisionApproveOnce` type. - - Schema for the `PermissionDecisionApproveForSession` type. - - Schema for the `PermissionDecisionApproveForLocation` type. - - Schema for the `PermissionDecisionApprovePermanently` type. - - Schema for the `PermissionDecisionReject` type. - - Schema for the `PermissionDecisionUserNotAvailable` type. - - Schema for the `PermissionDecisionApproved` type. - - Schema for the `PermissionDecisionApprovedForSession` type. - - Schema for the `PermissionDecisionApprovedForLocation` type. - - Schema for the `PermissionDecisionCancelled` type. - - Schema for the `PermissionDecisionDeniedByRules` type. - - Schema for the `PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser` type. - - Schema for the `PermissionDecisionDeniedInteractivelyByUser` type. - - Schema for the `PermissionDecisionDeniedByContentExclusionPolicy` type. - - Schema for the `PermissionDecisionDeniedByPermissionRequestHook` type. - """ - kind: PermissionDecisionKind - """Approve this single request only - - Approve and remember for the rest of the session - - Approve and persist for this project location - - Approve and persist across sessions (URL prompts only) - - Reject the request - - No user is available to confirm the request - - The permission request was approved - - Approved and remembered for the rest of the session - - Approved and persisted for this project location - - The permission request was cancelled before a response was used - - Denied because approval rules explicitly blocked it - - Denied because no approval rule matched and user confirmation was unavailable - - Denied by the user during an interactive prompt - - Denied by the organization's content exclusion policy - - Denied by a permission request hook registered by an extension or plugin - """ - approval: PermissionDecisionApproveForIonApproval | None = None - """Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) - - Approval to persist for this location - - The approval to add as a session-scoped rule - - The approval to persist for this location - """ - domain: str | None = None - """URL domain to approve for the rest of the session (URL prompts only) - - URL domain to approve permanently - """ - location_key: str | None = None - """Location key (git root or cwd) to persist the approval to - - The location key (git root or cwd) to persist the approval to - """ - feedback: str | None = None - """Optional feedback explaining the rejection - - Optional feedback from the user explaining the denial - """ - reason: str | None = None - """Optional explanation of why the request was cancelled""" - - rules: list[PermissionRule] | None = None - """Rules that denied the request""" - - force_reject: bool | None = None - """Whether to force-reject the current agent turn""" - - message: str | None = None - """Human-readable explanation of why the path was excluded - - Optional message from the hook explaining the denial - """ - path: str | None = None - """File path that triggered the exclusion""" - - interrupt: bool | None = None - """Whether to interrupt the current agent turn""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionDecision': - assert isinstance(obj, dict) - kind = PermissionDecisionKind(obj.get("kind")) - approval = from_union([PermissionDecisionApproveForIonApproval.from_dict, from_none], obj.get("approval")) - domain = from_union([from_str, from_none], obj.get("domain")) - location_key = from_union([from_str, from_none], obj.get("locationKey")) - feedback = from_union([from_str, from_none], obj.get("feedback")) - reason = from_union([from_str, from_none], obj.get("reason")) - rules = from_union([lambda x: from_list(PermissionRule.from_dict, x), from_none], obj.get("rules")) - force_reject = from_union([from_bool, from_none], obj.get("forceReject")) - message = from_union([from_str, from_none], obj.get("message")) - path = from_union([from_str, from_none], obj.get("path")) - interrupt = from_union([from_bool, from_none], obj.get("interrupt")) - return PermissionDecision(kind, approval, domain, location_key, feedback, reason, rules, force_reject, message, path, interrupt) - - def to_dict(self) -> dict: - result: dict = {} - result["kind"] = to_enum(PermissionDecisionKind, self.kind) - if self.approval is not None: - result["approval"] = from_union([lambda x: to_class(PermissionDecisionApproveForIonApproval, x), from_none], self.approval) - if self.domain is not None: - result["domain"] = from_union([from_str, from_none], self.domain) - if self.location_key is not None: - result["locationKey"] = from_union([from_str, from_none], self.location_key) - if self.feedback is not None: - result["feedback"] = from_union([from_str, from_none], self.feedback) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.rules is not None: - result["rules"] = from_union([lambda x: from_list(lambda x: to_class(PermissionRule, x), x), from_none], self.rules) - if self.force_reject is not None: - result["forceReject"] = from_union([from_bool, from_none], self.force_reject) - if self.message is not None: - result["message"] = from_union([from_str, from_none], self.message) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.interrupt is not None: - result["interrupt"] = from_union([from_bool, from_none], self.interrupt) + result["installedPlugins"] = from_union([lambda x: from_list(lambda x: to_class(SessionInstalledPlugin, x), x), from_none], self.installed_plugins) + if self.integration_id is not None: + result["integrationId"] = from_union([from_str, from_none], self.integration_id) + if self.is_experimental_mode is not None: + result["isExperimentalMode"] = from_union([from_bool, from_none], self.is_experimental_mode) + if self.log_interactive_shells is not None: + result["logInteractiveShells"] = from_union([from_bool, from_none], self.log_interactive_shells) + if self.lsp_client_name is not None: + result["lspClientName"] = from_union([from_str, from_none], self.lsp_client_name) + if self.manage_schedule_enabled is not None: + result["manageScheduleEnabled"] = from_union([from_bool, from_none], self.manage_schedule_enabled) + if self.model is not None: + result["model"] = from_union([from_str, from_none], self.model) + if self.provider is not None: + result["provider"] = self.provider + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) + if self.running_in_interactive_mode is not None: + result["runningInInteractiveMode"] = from_union([from_bool, from_none], self.running_in_interactive_mode) + if self.sandbox_config is not None: + result["sandboxConfig"] = self.sandbox_config + if self.shell_init_profile is not None: + result["shellInitProfile"] = from_union([from_str, from_none], self.shell_init_profile) + if self.shell_process_flags is not None: + result["shellProcessFlags"] = from_union([lambda x: from_list(from_str, x), from_none], self.shell_process_flags) + if self.skill_directories is not None: + result["skillDirectories"] = from_union([lambda x: from_list(from_str, x), from_none], self.skill_directories) + if self.skip_custom_instructions is not None: + result["skipCustomInstructions"] = from_union([from_bool, from_none], self.skip_custom_instructions) + if self.trajectory_file is not None: + result["trajectoryFile"] = from_union([from_str, from_none], self.trajectory_file) + if self.working_directory is not None: + result["workingDirectory"] = from_union([from_str, from_none], self.working_directory) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class PermissionDecisionRequest: - """Pending permission request ID and the decision to apply (approve/reject and scope).""" +class UIElicitationRequest: + """Prompt message and JSON schema describing the form fields to elicit from the user.""" - request_id: str - """Request ID of the pending permission request""" + message: str + """Message describing what information is needed from the user""" - result: PermissionDecision - """The client's response to the pending permission prompt""" + requested_schema: UIElicitationSchema + """JSON Schema describing the form fields to present to the user""" @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionRequest': + def from_dict(obj: Any) -> 'UIElicitationRequest': assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - result = PermissionDecision.from_dict(obj.get("result")) - return PermissionDecisionRequest(request_id, result) + message = from_str(obj.get("message")) + requested_schema = UIElicitationSchema.from_dict(obj.get("requestedSchema")) + return UIElicitationRequest(message, requested_schema) def to_dict(self) -> dict: result: dict = {} - result["requestId"] = from_str(self.request_id) - result["result"] = to_class(PermissionDecision, self.result) + result["message"] = from_str(self.message) + result["requestedSchema"] = to_class(UIElicitationSchema, self.requested_schema) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -14642,7 +13589,7 @@ class TaskAgentInfo: tool_call_id: str """Tool call ID associated with this agent task""" - type: TaskAgentInfoType + type: ClassVar[str] = "agent" """Task kind""" active_started_at: datetime | None = None @@ -14687,7 +13634,6 @@ def from_dict(obj: Any) -> 'TaskAgentInfo': started_at = from_datetime(obj.get("startedAt")) status = TaskStatus(obj.get("status")) tool_call_id = from_str(obj.get("toolCallId")) - type = TaskAgentInfoType(obj.get("type")) active_started_at = from_union([from_datetime, from_none], obj.get("activeStartedAt")) active_time_ms = from_union([from_int, from_none], obj.get("activeTimeMs")) can_promote_to_background = from_union([from_bool, from_none], obj.get("canPromoteToBackground")) @@ -14698,7 +13644,7 @@ def from_dict(obj: Any) -> 'TaskAgentInfo': latest_response = from_union([from_str, from_none], obj.get("latestResponse")) model = from_union([from_str, from_none], obj.get("model")) result = from_union([from_str, from_none], obj.get("result")) - return TaskAgentInfo(agent_type, description, id, prompt, started_at, status, tool_call_id, type, active_started_at, active_time_ms, can_promote_to_background, completed_at, error, execution_mode, idle_since, latest_response, model, result) + return TaskAgentInfo(agent_type, description, id, prompt, started_at, status, tool_call_id, active_started_at, active_time_ms, can_promote_to_background, completed_at, error, execution_mode, idle_since, latest_response, model, result) def to_dict(self) -> dict: result: dict = {} @@ -14709,157 +13655,11 @@ def to_dict(self) -> dict: result["startedAt"] = self.started_at.isoformat() result["status"] = to_enum(TaskStatus, self.status) result["toolCallId"] = from_str(self.tool_call_id) - result["type"] = to_enum(TaskAgentInfoType, self.type) - if self.active_started_at is not None: - result["activeStartedAt"] = from_union([lambda x: x.isoformat(), from_none], self.active_started_at) - if self.active_time_ms is not None: - result["activeTimeMs"] = from_union([from_int, from_none], self.active_time_ms) - if self.can_promote_to_background is not None: - result["canPromoteToBackground"] = from_union([from_bool, from_none], self.can_promote_to_background) - if self.completed_at is not None: - result["completedAt"] = from_union([lambda x: x.isoformat(), from_none], self.completed_at) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.execution_mode is not None: - result["executionMode"] = from_union([lambda x: to_enum(TaskExecutionMode, x), from_none], self.execution_mode) - if self.idle_since is not None: - result["idleSince"] = from_union([lambda x: x.isoformat(), from_none], self.idle_since) - if self.latest_response is not None: - result["latestResponse"] = from_union([from_str, from_none], self.latest_response) - if self.model is not None: - result["model"] = from_union([from_str, from_none], self.model) - if self.result is not None: - result["result"] = from_union([from_str, from_none], self.result) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class TaskInfo: - """Schema for the `TaskInfo` type. - - The first sync-waiting task (agent first, then shell) that can currently be promoted to - background mode. Omitted if no such task exists. The returned task is guaranteed to have - executionMode='sync' and canPromoteToBackground=true at the time of the call. - - The promoted task as it now exists in background mode, omitted if no promotable task was - waiting. Atomic operation: avoids the race window of getCurrentPromotable + - promoteToBackground. - - Schema for the `TaskAgentInfo` type. - - Schema for the `TaskShellInfo` type. - """ - description: str - """Short description of the task""" - - id: str - """Unique task identifier""" - - started_at: datetime - """ISO 8601 timestamp when the task was started""" - - status: TaskStatus - """Current lifecycle status of the task""" - - type: TaskInfoType - """Task kind""" - - active_started_at: datetime | None = None - """ISO 8601 timestamp when the current active period began""" - - active_time_ms: int | None = None - """Accumulated active execution time in milliseconds""" - - agent_type: str | None = None - """Type of agent running this task""" - - can_promote_to_background: bool | None = None - """Whether the task is currently in the original sync wait and can be moved to background - mode. False once it is already backgrounded, idle, finished, or no longer has a - promotable sync waiter. - - Whether this shell task can be promoted to background mode - """ - completed_at: datetime | None = None - """ISO 8601 timestamp when the task finished""" - - error: str | None = None - """Error message when the task failed""" - - execution_mode: TaskExecutionMode | None = None - """Whether task execution is synchronously awaited or managed in the background""" - - idle_since: datetime | None = None - """ISO 8601 timestamp when the agent entered idle state""" - - latest_response: str | None = None - """Most recent response text from the agent""" - - model: str | None = None - """Model used for the task when specified""" - - prompt: str | None = None - """Prompt passed to the agent""" - - result: str | None = None - """Result text from the task when available""" - - tool_call_id: str | None = None - """Tool call ID associated with this agent task""" - - attachment_mode: TaskShellInfoAttachmentMode | None = None - """Whether the shell runs inside a managed PTY session or as an independent background - process - """ - command: str | None = None - """Command being executed""" - - log_path: str | None = None - """Path to the detached shell log, when available""" - - pid: int | None = None - """Process ID when available""" - - @staticmethod - def from_dict(obj: Any) -> 'TaskInfo': - assert isinstance(obj, dict) - description = from_str(obj.get("description")) - id = from_str(obj.get("id")) - started_at = from_datetime(obj.get("startedAt")) - status = TaskStatus(obj.get("status")) - type = TaskInfoType(obj.get("type")) - active_started_at = from_union([from_datetime, from_none], obj.get("activeStartedAt")) - active_time_ms = from_union([from_int, from_none], obj.get("activeTimeMs")) - agent_type = from_union([from_str, from_none], obj.get("agentType")) - can_promote_to_background = from_union([from_bool, from_none], obj.get("canPromoteToBackground")) - completed_at = from_union([from_datetime, from_none], obj.get("completedAt")) - error = from_union([from_str, from_none], obj.get("error")) - execution_mode = from_union([TaskExecutionMode, from_none], obj.get("executionMode")) - idle_since = from_union([from_datetime, from_none], obj.get("idleSince")) - latest_response = from_union([from_str, from_none], obj.get("latestResponse")) - model = from_union([from_str, from_none], obj.get("model")) - prompt = from_union([from_str, from_none], obj.get("prompt")) - result = from_union([from_str, from_none], obj.get("result")) - tool_call_id = from_union([from_str, from_none], obj.get("toolCallId")) - attachment_mode = from_union([TaskShellInfoAttachmentMode, from_none], obj.get("attachmentMode")) - command = from_union([from_str, from_none], obj.get("command")) - log_path = from_union([from_str, from_none], obj.get("logPath")) - pid = from_union([from_int, from_none], obj.get("pid")) - return TaskInfo(description, id, started_at, status, type, active_started_at, active_time_ms, agent_type, can_promote_to_background, completed_at, error, execution_mode, idle_since, latest_response, model, prompt, result, tool_call_id, attachment_mode, command, log_path, pid) - - def to_dict(self) -> dict: - result: dict = {} - result["description"] = from_str(self.description) - result["id"] = from_str(self.id) - result["startedAt"] = self.started_at.isoformat() - result["status"] = to_enum(TaskStatus, self.status) - result["type"] = to_enum(TaskInfoType, self.type) + result["type"] = self.type if self.active_started_at is not None: result["activeStartedAt"] = from_union([lambda x: x.isoformat(), from_none], self.active_started_at) if self.active_time_ms is not None: result["activeTimeMs"] = from_union([from_int, from_none], self.active_time_ms) - if self.agent_type is not None: - result["agentType"] = from_union([from_str, from_none], self.agent_type) if self.can_promote_to_background is not None: result["canPromoteToBackground"] = from_union([from_bool, from_none], self.can_promote_to_background) if self.completed_at is not None: @@ -14874,86 +13674,8 @@ def to_dict(self) -> dict: result["latestResponse"] = from_union([from_str, from_none], self.latest_response) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) - if self.prompt is not None: - result["prompt"] = from_union([from_str, from_none], self.prompt) if self.result is not None: result["result"] = from_union([from_str, from_none], self.result) - if self.tool_call_id is not None: - result["toolCallId"] = from_union([from_str, from_none], self.tool_call_id) - if self.attachment_mode is not None: - result["attachmentMode"] = from_union([lambda x: to_enum(TaskShellInfoAttachmentMode, x), from_none], self.attachment_mode) - if self.command is not None: - result["command"] = from_union([from_str, from_none], self.command) - if self.log_path is not None: - result["logPath"] = from_union([from_str, from_none], self.log_path) - if self.pid is not None: - result["pid"] = from_union([from_int, from_none], self.pid) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class TaskList: - """Background tasks currently tracked by the session.""" - - tasks: list[TaskInfo] - """Currently tracked tasks""" - - @staticmethod - def from_dict(obj: Any) -> 'TaskList': - assert isinstance(obj, dict) - tasks = from_list(TaskInfo.from_dict, obj.get("tasks")) - return TaskList(tasks) - - def to_dict(self) -> dict: - result: dict = {} - result["tasks"] = from_list(lambda x: to_class(TaskInfo, x), self.tasks) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class TasksGetCurrentPromotableResult: - """The first sync-waiting task that can currently be promoted to background mode.""" - - task: TaskInfo | None = None - """The first sync-waiting task (agent first, then shell) that can currently be promoted to - background mode. Omitted if no such task exists. The returned task is guaranteed to have - executionMode='sync' and canPromoteToBackground=true at the time of the call. - """ - - @staticmethod - def from_dict(obj: Any) -> 'TasksGetCurrentPromotableResult': - assert isinstance(obj, dict) - task = from_union([TaskInfo.from_dict, from_none], obj.get("task")) - return TasksGetCurrentPromotableResult(task) - - def to_dict(self) -> dict: - result: dict = {} - if self.task is not None: - result["task"] = from_union([lambda x: to_class(TaskInfo, x), from_none], self.task) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class TasksPromoteCurrentToBackgroundResult: - """The promoted task as it now exists in background mode, omitted if no promotable task was - waiting. - """ - task: TaskInfo | None = None - """The promoted task as it now exists in background mode, omitted if no promotable task was - waiting. Atomic operation: avoids the race window of getCurrentPromotable + - promoteToBackground. - """ - - @staticmethod - def from_dict(obj: Any) -> 'TasksPromoteCurrentToBackgroundResult': - assert isinstance(obj, dict) - task = from_union([TaskInfo.from_dict, from_none], obj.get("task")) - return TasksPromoteCurrentToBackgroundResult(task) - - def to_dict(self) -> dict: - result: dict = {} - if self.task is not None: - result["task"] = from_union([lambda x: to_class(TaskInfo, x), from_none], self.task) return result @dataclass @@ -15449,14 +14171,6 @@ class RPC: usage_metrics_model_metric_usage: UsageMetricsModelMetricUsage usage_metrics_token_detail: UsageMetricsTokenDetail user_auth_info: UserAuthInfo - user_tool_session_approval_commands: UserToolSessionApprovalCommands - user_tool_session_approval_custom_tool: UserToolSessionApprovalCustomTool - user_tool_session_approval_extension_management: UserToolSessionApprovalExtensionManagement - user_tool_session_approval_extension_permission_access: UserToolSessionApprovalExtensionPermissionAccess - user_tool_session_approval_mcp: UserToolSessionApprovalMCP - user_tool_session_approval_memory: UserToolSessionApprovalMemory - user_tool_session_approval_read: UserToolSessionApprovalRead - user_tool_session_approval_write: UserToolSessionApprovalWrite workspaces_checkpoints: WorkspacesCheckpoints workspaces_create_file_request: WorkspacesCreateFileRequest workspaces_get_workspace_result: WorkspacesGetWorkspaceResult @@ -15490,7 +14204,7 @@ def from_dict(obj: Any) -> 'RPC': agent_select_request = AgentSelectRequest.from_dict(obj.get("AgentSelectRequest")) agent_select_result = AgentSelectResult.from_dict(obj.get("AgentSelectResult")) api_key_auth_info = APIKeyAuthInfo.from_dict(obj.get("ApiKeyAuthInfo")) - auth_info = AuthInfo.from_dict(obj.get("AuthInfo")) + auth_info = _load_AuthInfo(obj.get("AuthInfo")) auth_info_type = AuthInfoType(obj.get("AuthInfoType")) command_list = CommandList.from_dict(obj.get("CommandList")) commands_handle_pending_command_request = CommandsHandlePendingCommandRequest.from_dict(obj.get("CommandsHandlePendingCommandRequest")) @@ -15538,7 +14252,7 @@ def from_dict(obj: Any) -> 'RPC': external_tool_text_result_for_llm = ExternalToolTextResultForLlm.from_dict(obj.get("ExternalToolTextResultForLlm")) external_tool_text_result_for_llm_binary_results_for_llm = ExternalToolTextResultForLlmBinaryResultsForLlm.from_dict(obj.get("ExternalToolTextResultForLlmBinaryResultsForLlm")) external_tool_text_result_for_llm_binary_results_for_llm_type = ExternalToolTextResultForLlmBinaryResultsForLlmType(obj.get("ExternalToolTextResultForLlmBinaryResultsForLlmType")) - external_tool_text_result_for_llm_content = ExternalToolTextResultForLlmContent.from_dict(obj.get("ExternalToolTextResultForLlmContent")) + external_tool_text_result_for_llm_content = _load_ExternalToolTextResultForLlmContent(obj.get("ExternalToolTextResultForLlmContent")) external_tool_text_result_for_llm_content_audio = ExternalToolTextResultForLlmContentAudio.from_dict(obj.get("ExternalToolTextResultForLlmContentAudio")) external_tool_text_result_for_llm_content_image = ExternalToolTextResultForLlmContentImage.from_dict(obj.get("ExternalToolTextResultForLlmContentImage")) external_tool_text_result_for_llm_content_resource = ExternalToolTextResultForLlmContentResource.from_dict(obj.get("ExternalToolTextResultForLlmContentResource")) @@ -15651,12 +14365,12 @@ def from_dict(obj: Any) -> 'RPC': options_update_env_value_mode = MCPSetEnvValueModeDetails(obj.get("OptionsUpdateEnvValueMode")) pending_permission_request = PendingPermissionRequest.from_dict(obj.get("PendingPermissionRequest")) pending_permission_request_list = PendingPermissionRequestList.from_dict(obj.get("PendingPermissionRequestList")) - permission_decision = PermissionDecision.from_dict(obj.get("PermissionDecision")) + permission_decision = _load_PermissionDecision(obj.get("PermissionDecision")) permission_decision_approved = PermissionDecisionApproved.from_dict(obj.get("PermissionDecisionApproved")) permission_decision_approved_for_location = PermissionDecisionApprovedForLocation.from_dict(obj.get("PermissionDecisionApprovedForLocation")) permission_decision_approved_for_session = PermissionDecisionApprovedForSession.from_dict(obj.get("PermissionDecisionApprovedForSession")) permission_decision_approve_for_location = PermissionDecisionApproveForLocation.from_dict(obj.get("PermissionDecisionApproveForLocation")) - permission_decision_approve_for_location_approval = PermissionDecisionApproveForLocationApproval.from_dict(obj.get("PermissionDecisionApproveForLocationApproval")) + permission_decision_approve_for_location_approval = _load_PermissionDecisionApproveForLocationApproval(obj.get("PermissionDecisionApproveForLocationApproval")) permission_decision_approve_for_location_approval_commands = PermissionDecisionApproveForLocationApprovalCommands.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalCommands")) permission_decision_approve_for_location_approval_custom_tool = PermissionDecisionApproveForLocationApprovalCustomTool.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalCustomTool")) permission_decision_approve_for_location_approval_extension_management = PermissionDecisionApproveForLocationApprovalExtensionManagement.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalExtensionManagement")) @@ -15667,7 +14381,7 @@ def from_dict(obj: Any) -> 'RPC': permission_decision_approve_for_location_approval_read = PermissionDecisionApproveForLocationApprovalRead.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalRead")) permission_decision_approve_for_location_approval_write = PermissionDecisionApproveForLocationApprovalWrite.from_dict(obj.get("PermissionDecisionApproveForLocationApprovalWrite")) permission_decision_approve_for_session = PermissionDecisionApproveForSession.from_dict(obj.get("PermissionDecisionApproveForSession")) - permission_decision_approve_for_session_approval = PermissionDecisionApproveForSessionApproval.from_dict(obj.get("PermissionDecisionApproveForSessionApproval")) + permission_decision_approve_for_session_approval = _load_PermissionDecisionApproveForSessionApproval(obj.get("PermissionDecisionApproveForSessionApproval")) permission_decision_approve_for_session_approval_commands = PermissionDecisionApproveForSessionApprovalCommands.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalCommands")) permission_decision_approve_for_session_approval_custom_tool = PermissionDecisionApproveForSessionApprovalCustomTool.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalCustomTool")) permission_decision_approve_for_session_approval_extension_management = PermissionDecisionApproveForSessionApprovalExtensionManagement.from_dict(obj.get("PermissionDecisionApproveForSessionApprovalExtensionManagement")) @@ -15712,7 +14426,7 @@ def from_dict(obj: Any) -> 'RPC': permissions_configure_params = PermissionsConfigureParams.from_dict(obj.get("PermissionsConfigureParams")) permissions_configure_result = PermissionsConfigureResult.from_dict(obj.get("PermissionsConfigureResult")) permissions_folder_trust_add_trusted_result = PermissionsFolderTrustAddTrustedResult.from_dict(obj.get("PermissionsFolderTrustAddTrustedResult")) - permissions_locations_add_tool_approval_details = PermissionsLocationsAddToolApprovalDetails.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetails")) + permissions_locations_add_tool_approval_details = _load_PermissionsLocationsAddToolApprovalDetails(obj.get("PermissionsLocationsAddToolApprovalDetails")) permissions_locations_add_tool_approval_details_commands = PermissionsLocationsAddToolApprovalDetailsCommands.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsCommands")) permissions_locations_add_tool_approval_details_custom_tool = PermissionsLocationsAddToolApprovalDetailsCustomTool.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsCustomTool")) permissions_locations_add_tool_approval_details_extension_management = PermissionsLocationsAddToolApprovalDetailsExtensionManagement.from_dict(obj.get("PermissionsLocationsAddToolApprovalDetailsExtensionManagement")) @@ -15749,7 +14463,7 @@ def from_dict(obj: Any) -> 'RPC': plugin_list = PluginList.from_dict(obj.get("PluginList")) queued_command_handled = QueuedCommandHandled.from_dict(obj.get("QueuedCommandHandled")) queued_command_not_handled = QueuedCommandNotHandled.from_dict(obj.get("QueuedCommandNotHandled")) - queued_command_result = QueuedCommandResult.from_dict(obj.get("QueuedCommandResult")) + queued_command_result = _load_QueuedCommandResult(obj.get("QueuedCommandResult")) queue_pending_items = QueuePendingItems.from_dict(obj.get("QueuePendingItems")) queue_pending_items_kind = QueuePendingItemsKind(obj.get("QueuePendingItemsKind")) queue_pending_items_result = QueuePendingItemsResult.from_dict(obj.get("QueuePendingItemsResult")) @@ -15770,7 +14484,7 @@ def from_dict(obj: Any) -> 'RPC': secrets_add_filter_values_request = SecretsAddFilterValuesRequest.from_dict(obj.get("SecretsAddFilterValuesRequest")) secrets_add_filter_values_result = SecretsAddFilterValuesResult.from_dict(obj.get("SecretsAddFilterValuesResult")) send_agent_mode = SendAgentMode(obj.get("SendAgentMode")) - send_attachment = SendAttachment.from_dict(obj.get("SendAttachment")) + send_attachment = _load_SendAttachment(obj.get("SendAttachment")) send_attachment_blob = SendAttachmentBlob.from_dict(obj.get("SendAttachmentBlob")) send_attachment_directory = SendAttachmentDirectory.from_dict(obj.get("SendAttachmentDirectory")) send_attachment_file = SendAttachmentFile.from_dict(obj.get("SendAttachmentFile")) @@ -15888,7 +14602,7 @@ def from_dict(obj: Any) -> 'RPC': slash_command_info = SlashCommandInfo.from_dict(obj.get("SlashCommandInfo")) slash_command_input = SlashCommandInput.from_dict(obj.get("SlashCommandInput")) slash_command_input_completion = SlashCommandInputCompletion(obj.get("SlashCommandInputCompletion")) - slash_command_invocation_result = SlashCommandInvocationResult.from_dict(obj.get("SlashCommandInvocationResult")) + slash_command_invocation_result = _load_SlashCommandInvocationResult(obj.get("SlashCommandInvocationResult")) slash_command_kind = SlashCommandKind(obj.get("SlashCommandKind")) slash_command_select_subcommand_option = SlashCommandSelectSubcommandOption.from_dict(obj.get("SlashCommandSelectSubcommandOption")) slash_command_select_subcommand_result = SlashCommandSelectSubcommandResult.from_dict(obj.get("SlashCommandSelectSubcommandResult")) @@ -15896,7 +14610,7 @@ def from_dict(obj: Any) -> 'RPC': task_agent_info = TaskAgentInfo.from_dict(obj.get("TaskAgentInfo")) task_agent_progress = TaskAgentProgress.from_dict(obj.get("TaskAgentProgress")) task_execution_mode = TaskExecutionMode(obj.get("TaskExecutionMode")) - task_info = TaskInfo.from_dict(obj.get("TaskInfo")) + task_info = _load_TaskInfo(obj.get("TaskInfo")) task_list = TaskList.from_dict(obj.get("TaskList")) task_progress_line = TaskProgressLine.from_dict(obj.get("TaskProgressLine")) tasks_cancel_request = TasksCancelRequest.from_dict(obj.get("TasksCancelRequest")) @@ -15968,14 +14682,6 @@ def from_dict(obj: Any) -> 'RPC': usage_metrics_model_metric_usage = UsageMetricsModelMetricUsage.from_dict(obj.get("UsageMetricsModelMetricUsage")) usage_metrics_token_detail = UsageMetricsTokenDetail.from_dict(obj.get("UsageMetricsTokenDetail")) user_auth_info = UserAuthInfo.from_dict(obj.get("UserAuthInfo")) - user_tool_session_approval_commands = UserToolSessionApprovalCommands.from_dict(obj.get("UserToolSessionApprovalCommands")) - user_tool_session_approval_custom_tool = UserToolSessionApprovalCustomTool.from_dict(obj.get("UserToolSessionApprovalCustomTool")) - user_tool_session_approval_extension_management = UserToolSessionApprovalExtensionManagement.from_dict(obj.get("UserToolSessionApprovalExtensionManagement")) - user_tool_session_approval_extension_permission_access = UserToolSessionApprovalExtensionPermissionAccess.from_dict(obj.get("UserToolSessionApprovalExtensionPermissionAccess")) - user_tool_session_approval_mcp = UserToolSessionApprovalMCP.from_dict(obj.get("UserToolSessionApprovalMcp")) - user_tool_session_approval_memory = UserToolSessionApprovalMemory.from_dict(obj.get("UserToolSessionApprovalMemory")) - user_tool_session_approval_read = UserToolSessionApprovalRead.from_dict(obj.get("UserToolSessionApprovalRead")) - user_tool_session_approval_write = UserToolSessionApprovalWrite.from_dict(obj.get("UserToolSessionApprovalWrite")) workspaces_checkpoints = WorkspacesCheckpoints.from_dict(obj.get("WorkspacesCheckpoints")) workspaces_create_file_request = WorkspacesCreateFileRequest.from_dict(obj.get("WorkspacesCreateFileRequest")) workspaces_get_workspace_result = WorkspacesGetWorkspaceResult.from_dict(obj.get("WorkspacesGetWorkspaceResult")) @@ -15992,7 +14698,7 @@ def from_dict(obj: Any) -> 'RPC': session_context_info = from_union([SessionContextInfo.from_dict, from_none], obj.get("SessionContextInfo")) task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress")) workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary")) - return RPC(abort_request, abort_result, account_get_quota_request, account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_reload_result, agent_select_request, agent_select_result, api_key_auth_info, auth_info, auth_info_type, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_filter_mode, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_result, execute_command_params, execute_command_result, extension, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, installed_plugin, installed_plugin_source, installed_plugin_source_github, installed_plugin_source_local, installed_plugin_source_url, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, lsp_initialize_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_oauth_login_request, mcp_oauth_login_result, mcp_remove_git_hub_result, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_auth, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_stdio, mcp_server_list, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_billing, model_billing_token_prices, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, options_update_env_value_mode, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_reject, permission_decision_request, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_update_request, plugin, plugin_list, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_mode, schedule_entry, schedule_list, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachment, send_attachment_blob, send_attachment_directory, send_attachment_file, send_attachment_file_line_range, send_attachment_github_reference, send_attachment_github_reference_type, send_attachment_selection, send_attachment_selection_details, send_attachment_selection_details_end, send_attachment_selection_details_start, send_mode, send_request, send_result, server_skill, server_skill_list, session_auth_status, session_bulk_delete_result, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_github, session_installed_plugin_source_local, session_installed_plugin_source_url, session_list, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_metadata, session_metadata_snapshot, session_mode, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_prune_old_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, session_update_options_params, session_update_options_result, session_working_directory_context, session_working_directory_context_host_type, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, task_agent_info, task_agent_progress, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, tool, tool_list, tools_initialize_and_validate_result, tools_list_request, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_tool_session_approval_commands, user_tool_session_approval_custom_tool, user_tool_session_approval_extension_management, user_tool_session_approval_extension_permission_access, user_tool_session_approval_mcp, user_tool_session_approval_memory, user_tool_session_approval_read, user_tool_session_approval_write, workspaces_checkpoints, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspace_summary_host_type, workspaces_workspace_details_host_type, session_context_info, task_progress, workspace_summary) + return RPC(abort_request, abort_result, account_get_quota_request, account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_reload_result, agent_select_request, agent_select_result, api_key_auth_info, auth_info, auth_info_type, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_filter_mode, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_result, execute_command_params, execute_command_result, extension, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, installed_plugin, installed_plugin_source, installed_plugin_source_github, installed_plugin_source_local, installed_plugin_source_url, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, lsp_initialize_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_oauth_login_request, mcp_oauth_login_result, mcp_remove_git_hub_result, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_auth, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_stdio, mcp_server_list, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_billing, model_billing_token_prices, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, options_update_env_value_mode, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_reject, permission_decision_request, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_update_request, plugin, plugin_list, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_mode, schedule_entry, schedule_list, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachment, send_attachment_blob, send_attachment_directory, send_attachment_file, send_attachment_file_line_range, send_attachment_github_reference, send_attachment_github_reference_type, send_attachment_selection, send_attachment_selection_details, send_attachment_selection_details_end, send_attachment_selection_details_start, send_mode, send_request, send_result, server_skill, server_skill_list, session_auth_status, session_bulk_delete_result, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_github, session_installed_plugin_source_local, session_installed_plugin_source_url, session_list, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_metadata, session_metadata_snapshot, session_mode, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_prune_old_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, session_update_options_params, session_update_options_result, session_working_directory_context, session_working_directory_context_host_type, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, task_agent_info, task_agent_progress, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, tool, tool_list, tools_initialize_and_validate_result, tools_list_request, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, workspaces_checkpoints, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspace_summary_host_type, workspaces_workspace_details_host_type, session_context_info, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -16009,7 +14715,7 @@ def to_dict(self) -> dict: result["AgentSelectRequest"] = to_class(AgentSelectRequest, self.agent_select_request) result["AgentSelectResult"] = to_class(AgentSelectResult, self.agent_select_result) result["ApiKeyAuthInfo"] = to_class(APIKeyAuthInfo, self.api_key_auth_info) - result["AuthInfo"] = to_class(AuthInfo, self.auth_info) + result["AuthInfo"] = (self.auth_info).to_dict() result["AuthInfoType"] = to_enum(AuthInfoType, self.auth_info_type) result["CommandList"] = to_class(CommandList, self.command_list) result["CommandsHandlePendingCommandRequest"] = to_class(CommandsHandlePendingCommandRequest, self.commands_handle_pending_command_request) @@ -16057,7 +14763,7 @@ def to_dict(self) -> dict: result["ExternalToolTextResultForLlm"] = to_class(ExternalToolTextResultForLlm, self.external_tool_text_result_for_llm) result["ExternalToolTextResultForLlmBinaryResultsForLlm"] = to_class(ExternalToolTextResultForLlmBinaryResultsForLlm, self.external_tool_text_result_for_llm_binary_results_for_llm) result["ExternalToolTextResultForLlmBinaryResultsForLlmType"] = to_enum(ExternalToolTextResultForLlmBinaryResultsForLlmType, self.external_tool_text_result_for_llm_binary_results_for_llm_type) - result["ExternalToolTextResultForLlmContent"] = to_class(ExternalToolTextResultForLlmContent, self.external_tool_text_result_for_llm_content) + result["ExternalToolTextResultForLlmContent"] = (self.external_tool_text_result_for_llm_content).to_dict() result["ExternalToolTextResultForLlmContentAudio"] = to_class(ExternalToolTextResultForLlmContentAudio, self.external_tool_text_result_for_llm_content_audio) result["ExternalToolTextResultForLlmContentImage"] = to_class(ExternalToolTextResultForLlmContentImage, self.external_tool_text_result_for_llm_content_image) result["ExternalToolTextResultForLlmContentResource"] = to_class(ExternalToolTextResultForLlmContentResource, self.external_tool_text_result_for_llm_content_resource) @@ -16170,12 +14876,12 @@ def to_dict(self) -> dict: result["OptionsUpdateEnvValueMode"] = to_enum(MCPSetEnvValueModeDetails, self.options_update_env_value_mode) result["PendingPermissionRequest"] = to_class(PendingPermissionRequest, self.pending_permission_request) result["PendingPermissionRequestList"] = to_class(PendingPermissionRequestList, self.pending_permission_request_list) - result["PermissionDecision"] = to_class(PermissionDecision, self.permission_decision) + result["PermissionDecision"] = (self.permission_decision).to_dict() result["PermissionDecisionApproved"] = to_class(PermissionDecisionApproved, self.permission_decision_approved) result["PermissionDecisionApprovedForLocation"] = to_class(PermissionDecisionApprovedForLocation, self.permission_decision_approved_for_location) result["PermissionDecisionApprovedForSession"] = to_class(PermissionDecisionApprovedForSession, self.permission_decision_approved_for_session) result["PermissionDecisionApproveForLocation"] = to_class(PermissionDecisionApproveForLocation, self.permission_decision_approve_for_location) - result["PermissionDecisionApproveForLocationApproval"] = to_class(PermissionDecisionApproveForLocationApproval, self.permission_decision_approve_for_location_approval) + result["PermissionDecisionApproveForLocationApproval"] = (self.permission_decision_approve_for_location_approval).to_dict() result["PermissionDecisionApproveForLocationApprovalCommands"] = to_class(PermissionDecisionApproveForLocationApprovalCommands, self.permission_decision_approve_for_location_approval_commands) result["PermissionDecisionApproveForLocationApprovalCustomTool"] = to_class(PermissionDecisionApproveForLocationApprovalCustomTool, self.permission_decision_approve_for_location_approval_custom_tool) result["PermissionDecisionApproveForLocationApprovalExtensionManagement"] = to_class(PermissionDecisionApproveForLocationApprovalExtensionManagement, self.permission_decision_approve_for_location_approval_extension_management) @@ -16186,7 +14892,7 @@ def to_dict(self) -> dict: result["PermissionDecisionApproveForLocationApprovalRead"] = to_class(PermissionDecisionApproveForLocationApprovalRead, self.permission_decision_approve_for_location_approval_read) result["PermissionDecisionApproveForLocationApprovalWrite"] = to_class(PermissionDecisionApproveForLocationApprovalWrite, self.permission_decision_approve_for_location_approval_write) result["PermissionDecisionApproveForSession"] = to_class(PermissionDecisionApproveForSession, self.permission_decision_approve_for_session) - result["PermissionDecisionApproveForSessionApproval"] = to_class(PermissionDecisionApproveForSessionApproval, self.permission_decision_approve_for_session_approval) + result["PermissionDecisionApproveForSessionApproval"] = (self.permission_decision_approve_for_session_approval).to_dict() result["PermissionDecisionApproveForSessionApprovalCommands"] = to_class(PermissionDecisionApproveForSessionApprovalCommands, self.permission_decision_approve_for_session_approval_commands) result["PermissionDecisionApproveForSessionApprovalCustomTool"] = to_class(PermissionDecisionApproveForSessionApprovalCustomTool, self.permission_decision_approve_for_session_approval_custom_tool) result["PermissionDecisionApproveForSessionApprovalExtensionManagement"] = to_class(PermissionDecisionApproveForSessionApprovalExtensionManagement, self.permission_decision_approve_for_session_approval_extension_management) @@ -16231,7 +14937,7 @@ def to_dict(self) -> dict: result["PermissionsConfigureParams"] = to_class(PermissionsConfigureParams, self.permissions_configure_params) result["PermissionsConfigureResult"] = to_class(PermissionsConfigureResult, self.permissions_configure_result) result["PermissionsFolderTrustAddTrustedResult"] = to_class(PermissionsFolderTrustAddTrustedResult, self.permissions_folder_trust_add_trusted_result) - result["PermissionsLocationsAddToolApprovalDetails"] = to_class(PermissionsLocationsAddToolApprovalDetails, self.permissions_locations_add_tool_approval_details) + result["PermissionsLocationsAddToolApprovalDetails"] = (self.permissions_locations_add_tool_approval_details).to_dict() result["PermissionsLocationsAddToolApprovalDetailsCommands"] = to_class(PermissionsLocationsAddToolApprovalDetailsCommands, self.permissions_locations_add_tool_approval_details_commands) result["PermissionsLocationsAddToolApprovalDetailsCustomTool"] = to_class(PermissionsLocationsAddToolApprovalDetailsCustomTool, self.permissions_locations_add_tool_approval_details_custom_tool) result["PermissionsLocationsAddToolApprovalDetailsExtensionManagement"] = to_class(PermissionsLocationsAddToolApprovalDetailsExtensionManagement, self.permissions_locations_add_tool_approval_details_extension_management) @@ -16268,7 +14974,7 @@ def to_dict(self) -> dict: result["PluginList"] = to_class(PluginList, self.plugin_list) result["QueuedCommandHandled"] = to_class(QueuedCommandHandled, self.queued_command_handled) result["QueuedCommandNotHandled"] = to_class(QueuedCommandNotHandled, self.queued_command_not_handled) - result["QueuedCommandResult"] = to_class(QueuedCommandResult, self.queued_command_result) + result["QueuedCommandResult"] = (self.queued_command_result).to_dict() result["QueuePendingItems"] = to_class(QueuePendingItems, self.queue_pending_items) result["QueuePendingItemsKind"] = to_enum(QueuePendingItemsKind, self.queue_pending_items_kind) result["QueuePendingItemsResult"] = to_class(QueuePendingItemsResult, self.queue_pending_items_result) @@ -16289,7 +14995,7 @@ def to_dict(self) -> dict: result["SecretsAddFilterValuesRequest"] = to_class(SecretsAddFilterValuesRequest, self.secrets_add_filter_values_request) result["SecretsAddFilterValuesResult"] = to_class(SecretsAddFilterValuesResult, self.secrets_add_filter_values_result) result["SendAgentMode"] = to_enum(SendAgentMode, self.send_agent_mode) - result["SendAttachment"] = to_class(SendAttachment, self.send_attachment) + result["SendAttachment"] = (self.send_attachment).to_dict() result["SendAttachmentBlob"] = to_class(SendAttachmentBlob, self.send_attachment_blob) result["SendAttachmentDirectory"] = to_class(SendAttachmentDirectory, self.send_attachment_directory) result["SendAttachmentFile"] = to_class(SendAttachmentFile, self.send_attachment_file) @@ -16407,7 +15113,7 @@ def to_dict(self) -> dict: result["SlashCommandInfo"] = to_class(SlashCommandInfo, self.slash_command_info) result["SlashCommandInput"] = to_class(SlashCommandInput, self.slash_command_input) result["SlashCommandInputCompletion"] = to_enum(SlashCommandInputCompletion, self.slash_command_input_completion) - result["SlashCommandInvocationResult"] = to_class(SlashCommandInvocationResult, self.slash_command_invocation_result) + result["SlashCommandInvocationResult"] = (self.slash_command_invocation_result).to_dict() result["SlashCommandKind"] = to_enum(SlashCommandKind, self.slash_command_kind) result["SlashCommandSelectSubcommandOption"] = to_class(SlashCommandSelectSubcommandOption, self.slash_command_select_subcommand_option) result["SlashCommandSelectSubcommandResult"] = to_class(SlashCommandSelectSubcommandResult, self.slash_command_select_subcommand_result) @@ -16415,7 +15121,7 @@ def to_dict(self) -> dict: result["TaskAgentInfo"] = to_class(TaskAgentInfo, self.task_agent_info) result["TaskAgentProgress"] = to_class(TaskAgentProgress, self.task_agent_progress) result["TaskExecutionMode"] = to_enum(TaskExecutionMode, self.task_execution_mode) - result["TaskInfo"] = to_class(TaskInfo, self.task_info) + result["TaskInfo"] = (self.task_info).to_dict() result["TaskList"] = to_class(TaskList, self.task_list) result["TaskProgressLine"] = to_class(TaskProgressLine, self.task_progress_line) result["TasksCancelRequest"] = to_class(TasksCancelRequest, self.tasks_cancel_request) @@ -16487,14 +15193,6 @@ def to_dict(self) -> dict: result["UsageMetricsModelMetricUsage"] = to_class(UsageMetricsModelMetricUsage, self.usage_metrics_model_metric_usage) result["UsageMetricsTokenDetail"] = to_class(UsageMetricsTokenDetail, self.usage_metrics_token_detail) result["UserAuthInfo"] = to_class(UserAuthInfo, self.user_auth_info) - result["UserToolSessionApprovalCommands"] = to_class(UserToolSessionApprovalCommands, self.user_tool_session_approval_commands) - result["UserToolSessionApprovalCustomTool"] = to_class(UserToolSessionApprovalCustomTool, self.user_tool_session_approval_custom_tool) - result["UserToolSessionApprovalExtensionManagement"] = to_class(UserToolSessionApprovalExtensionManagement, self.user_tool_session_approval_extension_management) - result["UserToolSessionApprovalExtensionPermissionAccess"] = to_class(UserToolSessionApprovalExtensionPermissionAccess, self.user_tool_session_approval_extension_permission_access) - result["UserToolSessionApprovalMcp"] = to_class(UserToolSessionApprovalMCP, self.user_tool_session_approval_mcp) - result["UserToolSessionApprovalMemory"] = to_class(UserToolSessionApprovalMemory, self.user_tool_session_approval_memory) - result["UserToolSessionApprovalRead"] = to_class(UserToolSessionApprovalRead, self.user_tool_session_approval_read) - result["UserToolSessionApprovalWrite"] = to_class(UserToolSessionApprovalWrite, self.user_tool_session_approval_write) result["WorkspacesCheckpoints"] = to_class(WorkspacesCheckpoints, self.workspaces_checkpoints) result["WorkspacesCreateFileRequest"] = to_class(WorkspacesCreateFileRequest, self.workspaces_create_file_request) result["WorkspacesGetWorkspaceResult"] = to_class(WorkspacesGetWorkspaceResult, self.workspaces_get_workspace_result) @@ -16519,6 +15217,164 @@ def rpc_from_dict(s: Any) -> RPC: def rpc_to_dict(x: RPC) -> Any: return to_class(RPC, x) +# The new auth credentials to install on the session. When omitted or `undefined`, the call is a no-op and the session's existing credentials are preserved. The runtime stores the value verbatim and uses it for outbound model/API requests; it does NOT re-validate or re-fetch the associated Copilot user response. Several variants carry secret material; treat this method's params as containing secrets at rest and in transit. +AuthInfo = HMACAuthInfo | EnvAuthInfo | TokenAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo + +def _load_AuthInfo(obj: Any) -> "AuthInfo": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "hmac": return HMACAuthInfo.from_dict(obj) + case "env": return EnvAuthInfo.from_dict(obj) + case "token": return TokenAuthInfo.from_dict(obj) + case "copilot-api-token": return CopilotAPITokenAuthInfo.from_dict(obj) + case "user": return UserAuthInfo.from_dict(obj) + case "gh-cli": return GhCLIAuthInfo.from_dict(obj) + case "api-key": return APIKeyAuthInfo.from_dict(obj) + case _: raise ValueError(f"Unknown AuthInfo type: {kind!r}") + +# A content block within a tool result, which may be text, terminal output, image, audio, or a resource +ExternalToolTextResultForLlmContent = ExternalToolTextResultForLlmContentText | ExternalToolTextResultForLlmContentTerminal | ExternalToolTextResultForLlmContentImage | ExternalToolTextResultForLlmContentAudio | ExternalToolTextResultForLlmContentResourceLink | ExternalToolTextResultForLlmContentResource + +def _load_ExternalToolTextResultForLlmContent(obj: Any) -> "ExternalToolTextResultForLlmContent": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "text": return ExternalToolTextResultForLlmContentText.from_dict(obj) + case "terminal": return ExternalToolTextResultForLlmContentTerminal.from_dict(obj) + case "image": return ExternalToolTextResultForLlmContentImage.from_dict(obj) + case "audio": return ExternalToolTextResultForLlmContentAudio.from_dict(obj) + case "resource_link": return ExternalToolTextResultForLlmContentResourceLink.from_dict(obj) + case "resource": return ExternalToolTextResultForLlmContentResource.from_dict(obj) + case _: raise ValueError(f"Unknown ExternalToolTextResultForLlmContent type: {kind!r}") + +# The client's response to the pending permission prompt +PermissionDecision = PermissionDecisionApproveOnce | PermissionDecisionApproveForSession | PermissionDecisionApproveForLocation | PermissionDecisionApprovePermanently | PermissionDecisionReject | PermissionDecisionUserNotAvailable | PermissionDecisionApproved | PermissionDecisionApprovedForSession | PermissionDecisionApprovedForLocation | PermissionDecisionCancelled | PermissionDecisionDeniedByRules | PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser | PermissionDecisionDeniedInteractivelyByUser | PermissionDecisionDeniedByContentExclusionPolicy | PermissionDecisionDeniedByPermissionRequestHook + +def _load_PermissionDecision(obj: Any) -> "PermissionDecision": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "approve-once": return PermissionDecisionApproveOnce.from_dict(obj) + case "approve-for-session": return PermissionDecisionApproveForSession.from_dict(obj) + case "approve-for-location": return PermissionDecisionApproveForLocation.from_dict(obj) + case "approve-permanently": return PermissionDecisionApprovePermanently.from_dict(obj) + case "reject": return PermissionDecisionReject.from_dict(obj) + case "user-not-available": return PermissionDecisionUserNotAvailable.from_dict(obj) + case "approved": return PermissionDecisionApproved.from_dict(obj) + case "approved-for-session": return PermissionDecisionApprovedForSession.from_dict(obj) + case "approved-for-location": return PermissionDecisionApprovedForLocation.from_dict(obj) + case "cancelled": return PermissionDecisionCancelled.from_dict(obj) + case "denied-by-rules": return PermissionDecisionDeniedByRules.from_dict(obj) + case "denied-no-approval-rule-and-could-not-request-from-user": return PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser.from_dict(obj) + case "denied-interactively-by-user": return PermissionDecisionDeniedInteractivelyByUser.from_dict(obj) + case "denied-by-content-exclusion-policy": return PermissionDecisionDeniedByContentExclusionPolicy.from_dict(obj) + case "denied-by-permission-request-hook": return PermissionDecisionDeniedByPermissionRequestHook.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionDecision kind: {kind!r}") + +# Approval to persist for this location +PermissionDecisionApproveForLocationApproval = PermissionDecisionApproveForLocationApprovalCommands | PermissionDecisionApproveForLocationApprovalRead | PermissionDecisionApproveForLocationApprovalWrite | PermissionDecisionApproveForLocationApprovalMCP | PermissionDecisionApproveForLocationApprovalMCPSampling | PermissionDecisionApproveForLocationApprovalMemory | PermissionDecisionApproveForLocationApprovalCustomTool | PermissionDecisionApproveForLocationApprovalExtensionManagement | PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess + +def _load_PermissionDecisionApproveForLocationApproval(obj: Any) -> "PermissionDecisionApproveForLocationApproval": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return PermissionDecisionApproveForLocationApprovalCommands.from_dict(obj) + case "read": return PermissionDecisionApproveForLocationApprovalRead.from_dict(obj) + case "write": return PermissionDecisionApproveForLocationApprovalWrite.from_dict(obj) + case "mcp": return PermissionDecisionApproveForLocationApprovalMCP.from_dict(obj) + case "mcp-sampling": return PermissionDecisionApproveForLocationApprovalMCPSampling.from_dict(obj) + case "memory": return PermissionDecisionApproveForLocationApprovalMemory.from_dict(obj) + case "custom-tool": return PermissionDecisionApproveForLocationApprovalCustomTool.from_dict(obj) + case "extension-management": return PermissionDecisionApproveForLocationApprovalExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionDecisionApproveForLocationApproval kind: {kind!r}") + +# Session-scoped approval to remember (tool prompts only; omitted for path/url prompts) +PermissionDecisionApproveForSessionApproval = PermissionDecisionApproveForSessionApprovalCommands | PermissionDecisionApproveForSessionApprovalRead | PermissionDecisionApproveForSessionApprovalWrite | PermissionDecisionApproveForSessionApprovalMCP | PermissionDecisionApproveForSessionApprovalMCPSampling | PermissionDecisionApproveForSessionApprovalMemory | PermissionDecisionApproveForSessionApprovalCustomTool | PermissionDecisionApproveForSessionApprovalExtensionManagement | PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess + +def _load_PermissionDecisionApproveForSessionApproval(obj: Any) -> "PermissionDecisionApproveForSessionApproval": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return PermissionDecisionApproveForSessionApprovalCommands.from_dict(obj) + case "read": return PermissionDecisionApproveForSessionApprovalRead.from_dict(obj) + case "write": return PermissionDecisionApproveForSessionApprovalWrite.from_dict(obj) + case "mcp": return PermissionDecisionApproveForSessionApprovalMCP.from_dict(obj) + case "mcp-sampling": return PermissionDecisionApproveForSessionApprovalMCPSampling.from_dict(obj) + case "memory": return PermissionDecisionApproveForSessionApprovalMemory.from_dict(obj) + case "custom-tool": return PermissionDecisionApproveForSessionApprovalCustomTool.from_dict(obj) + case "extension-management": return PermissionDecisionApproveForSessionApprovalExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionDecisionApproveForSessionApproval kind: {kind!r}") + +# Tool approval to persist and apply +PermissionsLocationsAddToolApprovalDetails = PermissionsLocationsAddToolApprovalDetailsCommands | PermissionsLocationsAddToolApprovalDetailsRead | PermissionsLocationsAddToolApprovalDetailsWrite | PermissionsLocationsAddToolApprovalDetailsMCP | PermissionsLocationsAddToolApprovalDetailsMCPSampling | PermissionsLocationsAddToolApprovalDetailsMemory | PermissionsLocationsAddToolApprovalDetailsCustomTool | PermissionsLocationsAddToolApprovalDetailsExtensionManagement | PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess + +def _load_PermissionsLocationsAddToolApprovalDetails(obj: Any) -> "PermissionsLocationsAddToolApprovalDetails": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return PermissionsLocationsAddToolApprovalDetailsCommands.from_dict(obj) + case "read": return PermissionsLocationsAddToolApprovalDetailsRead.from_dict(obj) + case "write": return PermissionsLocationsAddToolApprovalDetailsWrite.from_dict(obj) + case "mcp": return PermissionsLocationsAddToolApprovalDetailsMCP.from_dict(obj) + case "mcp-sampling": return PermissionsLocationsAddToolApprovalDetailsMCPSampling.from_dict(obj) + case "memory": return PermissionsLocationsAddToolApprovalDetailsMemory.from_dict(obj) + case "custom-tool": return PermissionsLocationsAddToolApprovalDetailsCustomTool.from_dict(obj) + case "extension-management": return PermissionsLocationsAddToolApprovalDetailsExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionsLocationsAddToolApprovalDetails kind: {kind!r}") + +# Result of the queued command execution. +QueuedCommandResult = QueuedCommandHandled | QueuedCommandNotHandled + +def _load_QueuedCommandResult(obj: Any) -> "QueuedCommandResult": + assert isinstance(obj, dict) + kind = obj.get("handled") + match kind: + case "true": return QueuedCommandHandled.from_dict(obj) + case "false": return QueuedCommandNotHandled.from_dict(obj) + case _: raise ValueError(f"Unknown QueuedCommandResult handled: {kind!r}") + +# A user message attachment — a file, directory, code selection, blob, or GitHub reference +SendAttachment = SendAttachmentFile | SendAttachmentDirectory | SendAttachmentSelection | SendAttachmentGithubReference | SendAttachmentBlob + +def _load_SendAttachment(obj: Any) -> "SendAttachment": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "file": return SendAttachmentFile.from_dict(obj) + case "directory": return SendAttachmentDirectory.from_dict(obj) + case "selection": return SendAttachmentSelection.from_dict(obj) + case "github_reference": return SendAttachmentGithubReference.from_dict(obj) + case "blob": return SendAttachmentBlob.from_dict(obj) + case _: raise ValueError(f"Unknown SendAttachment type: {kind!r}") + +# Result of invoking the slash command (text output, prompt to send to the agent, or completion). +SlashCommandInvocationResult = SlashCommandTextResult | SlashCommandAgentPromptResult | SlashCommandCompletedResult | SlashCommandSelectSubcommandResult + +def _load_SlashCommandInvocationResult(obj: Any) -> "SlashCommandInvocationResult": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "text": return SlashCommandTextResult.from_dict(obj) + case "agent-prompt": return SlashCommandAgentPromptResult.from_dict(obj) + case "completed": return SlashCommandCompletedResult.from_dict(obj) + case "select-subcommand": return SlashCommandSelectSubcommandResult.from_dict(obj) + case _: raise ValueError(f"Unknown SlashCommandInvocationResult kind: {kind!r}") + +# Schema for the `TaskInfo` type. +TaskInfo = TaskAgentInfo | TaskShellInfo + +def _load_TaskInfo(obj: Any) -> "TaskInfo": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "agent": return TaskAgentInfo.from_dict(obj) + case "shell": return TaskShellInfo.from_dict(obj) + case _: raise ValueError(f"Unknown TaskInfo type: {kind!r}") + ExternalToolResult = ExternalToolTextResultForLlm FilterMapping = dict @@ -17261,7 +16117,7 @@ async def invoke(self, params: CommandsInvokeRequest, *, timeout: float | None = "Invokes a slash command in the session.\n\nArgs:\n params: Slash command name and optional raw input string to invoke.\n\nReturns:\n Result of invoking the slash command (text output, prompt to send to the agent, or completion)." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id - return SlashCommandInvocationResult.from_dict(await self._client.request("session.commands.invoke", params_dict, **_timeout_kwargs(timeout))) + return _load_SlashCommandInvocationResult(await self._client.request("session.commands.invoke", params_dict, **_timeout_kwargs(timeout))) async def handle_pending_command(self, params: CommandsHandlePendingCommandRequest, *, timeout: float | None = None) -> CommandsHandlePendingCommandResult: "Reports completion of a pending client-handled slash command.\n\nArgs:\n params: Pending command request ID and an optional error if the client handler failed.\n\nReturns:\n Indicates whether the pending client-handled command was completed successfully." diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 515f9c317..4b72621df 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum -from typing import Any, TypeVar, cast +from typing import Any, ClassVar, TypeVar, cast from uuid import UUID import dateutil.parser @@ -1812,6 +1812,91 @@ def to_dict(self) -> dict: return {} +@dataclass +class PermissionApproved: + "Schema for the `PermissionApproved` type." + kind: ClassVar[str] = "approved" + + @staticmethod + def from_dict(obj: Any) -> "PermissionApproved": + assert isinstance(obj, dict) + return PermissionApproved( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + return result + + +@dataclass +class PermissionApprovedForLocation: + "Schema for the `PermissionApprovedForLocation` type." + approval: UserToolSessionApproval + kind: ClassVar[str] = "approved-for-location" + location_key: str + + @staticmethod + def from_dict(obj: Any) -> "PermissionApprovedForLocation": + assert isinstance(obj, dict) + approval = _load_UserToolSessionApproval(obj.get("approval")) + location_key = from_str(obj.get("locationKey")) + return PermissionApprovedForLocation( + approval=approval, + location_key=location_key, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["approval"] = self.approval.to_dict() + result["kind"] = self.kind + result["locationKey"] = from_str(self.location_key) + return result + + +@dataclass +class PermissionApprovedForSession: + "Schema for the `PermissionApprovedForSession` type." + approval: UserToolSessionApproval + kind: ClassVar[str] = "approved-for-session" + + @staticmethod + def from_dict(obj: Any) -> "PermissionApprovedForSession": + assert isinstance(obj, dict) + approval = _load_UserToolSessionApproval(obj.get("approval")) + return PermissionApprovedForSession( + approval=approval, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["approval"] = self.approval.to_dict() + result["kind"] = self.kind + return result + + +@dataclass +class PermissionCancelled: + "Schema for the `PermissionCancelled` type." + kind: ClassVar[str] = "cancelled" + reason: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionCancelled": + assert isinstance(obj, dict) + reason = from_union([from_none, from_str], obj.get("reason")) + return PermissionCancelled( + reason=reason, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.reason is not None: + result["reason"] = from_union([from_none, from_str], self.reason) + return result + + @dataclass class PermissionCompletedData: "Permission request completion notification signaling UI dismissal" @@ -1822,357 +1907,845 @@ class PermissionCompletedData: @staticmethod def from_dict(obj: Any) -> "PermissionCompletedData": assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - result = PermissionResult.from_dict(obj.get("result")) + request_id = from_str(obj.get("requestId")) + result = _load_PermissionResult(obj.get("result")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionCompletedData( + request_id=request_id, + result=result, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = self.result.to_dict() + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionDeniedByContentExclusionPolicy: + "Schema for the `PermissionDeniedByContentExclusionPolicy` type." + kind: ClassVar[str] = "denied-by-content-exclusion-policy" + message: str + path: str + + @staticmethod + def from_dict(obj: Any) -> "PermissionDeniedByContentExclusionPolicy": + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + path = from_str(obj.get("path")) + return PermissionDeniedByContentExclusionPolicy( + message=message, + path=path, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["message"] = from_str(self.message) + result["path"] = from_str(self.path) + return result + + +@dataclass +class PermissionDeniedByPermissionRequestHook: + "Schema for the `PermissionDeniedByPermissionRequestHook` type." + kind: ClassVar[str] = "denied-by-permission-request-hook" + interrupt: bool | None = None + message: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionDeniedByPermissionRequestHook": + assert isinstance(obj, dict) + interrupt = from_union([from_none, from_bool], obj.get("interrupt")) + message = from_union([from_none, from_str], obj.get("message")) + return PermissionDeniedByPermissionRequestHook( + interrupt=interrupt, + message=message, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.interrupt is not None: + result["interrupt"] = from_union([from_none, from_bool], self.interrupt) + if self.message is not None: + result["message"] = from_union([from_none, from_str], self.message) + return result + + +@dataclass +class PermissionDeniedByRules: + "Schema for the `PermissionDeniedByRules` type." + kind: ClassVar[str] = "denied-by-rules" + rules: list[PermissionRule] + + @staticmethod + def from_dict(obj: Any) -> "PermissionDeniedByRules": + assert isinstance(obj, dict) + rules = from_list(PermissionRule.from_dict, obj.get("rules")) + return PermissionDeniedByRules( + rules=rules, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["rules"] = from_list(lambda x: to_class(PermissionRule, x), self.rules) + return result + + +@dataclass +class PermissionDeniedInteractivelyByUser: + "Schema for the `PermissionDeniedInteractivelyByUser` type." + kind: ClassVar[str] = "denied-interactively-by-user" + feedback: str | None = None + force_reject: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionDeniedInteractivelyByUser": + assert isinstance(obj, dict) + feedback = from_union([from_none, from_str], obj.get("feedback")) + force_reject = from_union([from_none, from_bool], obj.get("forceReject")) + return PermissionDeniedInteractivelyByUser( + feedback=feedback, + force_reject=force_reject, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + if self.feedback is not None: + result["feedback"] = from_union([from_none, from_str], self.feedback) + if self.force_reject is not None: + result["forceReject"] = from_union([from_none, from_bool], self.force_reject) + return result + + +@dataclass +class PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser: + "Schema for the `PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser` type." + kind: ClassVar[str] = "denied-no-approval-rule-and-could-not-request-from-user" + + @staticmethod + def from_dict(obj: Any) -> "PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser": + assert isinstance(obj, dict) + return PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + return result + + +@dataclass +class PermissionPromptRequestCommands: + "Shell command permission prompt" + can_offer_session_approval: bool + command_identifiers: list[str] + full_command_text: str + intention: str + kind: ClassVar[str] = "commands" + tool_call_id: str | None = None + warning: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestCommands": + assert isinstance(obj, dict) + can_offer_session_approval = from_bool(obj.get("canOfferSessionApproval")) + command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) + full_command_text = from_str(obj.get("fullCommandText")) + intention = from_str(obj.get("intention")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + warning = from_union([from_none, from_str], obj.get("warning")) + return PermissionPromptRequestCommands( + can_offer_session_approval=can_offer_session_approval, + command_identifiers=command_identifiers, + full_command_text=full_command_text, + intention=intention, + tool_call_id=tool_call_id, + warning=warning, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["canOfferSessionApproval"] = from_bool(self.can_offer_session_approval) + result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) + result["fullCommandText"] = from_str(self.full_command_text) + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + if self.warning is not None: + result["warning"] = from_union([from_none, from_str], self.warning) + return result + + +@dataclass +class PermissionPromptRequestCustomTool: + "Custom tool invocation permission prompt" + kind: ClassVar[str] = "custom-tool" + tool_description: str + tool_name: str + args: Any = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestCustomTool": + assert isinstance(obj, dict) + tool_description = from_str(obj.get("toolDescription")) + tool_name = from_str(obj.get("toolName")) + args = obj.get("args") + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestCustomTool( + tool_description=tool_description, + tool_name=tool_name, + args=args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolDescription"] = from_str(self.tool_description) + result["toolName"] = from_str(self.tool_name) + if self.args is not None: + result["args"] = self.args + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestExtensionManagement: + "Extension management permission prompt" + kind: ClassVar[str] = "extension-management" + operation: str + extension_name: str | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestExtensionManagement": + assert isinstance(obj, dict) + operation = from_str(obj.get("operation")) + extension_name = from_union([from_none, from_str], obj.get("extensionName")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestExtensionManagement( + operation=operation, + extension_name=extension_name, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["operation"] = from_str(self.operation) + if self.extension_name is not None: + result["extensionName"] = from_union([from_none, from_str], self.extension_name) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestExtensionPermissionAccess: + "Extension permission access prompt" + capabilities: list[str] + extension_name: str + kind: ClassVar[str] = "extension-permission-access" + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestExtensionPermissionAccess": + assert isinstance(obj, dict) + capabilities = from_list(from_str, obj.get("capabilities")) + extension_name = from_str(obj.get("extensionName")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestExtensionPermissionAccess( + capabilities=capabilities, + extension_name=extension_name, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["capabilities"] = from_list(from_str, self.capabilities) + result["extensionName"] = from_str(self.extension_name) + result["kind"] = self.kind + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestHook: + "Hook confirmation permission prompt" + kind: ClassVar[str] = "hook" + tool_name: str + hook_message: str | None = None + tool_args: Any = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestHook": + assert isinstance(obj, dict) + tool_name = from_str(obj.get("toolName")) + hook_message = from_union([from_none, from_str], obj.get("hookMessage")) + tool_args = obj.get("toolArgs") + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestHook( + tool_name=tool_name, + hook_message=hook_message, + tool_args=tool_args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolName"] = from_str(self.tool_name) + if self.hook_message is not None: + result["hookMessage"] = from_union([from_none, from_str], self.hook_message) + if self.tool_args is not None: + result["toolArgs"] = self.tool_args + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestMcp: + "MCP tool invocation permission prompt" + kind: ClassVar[str] = "mcp" + server_name: str + tool_name: str + tool_title: str + args: Any | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestMcp": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + tool_name = from_str(obj.get("toolName")) + tool_title = from_str(obj.get("toolTitle")) + args = from_union([from_none, lambda x: x], obj.get("args")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestMcp( + server_name=server_name, + tool_name=tool_name, + tool_title=tool_title, + args=args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["serverName"] = from_str(self.server_name) + result["toolName"] = from_str(self.tool_name) + result["toolTitle"] = from_str(self.tool_title) + if self.args is not None: + result["args"] = from_union([from_none, lambda x: x], self.args) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestMemory: + "Memory operation permission prompt" + fact: str + kind: ClassVar[str] = "memory" + action: PermissionRequestMemoryAction | None = None + citations: str | None = None + direction: PermissionRequestMemoryDirection | None = None + reason: str | None = None + subject: str | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestMemory": + assert isinstance(obj, dict) + fact = from_str(obj.get("fact")) + action = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryAction, x)], obj.get("action")) + citations = from_union([from_none, from_str], obj.get("citations")) + direction = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryDirection, x)], obj.get("direction")) + reason = from_union([from_none, from_str], obj.get("reason")) + subject = from_union([from_none, from_str], obj.get("subject")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestMemory( + fact=fact, + action=action, + citations=citations, + direction=direction, + reason=reason, + subject=subject, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["fact"] = from_str(self.fact) + result["kind"] = self.kind + if self.action is not None: + result["action"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryAction, x)], self.action) + if self.citations is not None: + result["citations"] = from_union([from_none, from_str], self.citations) + if self.direction is not None: + result["direction"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryDirection, x)], self.direction) + if self.reason is not None: + result["reason"] = from_union([from_none, from_str], self.reason) + if self.subject is not None: + result["subject"] = from_union([from_none, from_str], self.subject) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestPath: + "Path access permission prompt" + access_kind: PermissionPromptRequestPathAccessKind + kind: ClassVar[str] = "path" + paths: list[str] + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestPath": + assert isinstance(obj, dict) + access_kind = parse_enum(PermissionPromptRequestPathAccessKind, obj.get("accessKind")) + paths = from_list(from_str, obj.get("paths")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestPath( + access_kind=access_kind, + paths=paths, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["accessKind"] = to_enum(PermissionPromptRequestPathAccessKind, self.access_kind) + result["kind"] = self.kind + result["paths"] = from_list(from_str, self.paths) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestRead: + "File read permission prompt" + intention: str + kind: ClassVar[str] = "read" + path: str + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestRead": + assert isinstance(obj, dict) + intention = from_str(obj.get("intention")) + path = from_str(obj.get("path")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestRead( + intention=intention, + path=path, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["path"] = from_str(self.path) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestUrl: + "URL access permission prompt" + intention: str + kind: ClassVar[str] = "url" + url: str + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestUrl": + assert isinstance(obj, dict) + intention = from_str(obj.get("intention")) + url = from_str(obj.get("url")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestUrl( + intention=intention, + url=url, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["url"] = from_str(self.url) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionPromptRequestWrite: + "File write permission prompt" + can_offer_session_approval: bool + diff: str + file_name: str + intention: str + kind: ClassVar[str] = "write" + new_file_contents: str | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionPromptRequestWrite": + assert isinstance(obj, dict) + can_offer_session_approval = from_bool(obj.get("canOfferSessionApproval")) + diff = from_str(obj.get("diff")) + file_name = from_str(obj.get("fileName")) + intention = from_str(obj.get("intention")) + new_file_contents = from_union([from_none, from_str], obj.get("newFileContents")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionPromptRequestWrite( + can_offer_session_approval=can_offer_session_approval, + diff=diff, + file_name=file_name, + intention=intention, + new_file_contents=new_file_contents, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["canOfferSessionApproval"] = from_bool(self.can_offer_session_approval) + result["diff"] = from_str(self.diff) + result["fileName"] = from_str(self.file_name) + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + if self.new_file_contents is not None: + result["newFileContents"] = from_union([from_none, from_str], self.new_file_contents) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestCustomTool: + "Custom tool invocation permission request" + kind: ClassVar[str] = "custom-tool" + tool_description: str + tool_name: str + args: Any = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestCustomTool": + assert isinstance(obj, dict) + tool_description = from_str(obj.get("toolDescription")) + tool_name = from_str(obj.get("toolName")) + args = obj.get("args") + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestCustomTool( + tool_description=tool_description, + tool_name=tool_name, + args=args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolDescription"] = from_str(self.tool_description) + result["toolName"] = from_str(self.tool_name) + if self.args is not None: + result["args"] = self.args + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestExtensionManagement: + "Extension management permission request" + kind: ClassVar[str] = "extension-management" + operation: str + extension_name: str | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestExtensionManagement": + assert isinstance(obj, dict) + operation = from_str(obj.get("operation")) + extension_name = from_union([from_none, from_str], obj.get("extensionName")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestExtensionManagement( + operation=operation, + extension_name=extension_name, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["operation"] = from_str(self.operation) + if self.extension_name is not None: + result["extensionName"] = from_union([from_none, from_str], self.extension_name) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestExtensionPermissionAccess: + "Extension permission access request" + capabilities: list[str] + extension_name: str + kind: ClassVar[str] = "extension-permission-access" + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestExtensionPermissionAccess": + assert isinstance(obj, dict) + capabilities = from_list(from_str, obj.get("capabilities")) + extension_name = from_str(obj.get("extensionName")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestExtensionPermissionAccess( + capabilities=capabilities, + extension_name=extension_name, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["capabilities"] = from_list(from_str, self.capabilities) + result["extensionName"] = from_str(self.extension_name) + result["kind"] = self.kind + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestHook: + "Hook confirmation permission request" + kind: ClassVar[str] = "hook" + tool_name: str + hook_message: str | None = None + tool_args: Any = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestHook": + assert isinstance(obj, dict) + tool_name = from_str(obj.get("toolName")) + hook_message = from_union([from_none, from_str], obj.get("hookMessage")) + tool_args = obj.get("toolArgs") + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestHook( + tool_name=tool_name, + hook_message=hook_message, + tool_args=tool_args, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolName"] = from_str(self.tool_name) + if self.hook_message is not None: + result["hookMessage"] = from_union([from_none, from_str], self.hook_message) + if self.tool_args is not None: + result["toolArgs"] = self.tool_args + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestMcp: + "MCP tool invocation permission request" + kind: ClassVar[str] = "mcp" + read_only: bool + server_name: str + tool_name: str + tool_title: str + args: Any = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestMcp": + assert isinstance(obj, dict) + read_only = from_bool(obj.get("readOnly")) + server_name = from_str(obj.get("serverName")) + tool_name = from_str(obj.get("toolName")) + tool_title = from_str(obj.get("toolTitle")) + args = obj.get("args") tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) - return PermissionCompletedData( - request_id=request_id, - result=result, + return PermissionRequestMcp( + read_only=read_only, + server_name=server_name, + tool_name=tool_name, + tool_title=tool_title, + args=args, tool_call_id=tool_call_id, ) def to_dict(self) -> dict: result: dict = {} - result["requestId"] = from_str(self.request_id) - result["result"] = to_class(PermissionResult, self.result) + result["kind"] = self.kind + result["readOnly"] = from_bool(self.read_only) + result["serverName"] = from_str(self.server_name) + result["toolName"] = from_str(self.tool_name) + result["toolTitle"] = from_str(self.tool_title) + if self.args is not None: + result["args"] = self.args if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) return result @dataclass -class PermissionPromptRequest: - "Derived user-facing permission prompt details for UI consumers" - kind: PermissionPromptRequestKind - access_kind: PermissionPromptRequestPathAccessKind | None = None +class PermissionRequestMemory: + "Memory operation permission request" + fact: str + kind: ClassVar[str] = "memory" action: PermissionRequestMemoryAction | None = None - args: Any | None = None - can_offer_session_approval: bool | None = None - capabilities: list[str] | None = None citations: str | None = None - command_identifiers: list[str] | None = None - diff: str | None = None direction: PermissionRequestMemoryDirection | None = None - extension_name: str | None = None - fact: str | None = None - file_name: str | None = None - full_command_text: str | None = None - hook_message: str | None = None - intention: str | None = None - new_file_contents: str | None = None - operation: str | None = None - path: str | None = None - paths: list[str] | None = None reason: str | None = None - server_name: str | None = None subject: str | None = None - tool_args: Any = None tool_call_id: str | None = None - tool_description: str | None = None - tool_name: str | None = None - tool_title: str | None = None - url: str | None = None - warning: str | None = None @staticmethod - def from_dict(obj: Any) -> "PermissionPromptRequest": + def from_dict(obj: Any) -> "PermissionRequestMemory": assert isinstance(obj, dict) - kind = parse_enum(PermissionPromptRequestKind, obj.get("kind")) - access_kind = from_union([from_none, lambda x: parse_enum(PermissionPromptRequestPathAccessKind, x)], obj.get("accessKind")) + fact = from_str(obj.get("fact")) action = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryAction, x)], obj.get("action")) - args = from_union([from_none, lambda x: x], obj.get("args")) - can_offer_session_approval = from_union([from_none, from_bool], obj.get("canOfferSessionApproval")) - capabilities = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("capabilities")) citations = from_union([from_none, from_str], obj.get("citations")) - command_identifiers = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("commandIdentifiers")) - diff = from_union([from_none, from_str], obj.get("diff")) direction = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryDirection, x)], obj.get("direction")) - extension_name = from_union([from_none, from_str], obj.get("extensionName")) - fact = from_union([from_none, from_str], obj.get("fact")) - file_name = from_union([from_none, from_str], obj.get("fileName")) - full_command_text = from_union([from_none, from_str], obj.get("fullCommandText")) - hook_message = from_union([from_none, from_str], obj.get("hookMessage")) - intention = from_union([from_none, from_str], obj.get("intention")) - new_file_contents = from_union([from_none, from_str], obj.get("newFileContents")) - operation = from_union([from_none, from_str], obj.get("operation")) - path = from_union([from_none, from_str], obj.get("path")) - paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("paths")) reason = from_union([from_none, from_str], obj.get("reason")) - server_name = from_union([from_none, from_str], obj.get("serverName")) subject = from_union([from_none, from_str], obj.get("subject")) - tool_args = obj.get("toolArgs") tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) - tool_description = from_union([from_none, from_str], obj.get("toolDescription")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - tool_title = from_union([from_none, from_str], obj.get("toolTitle")) - url = from_union([from_none, from_str], obj.get("url")) - warning = from_union([from_none, from_str], obj.get("warning")) - return PermissionPromptRequest( - kind=kind, - access_kind=access_kind, + return PermissionRequestMemory( + fact=fact, action=action, - args=args, - can_offer_session_approval=can_offer_session_approval, - capabilities=capabilities, citations=citations, - command_identifiers=command_identifiers, - diff=diff, direction=direction, - extension_name=extension_name, - fact=fact, - file_name=file_name, - full_command_text=full_command_text, - hook_message=hook_message, - intention=intention, - new_file_contents=new_file_contents, - operation=operation, - path=path, - paths=paths, reason=reason, - server_name=server_name, subject=subject, - tool_args=tool_args, tool_call_id=tool_call_id, - tool_description=tool_description, - tool_name=tool_name, - tool_title=tool_title, - url=url, - warning=warning, ) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionPromptRequestKind, self.kind) - if self.access_kind is not None: - result["accessKind"] = from_union([from_none, lambda x: to_enum(PermissionPromptRequestPathAccessKind, x)], self.access_kind) + result["fact"] = from_str(self.fact) + result["kind"] = self.kind if self.action is not None: result["action"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryAction, x)], self.action) - if self.args is not None: - result["args"] = from_union([from_none, lambda x: x], self.args) - if self.can_offer_session_approval is not None: - result["canOfferSessionApproval"] = from_union([from_none, from_bool], self.can_offer_session_approval) - if self.capabilities is not None: - result["capabilities"] = from_union([from_none, lambda x: from_list(from_str, x)], self.capabilities) if self.citations is not None: result["citations"] = from_union([from_none, from_str], self.citations) - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([from_none, lambda x: from_list(from_str, x)], self.command_identifiers) - if self.diff is not None: - result["diff"] = from_union([from_none, from_str], self.diff) if self.direction is not None: result["direction"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryDirection, x)], self.direction) - if self.extension_name is not None: - result["extensionName"] = from_union([from_none, from_str], self.extension_name) - if self.fact is not None: - result["fact"] = from_union([from_none, from_str], self.fact) - if self.file_name is not None: - result["fileName"] = from_union([from_none, from_str], self.file_name) - if self.full_command_text is not None: - result["fullCommandText"] = from_union([from_none, from_str], self.full_command_text) - if self.hook_message is not None: - result["hookMessage"] = from_union([from_none, from_str], self.hook_message) - if self.intention is not None: - result["intention"] = from_union([from_none, from_str], self.intention) - if self.new_file_contents is not None: - result["newFileContents"] = from_union([from_none, from_str], self.new_file_contents) - if self.operation is not None: - result["operation"] = from_union([from_none, from_str], self.operation) - if self.path is not None: - result["path"] = from_union([from_none, from_str], self.path) - if self.paths is not None: - result["paths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.paths) if self.reason is not None: result["reason"] = from_union([from_none, from_str], self.reason) - if self.server_name is not None: - result["serverName"] = from_union([from_none, from_str], self.server_name) if self.subject is not None: result["subject"] = from_union([from_none, from_str], self.subject) - if self.tool_args is not None: - result["toolArgs"] = self.tool_args if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) - if self.tool_description is not None: - result["toolDescription"] = from_union([from_none, from_str], self.tool_description) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.tool_title is not None: - result["toolTitle"] = from_union([from_none, from_str], self.tool_title) - if self.url is not None: - result["url"] = from_union([from_none, from_str], self.url) - if self.warning is not None: - result["warning"] = from_union([from_none, from_str], self.warning) return result @dataclass -class PermissionRequest: - "Details of the permission being requested" - kind: PermissionRequestKind - action: PermissionRequestMemoryAction | None = None - args: Any = None - can_offer_session_approval: bool | None = None - capabilities: list[str] | None = None - citations: str | None = None - commands: list[PermissionRequestShellCommand] | None = None - diff: str | None = None - direction: PermissionRequestMemoryDirection | None = None - extension_name: str | None = None - fact: str | None = None - file_name: str | None = None - full_command_text: str | None = None - has_write_file_redirection: bool | None = None - hook_message: str | None = None - intention: str | None = None - new_file_contents: str | None = None - operation: str | None = None - path: str | None = None - possible_paths: list[str] | None = None - possible_urls: list[PermissionRequestShellPossibleUrl] | None = None - read_only: bool | None = None - reason: str | None = None - server_name: str | None = None - subject: str | None = None - tool_args: Any = None +class PermissionRequestRead: + "File or directory read permission request" + intention: str + kind: ClassVar[str] = "read" + path: str + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestRead": + assert isinstance(obj, dict) + intention = from_str(obj.get("intention")) + path = from_str(obj.get("path")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestRead( + intention=intention, + path=path, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["path"] = from_str(self.path) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestShell: + "Shell command permission request" + can_offer_session_approval: bool + commands: list[PermissionRequestShellCommand] + full_command_text: str + has_write_file_redirection: bool + intention: str + kind: ClassVar[str] = "shell" + possible_paths: list[str] + possible_urls: list[PermissionRequestShellPossibleUrl] tool_call_id: str | None = None - tool_description: str | None = None - tool_name: str | None = None - tool_title: str | None = None - url: str | None = None warning: str | None = None @staticmethod - def from_dict(obj: Any) -> "PermissionRequest": + def from_dict(obj: Any) -> "PermissionRequestShell": assert isinstance(obj, dict) - kind = parse_enum(PermissionRequestKind, obj.get("kind")) - action = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryAction, x)], obj.get("action")) - args = obj.get("args") - can_offer_session_approval = from_union([from_none, from_bool], obj.get("canOfferSessionApproval")) - capabilities = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("capabilities")) - citations = from_union([from_none, from_str], obj.get("citations")) - commands = from_union([from_none, lambda x: from_list(PermissionRequestShellCommand.from_dict, x)], obj.get("commands")) - diff = from_union([from_none, from_str], obj.get("diff")) - direction = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryDirection, x)], obj.get("direction")) - extension_name = from_union([from_none, from_str], obj.get("extensionName")) - fact = from_union([from_none, from_str], obj.get("fact")) - file_name = from_union([from_none, from_str], obj.get("fileName")) - full_command_text = from_union([from_none, from_str], obj.get("fullCommandText")) - has_write_file_redirection = from_union([from_none, from_bool], obj.get("hasWriteFileRedirection")) - hook_message = from_union([from_none, from_str], obj.get("hookMessage")) - intention = from_union([from_none, from_str], obj.get("intention")) - new_file_contents = from_union([from_none, from_str], obj.get("newFileContents")) - operation = from_union([from_none, from_str], obj.get("operation")) - path = from_union([from_none, from_str], obj.get("path")) - possible_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("possiblePaths")) - possible_urls = from_union([from_none, lambda x: from_list(PermissionRequestShellPossibleUrl.from_dict, x)], obj.get("possibleUrls")) - read_only = from_union([from_none, from_bool], obj.get("readOnly")) - reason = from_union([from_none, from_str], obj.get("reason")) - server_name = from_union([from_none, from_str], obj.get("serverName")) - subject = from_union([from_none, from_str], obj.get("subject")) - tool_args = obj.get("toolArgs") + can_offer_session_approval = from_bool(obj.get("canOfferSessionApproval")) + commands = from_list(PermissionRequestShellCommand.from_dict, obj.get("commands")) + full_command_text = from_str(obj.get("fullCommandText")) + has_write_file_redirection = from_bool(obj.get("hasWriteFileRedirection")) + intention = from_str(obj.get("intention")) + possible_paths = from_list(from_str, obj.get("possiblePaths")) + possible_urls = from_list(PermissionRequestShellPossibleUrl.from_dict, obj.get("possibleUrls")) tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) - tool_description = from_union([from_none, from_str], obj.get("toolDescription")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - tool_title = from_union([from_none, from_str], obj.get("toolTitle")) - url = from_union([from_none, from_str], obj.get("url")) warning = from_union([from_none, from_str], obj.get("warning")) - return PermissionRequest( - kind=kind, - action=action, - args=args, + return PermissionRequestShell( can_offer_session_approval=can_offer_session_approval, - capabilities=capabilities, - citations=citations, commands=commands, - diff=diff, - direction=direction, - extension_name=extension_name, - fact=fact, - file_name=file_name, full_command_text=full_command_text, has_write_file_redirection=has_write_file_redirection, - hook_message=hook_message, intention=intention, - new_file_contents=new_file_contents, - operation=operation, - path=path, possible_paths=possible_paths, possible_urls=possible_urls, - read_only=read_only, - reason=reason, - server_name=server_name, - subject=subject, - tool_args=tool_args, tool_call_id=tool_call_id, - tool_description=tool_description, - tool_name=tool_name, - tool_title=tool_title, - url=url, warning=warning, ) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionRequestKind, self.kind) - if self.action is not None: - result["action"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryAction, x)], self.action) - if self.args is not None: - result["args"] = self.args - if self.can_offer_session_approval is not None: - result["canOfferSessionApproval"] = from_union([from_none, from_bool], self.can_offer_session_approval) - if self.capabilities is not None: - result["capabilities"] = from_union([from_none, lambda x: from_list(from_str, x)], self.capabilities) - if self.citations is not None: - result["citations"] = from_union([from_none, from_str], self.citations) - if self.commands is not None: - result["commands"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestShellCommand, x), x)], self.commands) - if self.diff is not None: - result["diff"] = from_union([from_none, from_str], self.diff) - if self.direction is not None: - result["direction"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryDirection, x)], self.direction) - if self.extension_name is not None: - result["extensionName"] = from_union([from_none, from_str], self.extension_name) - if self.fact is not None: - result["fact"] = from_union([from_none, from_str], self.fact) - if self.file_name is not None: - result["fileName"] = from_union([from_none, from_str], self.file_name) - if self.full_command_text is not None: - result["fullCommandText"] = from_union([from_none, from_str], self.full_command_text) - if self.has_write_file_redirection is not None: - result["hasWriteFileRedirection"] = from_union([from_none, from_bool], self.has_write_file_redirection) - if self.hook_message is not None: - result["hookMessage"] = from_union([from_none, from_str], self.hook_message) - if self.intention is not None: - result["intention"] = from_union([from_none, from_str], self.intention) - if self.new_file_contents is not None: - result["newFileContents"] = from_union([from_none, from_str], self.new_file_contents) - if self.operation is not None: - result["operation"] = from_union([from_none, from_str], self.operation) - if self.path is not None: - result["path"] = from_union([from_none, from_str], self.path) - if self.possible_paths is not None: - result["possiblePaths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.possible_paths) - if self.possible_urls is not None: - result["possibleUrls"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestShellPossibleUrl, x), x)], self.possible_urls) - if self.read_only is not None: - result["readOnly"] = from_union([from_none, from_bool], self.read_only) - if self.reason is not None: - result["reason"] = from_union([from_none, from_str], self.reason) - if self.server_name is not None: - result["serverName"] = from_union([from_none, from_str], self.server_name) - if self.subject is not None: - result["subject"] = from_union([from_none, from_str], self.subject) - if self.tool_args is not None: - result["toolArgs"] = self.tool_args + result["canOfferSessionApproval"] = from_bool(self.can_offer_session_approval) + result["commands"] = from_list(lambda x: to_class(PermissionRequestShellCommand, x), self.commands) + result["fullCommandText"] = from_str(self.full_command_text) + result["hasWriteFileRedirection"] = from_bool(self.has_write_file_redirection) + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["possiblePaths"] = from_list(from_str, self.possible_paths) + result["possibleUrls"] = from_list(lambda x: to_class(PermissionRequestShellPossibleUrl, x), self.possible_urls) if self.tool_call_id is not None: result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) - if self.tool_description is not None: - result["toolDescription"] = from_union([from_none, from_str], self.tool_description) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) - if self.tool_title is not None: - result["toolTitle"] = from_union([from_none, from_str], self.tool_title) - if self.url is not None: - result["url"] = from_union([from_none, from_str], self.url) if self.warning is not None: result["warning"] = from_union([from_none, from_str], self.warning) return result @@ -2221,99 +2794,108 @@ def to_dict(self) -> dict: @dataclass -class PermissionRequestedData: - "Permission request notification requiring client approval with request details" - permission_request: PermissionRequest - request_id: str - prompt_request: PermissionPromptRequest | None = None - resolved_by_hook: bool | None = None +class PermissionRequestUrl: + "URL access permission request" + intention: str + kind: ClassVar[str] = "url" + url: str + tool_call_id: str | None = None @staticmethod - def from_dict(obj: Any) -> "PermissionRequestedData": + def from_dict(obj: Any) -> "PermissionRequestUrl": assert isinstance(obj, dict) - permission_request = PermissionRequest.from_dict(obj.get("permissionRequest")) - request_id = from_str(obj.get("requestId")) - prompt_request = from_union([from_none, PermissionPromptRequest.from_dict], obj.get("promptRequest")) - resolved_by_hook = from_union([from_none, from_bool], obj.get("resolvedByHook")) - return PermissionRequestedData( - permission_request=permission_request, - request_id=request_id, - prompt_request=prompt_request, - resolved_by_hook=resolved_by_hook, + intention = from_str(obj.get("intention")) + url = from_str(obj.get("url")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestUrl( + intention=intention, + url=url, + tool_call_id=tool_call_id, ) def to_dict(self) -> dict: result: dict = {} - result["permissionRequest"] = to_class(PermissionRequest, self.permission_request) - result["requestId"] = from_str(self.request_id) - if self.prompt_request is not None: - result["promptRequest"] = from_union([from_none, lambda x: to_class(PermissionPromptRequest, x)], self.prompt_request) - if self.resolved_by_hook is not None: - result["resolvedByHook"] = from_union([from_none, from_bool], self.resolved_by_hook) + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + result["url"] = from_str(self.url) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) return result @dataclass -class PermissionResult: - "The result of the permission request" - kind: PermissionResultKind - approval: UserToolSessionApproval | None = None - feedback: str | None = None - force_reject: bool | None = None - interrupt: bool | None = None - location_key: str | None = None - message: str | None = None - path: str | None = None - reason: str | None = None - rules: list[PermissionRule] | None = None +class PermissionRequestWrite: + "File write permission request" + can_offer_session_approval: bool + diff: str + file_name: str + intention: str + kind: ClassVar[str] = "write" + new_file_contents: str | None = None + tool_call_id: str | None = None @staticmethod - def from_dict(obj: Any) -> "PermissionResult": + def from_dict(obj: Any) -> "PermissionRequestWrite": assert isinstance(obj, dict) - kind = parse_enum(PermissionResultKind, obj.get("kind")) - approval = from_union([from_none, UserToolSessionApproval.from_dict], obj.get("approval")) - feedback = from_union([from_none, from_str], obj.get("feedback")) - force_reject = from_union([from_none, from_bool], obj.get("forceReject")) - interrupt = from_union([from_none, from_bool], obj.get("interrupt")) - location_key = from_union([from_none, from_str], obj.get("locationKey")) - message = from_union([from_none, from_str], obj.get("message")) - path = from_union([from_none, from_str], obj.get("path")) - reason = from_union([from_none, from_str], obj.get("reason")) - rules = from_union([from_none, lambda x: from_list(PermissionRule.from_dict, x)], obj.get("rules")) - return PermissionResult( - kind=kind, - approval=approval, - feedback=feedback, - force_reject=force_reject, - interrupt=interrupt, - location_key=location_key, - message=message, - path=path, - reason=reason, - rules=rules, + can_offer_session_approval = from_bool(obj.get("canOfferSessionApproval")) + diff = from_str(obj.get("diff")) + file_name = from_str(obj.get("fileName")) + intention = from_str(obj.get("intention")) + new_file_contents = from_union([from_none, from_str], obj.get("newFileContents")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return PermissionRequestWrite( + can_offer_session_approval=can_offer_session_approval, + diff=diff, + file_name=file_name, + intention=intention, + new_file_contents=new_file_contents, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["canOfferSessionApproval"] = from_bool(self.can_offer_session_approval) + result["diff"] = from_str(self.diff) + result["fileName"] = from_str(self.file_name) + result["intention"] = from_str(self.intention) + result["kind"] = self.kind + if self.new_file_contents is not None: + result["newFileContents"] = from_union([from_none, from_str], self.new_file_contents) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class PermissionRequestedData: + "Permission request notification requiring client approval with request details" + permission_request: PermissionRequest + request_id: str + prompt_request: PermissionPromptRequest | None = None + resolved_by_hook: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestedData": + assert isinstance(obj, dict) + permission_request = _load_PermissionRequest(obj.get("permissionRequest")) + request_id = from_str(obj.get("requestId")) + prompt_request = from_union([from_none, _load_PermissionPromptRequest], obj.get("promptRequest")) + resolved_by_hook = from_union([from_none, from_bool], obj.get("resolvedByHook")) + return PermissionRequestedData( + permission_request=permission_request, + request_id=request_id, + prompt_request=prompt_request, + resolved_by_hook=resolved_by_hook, ) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionResultKind, self.kind) - if self.approval is not None: - result["approval"] = from_union([from_none, lambda x: to_class(UserToolSessionApproval, x)], self.approval) - if self.feedback is not None: - result["feedback"] = from_union([from_none, from_str], self.feedback) - if self.force_reject is not None: - result["forceReject"] = from_union([from_none, from_bool], self.force_reject) - if self.interrupt is not None: - result["interrupt"] = from_union([from_none, from_bool], self.interrupt) - if self.location_key is not None: - result["locationKey"] = from_union([from_none, from_str], self.location_key) - if self.message is not None: - result["message"] = from_union([from_none, from_str], self.message) - if self.path is not None: - result["path"] = from_union([from_none, from_str], self.path) - if self.reason is not None: - result["reason"] = from_union([from_none, from_str], self.reason) - if self.rules is not None: - result["rules"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRule, x), x)], self.rules) + result["permissionRequest"] = self.permission_request.to_dict() + result["requestId"] = from_str(self.request_id) + if self.prompt_request is not None: + result["promptRequest"] = from_union([from_none, lambda x: x.to_dict()], self.prompt_request) + if self.resolved_by_hook is not None: + result["resolvedByHook"] = from_union([from_none, from_bool], self.resolved_by_hook) return result @@ -3962,91 +4544,71 @@ def to_dict(self) -> dict: @dataclass -class SystemNotification: - "Structured metadata identifying what triggered this notification" - type: SystemNotificationType - agent_id: str | None = None - agent_type: str | None = None +class SystemNotificationAgentCompleted: + "Schema for the `SystemNotificationAgentCompleted` type." + agent_id: str + agent_type: str + status: SystemNotificationAgentCompletedStatus + type: ClassVar[str] = "agent_completed" description: str | None = None - entry_id: str | None = None - exit_code: int | None = None prompt: str | None = None - sender_name: str | None = None - sender_type: str | None = None - shell_id: str | None = None - source_path: str | None = None - status: SystemNotificationAgentCompletedStatus | None = None - summary: str | None = None - trigger_file: str | None = None - trigger_tool: str | None = None @staticmethod - def from_dict(obj: Any) -> "SystemNotification": + def from_dict(obj: Any) -> "SystemNotificationAgentCompleted": assert isinstance(obj, dict) - type = parse_enum(SystemNotificationType, obj.get("type")) - agent_id = from_union([from_none, from_str], obj.get("agentId")) - agent_type = from_union([from_none, from_str], obj.get("agentType")) + agent_id = from_str(obj.get("agentId")) + agent_type = from_str(obj.get("agentType")) + status = parse_enum(SystemNotificationAgentCompletedStatus, obj.get("status")) description = from_union([from_none, from_str], obj.get("description")) - entry_id = from_union([from_none, from_str], obj.get("entryId")) - exit_code = from_union([from_none, from_int], obj.get("exitCode")) prompt = from_union([from_none, from_str], obj.get("prompt")) - sender_name = from_union([from_none, from_str], obj.get("senderName")) - sender_type = from_union([from_none, from_str], obj.get("senderType")) - shell_id = from_union([from_none, from_str], obj.get("shellId")) - source_path = from_union([from_none, from_str], obj.get("sourcePath")) - status = from_union([from_none, lambda x: parse_enum(SystemNotificationAgentCompletedStatus, x)], obj.get("status")) - summary = from_union([from_none, from_str], obj.get("summary")) - trigger_file = from_union([from_none, from_str], obj.get("triggerFile")) - trigger_tool = from_union([from_none, from_str], obj.get("triggerTool")) - return SystemNotification( - type=type, + return SystemNotificationAgentCompleted( agent_id=agent_id, agent_type=agent_type, + status=status, description=description, - entry_id=entry_id, - exit_code=exit_code, prompt=prompt, - sender_name=sender_name, - sender_type=sender_type, - shell_id=shell_id, - source_path=source_path, - status=status, - summary=summary, - trigger_file=trigger_file, - trigger_tool=trigger_tool, ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(SystemNotificationType, self.type) - if self.agent_id is not None: - result["agentId"] = from_union([from_none, from_str], self.agent_id) - if self.agent_type is not None: - result["agentType"] = from_union([from_none, from_str], self.agent_type) + result["agentId"] = from_str(self.agent_id) + result["agentType"] = from_str(self.agent_type) + result["status"] = to_enum(SystemNotificationAgentCompletedStatus, self.status) + result["type"] = self.type if self.description is not None: result["description"] = from_union([from_none, from_str], self.description) - if self.entry_id is not None: - result["entryId"] = from_union([from_none, from_str], self.entry_id) - if self.exit_code is not None: - result["exitCode"] = from_union([from_none, to_int], self.exit_code) if self.prompt is not None: result["prompt"] = from_union([from_none, from_str], self.prompt) - if self.sender_name is not None: - result["senderName"] = from_union([from_none, from_str], self.sender_name) - if self.sender_type is not None: - result["senderType"] = from_union([from_none, from_str], self.sender_type) - if self.shell_id is not None: - result["shellId"] = from_union([from_none, from_str], self.shell_id) - if self.source_path is not None: - result["sourcePath"] = from_union([from_none, from_str], self.source_path) - if self.status is not None: - result["status"] = from_union([from_none, lambda x: to_enum(SystemNotificationAgentCompletedStatus, x)], self.status) - if self.summary is not None: - result["summary"] = from_union([from_none, from_str], self.summary) - if self.trigger_file is not None: - result["triggerFile"] = from_union([from_none, from_str], self.trigger_file) - if self.trigger_tool is not None: - result["triggerTool"] = from_union([from_none, from_str], self.trigger_tool) + return result + + +@dataclass +class SystemNotificationAgentIdle: + "Schema for the `SystemNotificationAgentIdle` type." + agent_id: str + agent_type: str + type: ClassVar[str] = "agent_idle" + description: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationAgentIdle": + assert isinstance(obj, dict) + agent_id = from_str(obj.get("agentId")) + agent_type = from_str(obj.get("agentType")) + description = from_union([from_none, from_str], obj.get("description")) + return SystemNotificationAgentIdle( + agent_id=agent_id, + agent_type=agent_type, + description=description, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["agentId"] = from_str(self.agent_id) + result["agentType"] = from_str(self.agent_type) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) return result @@ -4060,7 +4622,7 @@ class SystemNotificationData: def from_dict(obj: Any) -> "SystemNotificationData": assert isinstance(obj, dict) content = from_str(obj.get("content")) - kind = SystemNotification.from_dict(obj.get("kind")) + kind = _load_SystemNotification(obj.get("kind")) return SystemNotificationData( content=content, kind=kind, @@ -4069,86 +4631,252 @@ def from_dict(obj: Any) -> "SystemNotificationData": def to_dict(self) -> dict: result: dict = {} result["content"] = from_str(self.content) - result["kind"] = to_class(SystemNotification, self.kind) + result["kind"] = self.kind.to_dict() return result @dataclass -class ToolExecutionCompleteContent: - "A content block within a tool result, which may be text, terminal output, image, audio, or a resource" - type: ToolExecutionCompleteContentType - cwd: str | None = None - data: str | None = None +class SystemNotificationInstructionDiscovered: + "Schema for the `SystemNotificationInstructionDiscovered` type." + source_path: str + trigger_file: str + trigger_tool: str + type: ClassVar[str] = "instruction_discovered" + description: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationInstructionDiscovered": + assert isinstance(obj, dict) + source_path = from_str(obj.get("sourcePath")) + trigger_file = from_str(obj.get("triggerFile")) + trigger_tool = from_str(obj.get("triggerTool")) + description = from_union([from_none, from_str], obj.get("description")) + return SystemNotificationInstructionDiscovered( + source_path=source_path, + trigger_file=trigger_file, + trigger_tool=trigger_tool, + description=description, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["sourcePath"] = from_str(self.source_path) + result["triggerFile"] = from_str(self.trigger_file) + result["triggerTool"] = from_str(self.trigger_tool) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + return result + + +@dataclass +class SystemNotificationNewInboxMessage: + "Schema for the `SystemNotificationNewInboxMessage` type." + entry_id: str + sender_name: str + sender_type: str + summary: str + type: ClassVar[str] = "new_inbox_message" + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationNewInboxMessage": + assert isinstance(obj, dict) + entry_id = from_str(obj.get("entryId")) + sender_name = from_str(obj.get("senderName")) + sender_type = from_str(obj.get("senderType")) + summary = from_str(obj.get("summary")) + return SystemNotificationNewInboxMessage( + entry_id=entry_id, + sender_name=sender_name, + sender_type=sender_type, + summary=summary, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["entryId"] = from_str(self.entry_id) + result["senderName"] = from_str(self.sender_name) + result["senderType"] = from_str(self.sender_type) + result["summary"] = from_str(self.summary) + result["type"] = self.type + return result + + +@dataclass +class SystemNotificationShellCompleted: + "Schema for the `SystemNotificationShellCompleted` type." + shell_id: str + type: ClassVar[str] = "shell_completed" description: str | None = None exit_code: int | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationShellCompleted": + assert isinstance(obj, dict) + shell_id = from_str(obj.get("shellId")) + description = from_union([from_none, from_str], obj.get("description")) + exit_code = from_union([from_none, from_int], obj.get("exitCode")) + return SystemNotificationShellCompleted( + shell_id=shell_id, + description=description, + exit_code=exit_code, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["shellId"] = from_str(self.shell_id) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, to_int], self.exit_code) + return result + + +@dataclass +class SystemNotificationShellDetachedCompleted: + "Schema for the `SystemNotificationShellDetachedCompleted` type." + shell_id: str + type: ClassVar[str] = "shell_detached_completed" + description: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationShellDetachedCompleted": + assert isinstance(obj, dict) + shell_id = from_str(obj.get("shellId")) + description = from_union([from_none, from_str], obj.get("description")) + return SystemNotificationShellDetachedCompleted( + shell_id=shell_id, + description=description, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["shellId"] = from_str(self.shell_id) + result["type"] = self.type + if self.description is not None: + result["description"] = from_union([from_none, from_str], self.description) + return result + + +@dataclass +class ToolExecutionCompleteContentAudio: + "Audio content block with base64-encoded data" + data: str + mime_type: str + type: ClassVar[str] = "audio" + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteContentAudio": + assert isinstance(obj, dict) + data = from_str(obj.get("data")) + mime_type = from_str(obj.get("mimeType")) + return ToolExecutionCompleteContentAudio( + data=data, + mime_type=mime_type, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["data"] = from_str(self.data) + result["mimeType"] = from_str(self.mime_type) + result["type"] = self.type + return result + + +@dataclass +class ToolExecutionCompleteContentImage: + "Image content block with base64-encoded data" + data: str + mime_type: str + type: ClassVar[str] = "image" + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteContentImage": + assert isinstance(obj, dict) + data = from_str(obj.get("data")) + mime_type = from_str(obj.get("mimeType")) + return ToolExecutionCompleteContentImage( + data=data, + mime_type=mime_type, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["data"] = from_str(self.data) + result["mimeType"] = from_str(self.mime_type) + result["type"] = self.type + return result + + +@dataclass +class ToolExecutionCompleteContentResource: + "Embedded resource content block with inline text or binary data" + resource: ToolExecutionCompleteContentResourceDetails + type: ClassVar[str] = "resource" + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteContentResource": + assert isinstance(obj, dict) + resource = from_union([EmbeddedTextResourceContents.from_dict, EmbeddedBlobResourceContents.from_dict], obj.get("resource")) + return ToolExecutionCompleteContentResource( + resource=resource, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["resource"] = from_union([lambda x: to_class(EmbeddedTextResourceContents, x), lambda x: to_class(EmbeddedBlobResourceContents, x)], self.resource) + result["type"] = self.type + return result + + +@dataclass +class ToolExecutionCompleteContentResourceLink: + "Resource link content block referencing an external resource" + name: str + type: ClassVar[str] = "resource_link" + uri: str + description: str | None = None icons: list[ToolExecutionCompleteContentResourceLinkIcon] | None = None mime_type: str | None = None - name: str | None = None - resource: ToolExecutionCompleteContentResourceDetails | None = None size: int | None = None - text: str | None = None title: str | None = None - uri: str | None = None @staticmethod - def from_dict(obj: Any) -> "ToolExecutionCompleteContent": + def from_dict(obj: Any) -> "ToolExecutionCompleteContentResourceLink": assert isinstance(obj, dict) - type = parse_enum(ToolExecutionCompleteContentType, obj.get("type")) - cwd = from_union([from_none, from_str], obj.get("cwd")) - data = from_union([from_none, from_str], obj.get("data")) + name = from_str(obj.get("name")) + uri = from_str(obj.get("uri")) description = from_union([from_none, from_str], obj.get("description")) - exit_code = from_union([from_none, from_int], obj.get("exitCode")) icons = from_union([from_none, lambda x: from_list(ToolExecutionCompleteContentResourceLinkIcon.from_dict, x)], obj.get("icons")) mime_type = from_union([from_none, from_str], obj.get("mimeType")) - name = from_union([from_none, from_str], obj.get("name")) - resource = from_union([from_none, lambda x: from_union([EmbeddedTextResourceContents.from_dict, EmbeddedBlobResourceContents.from_dict], x)], obj.get("resource")) size = from_union([from_none, from_int], obj.get("size")) - text = from_union([from_none, from_str], obj.get("text")) title = from_union([from_none, from_str], obj.get("title")) - uri = from_union([from_none, from_str], obj.get("uri")) - return ToolExecutionCompleteContent( - type=type, - cwd=cwd, - data=data, + return ToolExecutionCompleteContentResourceLink( + name=name, + uri=uri, description=description, - exit_code=exit_code, icons=icons, mime_type=mime_type, - name=name, - resource=resource, size=size, - text=text, title=title, - uri=uri, ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(ToolExecutionCompleteContentType, self.type) - if self.cwd is not None: - result["cwd"] = from_union([from_none, from_str], self.cwd) - if self.data is not None: - result["data"] = from_union([from_none, from_str], self.data) + result["name"] = from_str(self.name) + result["type"] = self.type + result["uri"] = from_str(self.uri) if self.description is not None: result["description"] = from_union([from_none, from_str], self.description) - if self.exit_code is not None: - result["exitCode"] = from_union([from_none, to_int], self.exit_code) if self.icons is not None: result["icons"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ToolExecutionCompleteContentResourceLinkIcon, x), x)], self.icons) if self.mime_type is not None: result["mimeType"] = from_union([from_none, from_str], self.mime_type) - if self.name is not None: - result["name"] = from_union([from_none, from_str], self.name) - if self.resource is not None: - result["resource"] = from_union([from_none, lambda x: from_union([lambda x: to_class(EmbeddedTextResourceContents, x), lambda x: to_class(EmbeddedBlobResourceContents, x)], x)], self.resource) if self.size is not None: result["size"] = from_union([from_none, to_int], self.size) - if self.text is not None: - result["text"] = from_union([from_none, from_str], self.text) if self.title is not None: result["title"] = from_union([from_none, from_str], self.title) - if self.uri is not None: - result["uri"] = from_union([from_none, from_str], self.uri) return result @@ -4176,13 +4904,65 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteContentResourceLinkIcon": def to_dict(self) -> dict: result: dict = {} - result["src"] = from_str(self.src) - if self.mime_type is not None: - result["mimeType"] = from_union([from_none, from_str], self.mime_type) - if self.sizes is not None: - result["sizes"] = from_union([from_none, lambda x: from_list(from_str, x)], self.sizes) - if self.theme is not None: - result["theme"] = from_union([from_none, lambda x: to_enum(ToolExecutionCompleteContentResourceLinkIconTheme, x)], self.theme) + result["src"] = from_str(self.src) + if self.mime_type is not None: + result["mimeType"] = from_union([from_none, from_str], self.mime_type) + if self.sizes is not None: + result["sizes"] = from_union([from_none, lambda x: from_list(from_str, x)], self.sizes) + if self.theme is not None: + result["theme"] = from_union([from_none, lambda x: to_enum(ToolExecutionCompleteContentResourceLinkIconTheme, x)], self.theme) + return result + + +@dataclass +class ToolExecutionCompleteContentTerminal: + "Terminal/shell output content block with optional exit code and working directory" + text: str + type: ClassVar[str] = "terminal" + cwd: str | None = None + exit_code: int | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteContentTerminal": + assert isinstance(obj, dict) + text = from_str(obj.get("text")) + cwd = from_union([from_none, from_str], obj.get("cwd")) + exit_code = from_union([from_none, from_int], obj.get("exitCode")) + return ToolExecutionCompleteContentTerminal( + text=text, + cwd=cwd, + exit_code=exit_code, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["text"] = from_str(self.text) + result["type"] = self.type + if self.cwd is not None: + result["cwd"] = from_union([from_none, from_str], self.cwd) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, to_int], self.exit_code) + return result + + +@dataclass +class ToolExecutionCompleteContentText: + "Plain text content block" + text: str + type: ClassVar[str] = "text" + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteContentText": + assert isinstance(obj, dict) + text = from_str(obj.get("text")) + return ToolExecutionCompleteContentText( + text=text, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["text"] = from_str(self.text) + result["type"] = self.type return result @@ -4290,7 +5070,7 @@ class ToolExecutionCompleteResult: def from_dict(obj: Any) -> "ToolExecutionCompleteResult": assert isinstance(obj, dict) content = from_str(obj.get("content")) - contents = from_union([from_none, lambda x: from_list(ToolExecutionCompleteContent.from_dict, x)], obj.get("contents")) + contents = from_union([from_none, lambda x: from_list(_load_ToolExecutionCompleteContent, x)], obj.get("contents")) detailed_content = from_union([from_none, from_str], obj.get("detailedContent")) return ToolExecutionCompleteResult( content=content, @@ -4302,7 +5082,7 @@ def to_dict(self) -> dict: result: dict = {} result["content"] = from_str(self.content) if self.contents is not None: - result["contents"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ToolExecutionCompleteContent, x), x)], self.contents) + result["contents"] = from_union([from_none, lambda x: from_list(lambda x: x.to_dict(), x)], self.contents) if self.detailed_content is not None: result["detailedContent"] = from_union([from_none, from_str], self.detailed_content) return result @@ -4499,86 +5279,87 @@ def to_dict(self) -> dict: @dataclass -class UserMessageAttachment: - "A user message attachment — a file, directory, code selection, blob, or GitHub reference" - type: UserMessageAttachmentType - data: str | None = None +class UserMessageAttachmentBlob: + "Blob attachment with inline base64-encoded data" + data: str + mime_type: str + type: ClassVar[str] = "blob" display_name: str | None = None - file_path: str | None = None - line_range: UserMessageAttachmentFileLineRange | None = None - mime_type: str | None = None - number: int | None = None - path: str | None = None - reference_type: UserMessageAttachmentGithubReferenceType | None = None - selection: UserMessageAttachmentSelectionDetails | None = None - state: str | None = None - text: str | None = None - title: str | None = None - url: str | None = None @staticmethod - def from_dict(obj: Any) -> "UserMessageAttachment": + def from_dict(obj: Any) -> "UserMessageAttachmentBlob": assert isinstance(obj, dict) - type = parse_enum(UserMessageAttachmentType, obj.get("type")) - data = from_union([from_none, from_str], obj.get("data")) + data = from_str(obj.get("data")) + mime_type = from_str(obj.get("mimeType")) display_name = from_union([from_none, from_str], obj.get("displayName")) - file_path = from_union([from_none, from_str], obj.get("filePath")) - line_range = from_union([from_none, UserMessageAttachmentFileLineRange.from_dict], obj.get("lineRange")) - mime_type = from_union([from_none, from_str], obj.get("mimeType")) - number = from_union([from_none, from_int], obj.get("number")) - path = from_union([from_none, from_str], obj.get("path")) - reference_type = from_union([from_none, lambda x: parse_enum(UserMessageAttachmentGithubReferenceType, x)], obj.get("referenceType")) - selection = from_union([from_none, UserMessageAttachmentSelectionDetails.from_dict], obj.get("selection")) - state = from_union([from_none, from_str], obj.get("state")) - text = from_union([from_none, from_str], obj.get("text")) - title = from_union([from_none, from_str], obj.get("title")) - url = from_union([from_none, from_str], obj.get("url")) - return UserMessageAttachment( - type=type, + return UserMessageAttachmentBlob( data=data, - display_name=display_name, - file_path=file_path, - line_range=line_range, mime_type=mime_type, - number=number, - path=path, - reference_type=reference_type, - selection=selection, - state=state, - text=text, - title=title, - url=url, + display_name=display_name, ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(UserMessageAttachmentType, self.type) - if self.data is not None: - result["data"] = from_union([from_none, from_str], self.data) + result["data"] = from_str(self.data) + result["mimeType"] = from_str(self.mime_type) + result["type"] = self.type if self.display_name is not None: result["displayName"] = from_union([from_none, from_str], self.display_name) - if self.file_path is not None: - result["filePath"] = from_union([from_none, from_str], self.file_path) + return result + + +@dataclass +class UserMessageAttachmentDirectory: + "Directory attachment" + display_name: str + path: str + type: ClassVar[str] = "directory" + + @staticmethod + def from_dict(obj: Any) -> "UserMessageAttachmentDirectory": + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + path = from_str(obj.get("path")) + return UserMessageAttachmentDirectory( + display_name=display_name, + path=path, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["path"] = from_str(self.path) + result["type"] = self.type + return result + + +@dataclass +class UserMessageAttachmentFile: + "File attachment" + display_name: str + path: str + type: ClassVar[str] = "file" + line_range: UserMessageAttachmentFileLineRange | None = None + + @staticmethod + def from_dict(obj: Any) -> "UserMessageAttachmentFile": + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + path = from_str(obj.get("path")) + line_range = from_union([from_none, UserMessageAttachmentFileLineRange.from_dict], obj.get("lineRange")) + return UserMessageAttachmentFile( + display_name=display_name, + path=path, + line_range=line_range, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["path"] = from_str(self.path) + result["type"] = self.type if self.line_range is not None: result["lineRange"] = from_union([from_none, lambda x: to_class(UserMessageAttachmentFileLineRange, x)], self.line_range) - if self.mime_type is not None: - result["mimeType"] = from_union([from_none, from_str], self.mime_type) - if self.number is not None: - result["number"] = from_union([from_none, to_int], self.number) - if self.path is not None: - result["path"] = from_union([from_none, from_str], self.path) - if self.reference_type is not None: - result["referenceType"] = from_union([from_none, lambda x: to_enum(UserMessageAttachmentGithubReferenceType, x)], self.reference_type) - if self.selection is not None: - result["selection"] = from_union([from_none, lambda x: to_class(UserMessageAttachmentSelectionDetails, x)], self.selection) - if self.state is not None: - result["state"] = from_union([from_none, from_str], self.state) - if self.text is not None: - result["text"] = from_union([from_none, from_str], self.text) - if self.title is not None: - result["title"] = from_union([from_none, from_str], self.title) - if self.url is not None: - result["url"] = from_union([from_none, from_str], self.url) return result @@ -4605,6 +5386,76 @@ def to_dict(self) -> dict: return result +@dataclass +class UserMessageAttachmentGithubReference: + "GitHub issue, pull request, or discussion reference" + number: int + reference_type: UserMessageAttachmentGithubReferenceType + state: str + title: str + type: ClassVar[str] = "github_reference" + url: str + + @staticmethod + def from_dict(obj: Any) -> "UserMessageAttachmentGithubReference": + assert isinstance(obj, dict) + number = from_int(obj.get("number")) + reference_type = parse_enum(UserMessageAttachmentGithubReferenceType, obj.get("referenceType")) + state = from_str(obj.get("state")) + title = from_str(obj.get("title")) + url = from_str(obj.get("url")) + return UserMessageAttachmentGithubReference( + number=number, + reference_type=reference_type, + state=state, + title=title, + url=url, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["number"] = to_int(self.number) + result["referenceType"] = to_enum(UserMessageAttachmentGithubReferenceType, self.reference_type) + result["state"] = from_str(self.state) + result["title"] = from_str(self.title) + result["type"] = self.type + result["url"] = from_str(self.url) + return result + + +@dataclass +class UserMessageAttachmentSelection: + "Code selection attachment from an editor" + display_name: str + file_path: str + selection: UserMessageAttachmentSelectionDetails + text: str + type: ClassVar[str] = "selection" + + @staticmethod + def from_dict(obj: Any) -> "UserMessageAttachmentSelection": + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + file_path = from_str(obj.get("filePath")) + selection = UserMessageAttachmentSelectionDetails.from_dict(obj.get("selection")) + text = from_str(obj.get("text")) + return UserMessageAttachmentSelection( + display_name=display_name, + file_path=file_path, + selection=selection, + text=text, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["filePath"] = from_str(self.file_path) + result["selection"] = to_class(UserMessageAttachmentSelectionDetails, self.selection) + result["text"] = from_str(self.text) + result["type"] = self.type + return result + + @dataclass class UserMessageAttachmentSelectionDetails: "Position range of the selection within the file" @@ -4693,7 +5544,7 @@ def from_dict(obj: Any) -> "UserMessageData": assert isinstance(obj, dict) content = from_str(obj.get("content")) agent_mode = from_union([from_none, lambda x: parse_enum(UserMessageAgentMode, x)], obj.get("agentMode")) - attachments = from_union([from_none, lambda x: from_list(UserMessageAttachment.from_dict, x)], obj.get("attachments")) + attachments = from_union([from_none, lambda x: from_list(_load_UserMessageAttachment, x)], obj.get("attachments")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) is_autopilot_continuation = from_union([from_none, from_bool], obj.get("isAutopilotContinuation")) native_document_path_fallback_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("nativeDocumentPathFallbackPaths")) @@ -4720,7 +5571,7 @@ def to_dict(self) -> dict: if self.agent_mode is not None: result["agentMode"] = from_union([from_none, lambda x: to_enum(UserMessageAgentMode, x)], self.agent_mode) if self.attachments is not None: - result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: to_class(UserMessageAttachment, x), x)], self.attachments) + result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: x.to_dict(), x)], self.attachments) if self.interaction_id is not None: result["interactionId"] = from_union([from_none, from_str], self.interaction_id) if self.is_autopilot_continuation is not None: @@ -4739,46 +5590,163 @@ def to_dict(self) -> dict: @dataclass -class UserToolSessionApproval: - "The approval to add as a session-scoped rule" - kind: UserToolSessionApprovalKind - command_identifiers: list[str] | None = None - extension_name: str | None = None +class UserToolSessionApprovalCommands: + "Schema for the `UserToolSessionApprovalCommands` type." + command_identifiers: list[str] + kind: ClassVar[str] = "commands" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalCommands": + assert isinstance(obj, dict) + command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) + return UserToolSessionApprovalCommands( + command_identifiers=command_identifiers, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) + result["kind"] = self.kind + return result + + +@dataclass +class UserToolSessionApprovalCustomTool: + "Schema for the `UserToolSessionApprovalCustomTool` type." + kind: ClassVar[str] = "custom-tool" + tool_name: str + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalCustomTool": + assert isinstance(obj, dict) + tool_name = from_str(obj.get("toolName")) + return UserToolSessionApprovalCustomTool( + tool_name=tool_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["toolName"] = from_str(self.tool_name) + return result + + +@dataclass +class UserToolSessionApprovalExtensionManagement: + "Schema for the `UserToolSessionApprovalExtensionManagement` type." + kind: ClassVar[str] = "extension-management" operation: str | None = None - server_name: str | None = None - tool_name: str | None = None @staticmethod - def from_dict(obj: Any) -> "UserToolSessionApproval": + def from_dict(obj: Any) -> "UserToolSessionApprovalExtensionManagement": assert isinstance(obj, dict) - kind = parse_enum(UserToolSessionApprovalKind, obj.get("kind")) - command_identifiers = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("commandIdentifiers")) - extension_name = from_union([from_none, from_str], obj.get("extensionName")) operation = from_union([from_none, from_str], obj.get("operation")) - server_name = from_union([from_none, from_str], obj.get("serverName")) - tool_name = from_union([from_none, from_str], obj.get("toolName")) - return UserToolSessionApproval( - kind=kind, - command_identifiers=command_identifiers, - extension_name=extension_name, + return UserToolSessionApprovalExtensionManagement( operation=operation, - server_name=server_name, - tool_name=tool_name, ) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(UserToolSessionApprovalKind, self.kind) - if self.command_identifiers is not None: - result["commandIdentifiers"] = from_union([from_none, lambda x: from_list(from_str, x)], self.command_identifiers) - if self.extension_name is not None: - result["extensionName"] = from_union([from_none, from_str], self.extension_name) + result["kind"] = self.kind if self.operation is not None: result["operation"] = from_union([from_none, from_str], self.operation) - if self.server_name is not None: - result["serverName"] = from_union([from_none, from_str], self.server_name) - if self.tool_name is not None: - result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result + + +@dataclass +class UserToolSessionApprovalExtensionPermissionAccess: + "Schema for the `UserToolSessionApprovalExtensionPermissionAccess` type." + extension_name: str + kind: ClassVar[str] = "extension-permission-access" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalExtensionPermissionAccess": + assert isinstance(obj, dict) + extension_name = from_str(obj.get("extensionName")) + return UserToolSessionApprovalExtensionPermissionAccess( + extension_name=extension_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["extensionName"] = from_str(self.extension_name) + result["kind"] = self.kind + return result + + +@dataclass +class UserToolSessionApprovalMcp: + "Schema for the `UserToolSessionApprovalMcp` type." + kind: ClassVar[str] = "mcp" + server_name: str + tool_name: str | None + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalMcp": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return UserToolSessionApprovalMcp( + server_name=server_name, + tool_name=tool_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + result["serverName"] = from_str(self.server_name) + result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result + + +@dataclass +class UserToolSessionApprovalMemory: + "Schema for the `UserToolSessionApprovalMemory` type." + kind: ClassVar[str] = "memory" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalMemory": + assert isinstance(obj, dict) + return UserToolSessionApprovalMemory( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + return result + + +@dataclass +class UserToolSessionApprovalRead: + "Schema for the `UserToolSessionApprovalRead` type." + kind: ClassVar[str] = "read" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalRead": + assert isinstance(obj, dict) + return UserToolSessionApprovalRead( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind + return result + + +@dataclass +class UserToolSessionApprovalWrite: + "Schema for the `UserToolSessionApprovalWrite` type." + kind: ClassVar[str] = "write" + + @staticmethod + def from_dict(obj: Any) -> "UserToolSessionApprovalWrite": + assert isinstance(obj, dict) + return UserToolSessionApprovalWrite( + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = self.kind return result @@ -4836,10 +5804,142 @@ def to_dict(self) -> dict: return result +def _load_PermissionPromptRequest(obj: Any) -> "PermissionPromptRequest": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return PermissionPromptRequestCommands.from_dict(obj) + case "write": return PermissionPromptRequestWrite.from_dict(obj) + case "read": return PermissionPromptRequestRead.from_dict(obj) + case "mcp": return PermissionPromptRequestMcp.from_dict(obj) + case "url": return PermissionPromptRequestUrl.from_dict(obj) + case "memory": return PermissionPromptRequestMemory.from_dict(obj) + case "custom-tool": return PermissionPromptRequestCustomTool.from_dict(obj) + case "path": return PermissionPromptRequestPath.from_dict(obj) + case "hook": return PermissionPromptRequestHook.from_dict(obj) + case "extension-management": return PermissionPromptRequestExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionPromptRequestExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionPromptRequest kind: {kind!r}") + + +def _load_PermissionRequest(obj: Any) -> "PermissionRequest": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "shell": return PermissionRequestShell.from_dict(obj) + case "write": return PermissionRequestWrite.from_dict(obj) + case "read": return PermissionRequestRead.from_dict(obj) + case "mcp": return PermissionRequestMcp.from_dict(obj) + case "url": return PermissionRequestUrl.from_dict(obj) + case "memory": return PermissionRequestMemory.from_dict(obj) + case "custom-tool": return PermissionRequestCustomTool.from_dict(obj) + case "hook": return PermissionRequestHook.from_dict(obj) + case "extension-management": return PermissionRequestExtensionManagement.from_dict(obj) + case "extension-permission-access": return PermissionRequestExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionRequest kind: {kind!r}") + + +def _load_PermissionResult(obj: Any) -> "PermissionResult": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "approved": return PermissionApproved.from_dict(obj) + case "approved-for-session": return PermissionApprovedForSession.from_dict(obj) + case "approved-for-location": return PermissionApprovedForLocation.from_dict(obj) + case "cancelled": return PermissionCancelled.from_dict(obj) + case "denied-by-rules": return PermissionDeniedByRules.from_dict(obj) + case "denied-no-approval-rule-and-could-not-request-from-user": return PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser.from_dict(obj) + case "denied-interactively-by-user": return PermissionDeniedInteractivelyByUser.from_dict(obj) + case "denied-by-content-exclusion-policy": return PermissionDeniedByContentExclusionPolicy.from_dict(obj) + case "denied-by-permission-request-hook": return PermissionDeniedByPermissionRequestHook.from_dict(obj) + case _: raise ValueError(f"Unknown PermissionResult kind: {kind!r}") + + +def _load_SystemNotification(obj: Any) -> "SystemNotification": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "agent_completed": return SystemNotificationAgentCompleted.from_dict(obj) + case "agent_idle": return SystemNotificationAgentIdle.from_dict(obj) + case "new_inbox_message": return SystemNotificationNewInboxMessage.from_dict(obj) + case "shell_completed": return SystemNotificationShellCompleted.from_dict(obj) + case "shell_detached_completed": return SystemNotificationShellDetachedCompleted.from_dict(obj) + case "instruction_discovered": return SystemNotificationInstructionDiscovered.from_dict(obj) + case _: raise ValueError(f"Unknown SystemNotification type: {kind!r}") + + +def _load_ToolExecutionCompleteContent(obj: Any) -> "ToolExecutionCompleteContent": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "text": return ToolExecutionCompleteContentText.from_dict(obj) + case "terminal": return ToolExecutionCompleteContentTerminal.from_dict(obj) + case "image": return ToolExecutionCompleteContentImage.from_dict(obj) + case "audio": return ToolExecutionCompleteContentAudio.from_dict(obj) + case "resource_link": return ToolExecutionCompleteContentResourceLink.from_dict(obj) + case "resource": return ToolExecutionCompleteContentResource.from_dict(obj) + case _: raise ValueError(f"Unknown ToolExecutionCompleteContent type: {kind!r}") + + +def _load_UserMessageAttachment(obj: Any) -> "UserMessageAttachment": + assert isinstance(obj, dict) + kind = obj.get("type") + match kind: + case "file": return UserMessageAttachmentFile.from_dict(obj) + case "directory": return UserMessageAttachmentDirectory.from_dict(obj) + case "selection": return UserMessageAttachmentSelection.from_dict(obj) + case "github_reference": return UserMessageAttachmentGithubReference.from_dict(obj) + case "blob": return UserMessageAttachmentBlob.from_dict(obj) + case _: raise ValueError(f"Unknown UserMessageAttachment type: {kind!r}") + + +def _load_UserToolSessionApproval(obj: Any) -> "UserToolSessionApproval": + assert isinstance(obj, dict) + kind = obj.get("kind") + match kind: + case "commands": return UserToolSessionApprovalCommands.from_dict(obj) + case "read": return UserToolSessionApprovalRead.from_dict(obj) + case "write": return UserToolSessionApprovalWrite.from_dict(obj) + case "mcp": return UserToolSessionApprovalMcp.from_dict(obj) + case "memory": return UserToolSessionApprovalMemory.from_dict(obj) + case "custom-tool": return UserToolSessionApprovalCustomTool.from_dict(obj) + case "extension-management": return UserToolSessionApprovalExtensionManagement.from_dict(obj) + case "extension-permission-access": return UserToolSessionApprovalExtensionPermissionAccess.from_dict(obj) + case _: raise ValueError(f"Unknown UserToolSessionApproval kind: {kind!r}") + + +# A content block within a tool result, which may be text, terminal output, image, audio, or a resource +ToolExecutionCompleteContent = ToolExecutionCompleteContentText | ToolExecutionCompleteContentTerminal | ToolExecutionCompleteContentImage | ToolExecutionCompleteContentAudio | ToolExecutionCompleteContentResourceLink | ToolExecutionCompleteContentResource + + +# A user message attachment — a file, directory, code selection, blob, or GitHub reference +UserMessageAttachment = UserMessageAttachmentFile | UserMessageAttachmentDirectory | UserMessageAttachmentSelection | UserMessageAttachmentGithubReference | UserMessageAttachmentBlob + + +# Derived user-facing permission prompt details for UI consumers +PermissionPromptRequest = PermissionPromptRequestCommands | PermissionPromptRequestWrite | PermissionPromptRequestRead | PermissionPromptRequestMcp | PermissionPromptRequestUrl | PermissionPromptRequestMemory | PermissionPromptRequestCustomTool | PermissionPromptRequestPath | PermissionPromptRequestHook | PermissionPromptRequestExtensionManagement | PermissionPromptRequestExtensionPermissionAccess + + +# Details of the permission being requested +PermissionRequest = PermissionRequestShell | PermissionRequestWrite | PermissionRequestRead | PermissionRequestMcp | PermissionRequestUrl | PermissionRequestMemory | PermissionRequestCustomTool | PermissionRequestHook | PermissionRequestExtensionManagement | PermissionRequestExtensionPermissionAccess + + +# Structured metadata identifying what triggered this notification +SystemNotification = SystemNotificationAgentCompleted | SystemNotificationAgentIdle | SystemNotificationNewInboxMessage | SystemNotificationShellCompleted | SystemNotificationShellDetachedCompleted | SystemNotificationInstructionDiscovered + + +# The approval to add as a session-scoped rule +UserToolSessionApproval = UserToolSessionApprovalCommands | UserToolSessionApprovalRead | UserToolSessionApprovalWrite | UserToolSessionApprovalMcp | UserToolSessionApprovalMemory | UserToolSessionApprovalCustomTool | UserToolSessionApprovalExtensionManagement | UserToolSessionApprovalExtensionPermissionAccess + + # The embedded resource contents, either text or base64-encoded binary ToolExecutionCompleteContentResourceDetails = EmbeddedTextResourceContents | EmbeddedBlobResourceContents +# The result of the permission request +PermissionResult = PermissionApproved | PermissionApprovedForSession | PermissionApprovedForLocation | PermissionCancelled | PermissionDeniedByRules | PermissionDeniedNoApprovalRuleAndCouldNotRequestFromUser | PermissionDeniedInteractivelyByUser | PermissionDeniedByContentExclusionPolicy | PermissionDeniedByPermissionRequestHook + + class AbortReason(Enum): "Finite reason code describing why the current turn was aborted" # The local user requested the abort, for example by pressing Ctrl+C in the CLI. @@ -4976,21 +6076,6 @@ class ModelCallFailureSource(Enum): MCP_SAMPLING = "mcp_sampling" -class PermissionPromptRequestKind(Enum): - "Derived user-facing permission prompt details for UI consumers discriminator" - COMMANDS = "commands" - WRITE = "write" - READ = "read" - MCP = "mcp" - URL = "url" - MEMORY = "memory" - CUSTOM_TOOL = "custom-tool" - PATH = "path" - HOOK = "hook" - EXTENSION_MANAGEMENT = "extension-management" - EXTENSION_PERMISSION_ACCESS = "extension-permission-access" - - class PermissionPromptRequestPathAccessKind(Enum): "Underlying permission kind that needs path approval" # Read access to a filesystem path. @@ -5001,20 +6086,6 @@ class PermissionPromptRequestPathAccessKind(Enum): WRITE = "write" -class PermissionRequestKind(Enum): - "Details of the permission being requested discriminator" - SHELL = "shell" - WRITE = "write" - READ = "read" - MCP = "mcp" - URL = "url" - MEMORY = "memory" - CUSTOM_TOOL = "custom-tool" - HOOK = "hook" - EXTENSION_MANAGEMENT = "extension-management" - EXTENSION_PERMISSION_ACCESS = "extension-permission-access" - - class PermissionRequestMemoryAction(Enum): "Whether this is a store or vote memory operation" # Store a new memory. @@ -5031,19 +6102,6 @@ class PermissionRequestMemoryDirection(Enum): DOWNVOTE = "downvote" -class PermissionResultKind(Enum): - "The result of the permission request discriminator" - APPROVED = "approved" - APPROVED_FOR_SESSION = "approved-for-session" - APPROVED_FOR_LOCATION = "approved-for-location" - CANCELLED = "cancelled" - DENIED_BY_RULES = "denied-by-rules" - DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user" - DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user" - DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy" - DENIED_BY_PERMISSION_REQUEST_HOOK = "denied-by-permission-request-hook" - - class PlanChangedOperation(Enum): "The type of operation performed on the plan file" # The plan file was created. @@ -5116,16 +6174,6 @@ class SystemNotificationAgentCompletedStatus(Enum): FAILED = "failed" -class SystemNotificationType(Enum): - "Structured metadata identifying what triggered this notification discriminator" - AGENT_COMPLETED = "agent_completed" - AGENT_IDLE = "agent_idle" - NEW_INBOX_MESSAGE = "new_inbox_message" - SHELL_COMPLETED = "shell_completed" - SHELL_DETACHED_COMPLETED = "shell_detached_completed" - INSTRUCTION_DISCOVERED = "instruction_discovered" - - class ToolExecutionCompleteContentResourceLinkIconTheme(Enum): "Theme variant this icon is intended for" # Icon intended for light themes. @@ -5134,16 +6182,6 @@ class ToolExecutionCompleteContentResourceLinkIconTheme(Enum): DARK = "dark" -class ToolExecutionCompleteContentType(Enum): - "A content block within a tool result, which may be text, terminal output, image, audio, or a resource discriminator" - TEXT = "text" - TERMINAL = "terminal" - IMAGE = "image" - AUDIO = "audio" - RESOURCE_LINK = "resource_link" - RESOURCE = "resource" - - class UserMessageAgentMode(Enum): "The agent mode that was active when this message was sent" # The agent is responding interactively to the user. @@ -5166,27 +6204,6 @@ class UserMessageAttachmentGithubReferenceType(Enum): DISCUSSION = "discussion" -class UserMessageAttachmentType(Enum): - "A user message attachment — a file, directory, code selection, blob, or GitHub reference discriminator" - FILE = "file" - DIRECTORY = "directory" - SELECTION = "selection" - GITHUB_REFERENCE = "github_reference" - BLOB = "blob" - - -class UserToolSessionApprovalKind(Enum): - "The approval to add as a session-scoped rule discriminator" - COMMANDS = "commands" - READ = "read" - WRITE = "write" - MCP = "mcp" - MEMORY = "memory" - CUSTOM_TOOL = "custom-tool" - EXTENSION_MANAGEMENT = "extension-management" - EXTENSION_PERMISSION_ACCESS = "extension-permission-access" - - class WorkingDirectoryContextHostType(Enum): "Hosting platform type of the repository (github or ado)" # Repository is hosted on GitHub. diff --git a/python/copilot/session.py b/python/copilot/session.py index caf2e3020..9798835e9 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -18,6 +18,7 @@ import time from collections.abc import Awaitable, Callable from dataclasses import dataclass +from datetime import UTC, datetime from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, NotRequired, Required, TypedDict, cast @@ -32,8 +33,9 @@ LogRequest, ModelSwitchToRequest, PermissionDecision, - PermissionDecisionKind, + PermissionDecisionApproveOnce, PermissionDecisionRequest, + PermissionDecisionUserNotAvailable, SessionLogLevel, SessionRpc, UIElicitationRequest, @@ -231,19 +233,26 @@ class SystemMessageCustomizeConfig(TypedDict, total=False): # Permission Types # ============================================================================ -PermissionRequestResultKind = Literal[ - "approve-once", - "reject", - "user-not-available", - "no-result", -] - @dataclass -class PermissionRequestResult: - """Result of a permission request.""" +class PermissionNoResult: + """Sentinel returned by a permission handler to leave the request unanswered. + + Only meaningful against protocol-v1 servers. v2 servers reject ``no-result`` + responses; the SDK raises :class:`ValueError` if a v2 server receives one. + Mirrors the ``{kind: "no-result"}`` extension TS adds to its ``PermissionDecision`` + union (see ``nodejs/src/types.ts:883``). + """ + + kind: Literal["no-result"] = "no-result" + - kind: PermissionRequestResultKind = "user-not-available" +# The decision returned by a permission handler. Identical shape to the wire +# ``PermissionDecision`` discriminated union, plus a :class:`PermissionNoResult` +# sentinel for v1 servers. Construct via the generated variant classes: +# ``PermissionDecisionApproveOnce(kind=...)``, ``PermissionDecisionReject(kind=..., +# feedback=...)``, etc. +PermissionRequestResult = PermissionDecision | PermissionNoResult _PermissionHandlerFn = Callable[ @@ -257,7 +266,7 @@ class PermissionHandler: def approve_all( request: PermissionRequest, invocation: dict[str, str] ) -> PermissionRequestResult: - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() # ============================================================================ @@ -619,7 +628,7 @@ class PreToolUseHookInput(TypedDict): """Input for pre-tool-use hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str toolName: str toolArgs: Any @@ -645,7 +654,7 @@ class PreMcpToolCallHookInput(TypedDict): """Input for pre-MCP-tool-call hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str serverName: str toolName: str @@ -676,7 +685,7 @@ class PostToolUseHookInput(TypedDict): """Input for post-tool-use hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str toolName: str toolArgs: Any @@ -701,7 +710,7 @@ class UserPromptSubmittedHookInput(TypedDict): """Input for user-prompt-submitted hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str prompt: str @@ -724,7 +733,7 @@ class SessionStartHookInput(TypedDict): """Input for session-start hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str source: Literal["startup", "resume", "new"] initialPrompt: NotRequired[str] @@ -747,7 +756,7 @@ class SessionEndHookInput(TypedDict): """Input for session-end hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str reason: Literal["complete", "error", "abort", "timeout", "user_exit"] finalMessage: NotRequired[str] @@ -772,7 +781,7 @@ class ErrorOccurredHookInput(TypedDict): """Input for error-occurred hook""" sessionId: str - timestamp: int + timestamp: datetime workingDirectory: str error: str errorContext: Literal["model_call", "tool_execution", "system", "user_input"] @@ -929,193 +938,12 @@ class ProviderConfig(TypedDict, total=False): # triggers conversation compaction before sending a request when the prompt # (system message, history, tool definitions, user message) would exceed # this limit. - max_input_tokens: int + max_prompt_tokens: int # Overrides the resolved model's default max output tokens. When hit, the # model stops generating and returns a truncated response. max_output_tokens: int -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. When specified, only these tools will be available. - # Applies to the full merged tool catalog (built-in, MCP, and custom tools - # registered via tools=). Takes precedence over excluded_tools. - available_tools: list[str] - # List of tool names to disable. Applies to all tools including custom tools - # registered via tools=. Ignored if available_tools is set. - excluded_tools: list[str] - # Optional handler for permission requests from the server. When omitted, - # requests are surfaced as events and left pending for manual resolution. - on_permission_request: _PermissionHandlerFn | None - # 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 - # Enables or disables internal session telemetry for this session. When False, - # disables session telemetry. When omitted (the default) or True, telemetry is enabled for - # GitHub-authenticated sessions. When a custom provider (BYOK) is configured, - # session telemetry is always disabled regardless of this setting. - # This is independent of the client OpenTelemetry configuration. - enable_session_telemetry: bool - # 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 - # Include sub-agent streaming events in the event stream. When True, streaming - # delta events from sub-agents (e.g., assistant.message_delta, - # assistant.reasoning_delta, assistant.streaming_delta with agentId set) are - # forwarded to this connection. When False, only non-streaming sub-agent events - # and subagent.* lifecycle events are forwarded; streaming deltas from sub-agents - # are suppressed. Defaults to True. - include_sub_agent_streaming_events: bool - # MCP server configurations for the session - mcp_servers: dict[str, MCPServerConfig] - # Custom agent configurations for the session - custom_agents: list[CustomAgentConfig] - # Configuration for the default agent. - # Use excluded_tools to hide tools from the default agent - # while keeping them available to sub-agents. - default_agent: DefaultAgentConfig - # 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 - # 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] - # Additional directories to search for custom instruction files. - instruction_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 - # Optional event handler that is registered on the session before the - # session.create RPC is issued, ensuring early events (e.g. session.start) - # are delivered. Equivalent to calling session.on(handler) immediately - # after creation, but executes earlier in the lifecycle so no events are missed. - on_event: Callable[[SessionEvent], None] - # Slash commands to register with the session. - # When the CLI has a TUI, each command appears as /name for the user to invoke. - commands: list[CommandDefinition] - # Handler for elicitation requests from the server. - # When provided, the server calls back to this client for form-based UI dialogs. - on_elicitation_request: ElicitationHandler - # Handler for exit-plan-mode requests from the server. - on_exit_plan_mode: ExitPlanModeHandler - # Handler for auto-mode-switch requests from the server. - on_auto_mode_switch: AutoModeSwitchHandler - # Handler factory for session-scoped sessionFs operations. - create_session_fs_handler: CreateSessionFsHandler - - -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. When specified, only these tools will be available. - # Applies to the full merged tool catalog (built-in, MCP, and custom tools - # registered via tools=). Takes precedence over excluded_tools. - available_tools: list[str] - # List of tool names to disable. Applies to all tools including custom tools - # registered via tools=. Ignored if available_tools is set. - excluded_tools: list[str] - provider: ProviderConfig - # Enables or disables internal session telemetry for this session. When False, - # disables session telemetry. When omitted (the default) or True, telemetry is enabled for - # GitHub-authenticated sessions. When a custom provider (BYOK) is configured, - # session telemetry is always disabled regardless of this setting. - # This is independent of the client OpenTelemetry configuration. - enable_session_telemetry: bool - # Reasoning effort level for models that support it. - reasoning_effort: ReasoningEffort - # Optional handler for permission requests from the server. When omitted, - # requests are surfaced as events and left pending for manual resolution. - on_permission_request: _PermissionHandlerFn | None - # 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 - # Include sub-agent streaming events in the event stream. When True, streaming - # delta events from sub-agents (e.g., assistant.message_delta, - # assistant.reasoning_delta, assistant.streaming_delta with agentId set) are - # forwarded to this connection. When False, only non-streaming sub-agent events - # and subagent.* lifecycle events are forwarded; streaming deltas from sub-agents - # are suppressed. Defaults to True. - include_sub_agent_streaming_events: bool - # MCP server configurations for the session - mcp_servers: dict[str, MCPServerConfig] - # Custom agent configurations for the session - custom_agents: list[CustomAgentConfig] - # Configuration for the default agent. - default_agent: DefaultAgentConfig - # 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] - # Additional directories to search for custom instruction files. - instruction_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 - # When True, instructs the runtime to continue any tool calls or permission prompts - # that were still pending when the session was last suspended. When False (the - # default), the runtime treats pending work as interrupted on resume. - # - # For permission requests, the runtime re-emits ``permission.requested`` so the - # registered ``on_permission_request`` handler can re-prompt; for external tool - # calls, the consumer is expected to supply the result via the corresponding - # low-level RPC method. - continue_pending_work: bool - # Optional event handler registered before the session.resume RPC is issued, - # ensuring early events are delivered. See SessionConfig.on_event. - on_event: Callable[[SessionEvent], None] - # Slash commands to register with the session. - commands: list[CommandDefinition] - # Handler for elicitation requests from the server. - on_elicitation_request: ElicitationHandler - # Handler for exit-plan-mode requests from the server. - on_exit_plan_mode: ExitPlanModeHandler - # Handler for auto-mode-switch requests from the server. - on_auto_mode_switch: AutoModeSwitchHandler - # Handler factory for session-scoped sessionFs operations. - create_session_fs_handler: CreateSessionFsHandler - - SessionEventHandler = Callable[[SessionEvent], None] @@ -1703,18 +1531,14 @@ async def _execute_permission_and_respond( ) result = cast(PermissionRequestResult, result) - if result.kind == "no-result": + if isinstance(result, PermissionNoResult): return - perm_result = PermissionDecision( - kind=PermissionDecisionKind(result.kind), - ) - rpc_start = time.perf_counter() await self.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id=request_id, - result=perm_result, + result=result, ) ) log_timing( @@ -1730,9 +1554,7 @@ async def _execute_permission_and_respond( await self.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id=request_id, - result=PermissionDecision( - kind=PermissionDecisionKind.USER_NOT_AVAILABLE, - ), + result=PermissionDecisionUserNotAvailable(), ) ) except (JsonRpcError, ProcessExitedError, OSError): @@ -1995,8 +1817,8 @@ async def _handle_permission_request( handler = self._permission_handler if not handler: - # No handler registered, deny permission - return PermissionRequestResult() + # No handler registered, deny permission. + return PermissionDecisionUserNotAvailable() try: handler_start = time.perf_counter() @@ -2012,13 +1834,13 @@ async def _handle_permission_request( ) return cast(PermissionRequestResult, result) except Exception: # pylint: disable=broad-except - # Handler failed, deny permission + # Handler failed, deny permission. logger.debug( "Error handling permission request", extra={"session_id": self.session_id}, exc_info=True, ) - return PermissionRequestResult() + return PermissionDecisionUserNotAvailable() def _register_user_input_handler(self, handler: UserInputHandler | None) -> None: """ @@ -2226,9 +2048,21 @@ async def _handle_hooks_invoke(self, hook_type: str, input_data: Any) -> Any: try: handler_start = time.perf_counter() - # Remap wire key "cwd" to public API key "workingDirectory" - if "cwd" in input_data: - input_data = {**input_data, "workingDirectory": input_data.pop("cwd")} + # Normalize input from the wire format: + # - Remap wire key "cwd" to public API key "workingDirectory". + # - Convert "timestamp" from epoch milliseconds to ``datetime`` so + # hook handlers see a timezone-aware ``datetime`` rather than a + # raw integer (matches TS PR #1357 Phase E). + transformed: dict[str, Any] = dict(input_data) + if "cwd" in transformed: + transformed["workingDirectory"] = transformed.pop("cwd") + timestamp = transformed.get("timestamp") + if isinstance(timestamp, (int, float)): + transformed["timestamp"] = datetime.fromtimestamp(timestamp / 1000, tz=UTC) + # Each per-hook-type TypedDict is structurally compatible with the + # normalized dict; cast to ``Any`` so ty doesn't try to narrow the + # specific TypedDict variant from the runtime ``dict``. + input_data = cast(Any, transformed) result = handler(input_data, {"session_id": self.session_id}) if inspect.isawaitable(result): result = await result @@ -2250,7 +2084,7 @@ async def _handle_hooks_invoke(self, hook_type: str, input_data: Any) -> Any: ) return None - async def get_messages(self) -> list[SessionEvent]: + async def get_events(self) -> list[SessionEvent]: """ Retrieve all events and messages from this session's history. @@ -2265,7 +2099,7 @@ async def get_messages(self) -> list[SessionEvent]: Example: >>> from copilot.generated.session_events import AssistantMessageData - >>> events = await session.get_messages() + >>> events = await session.get_events() >>> for event in events: ... match event.data: ... case AssistantMessageData() as data: @@ -2325,26 +2159,6 @@ async def disconnect(self) -> None: with self._auto_mode_switch_handler_lock: self._auto_mode_switch_handler = None - async def destroy(self) -> None: - """ - .. deprecated:: - Use :meth:`disconnect` instead. This method will be removed in a future release. - - Disconnect this session and release all in-memory resources. - Session data on disk is preserved for later resumption. - - Raises: - Exception: If the connection fails. - """ - import warnings - - warnings.warn( - "destroy() is deprecated, use disconnect() instead", - DeprecationWarning, - stacklevel=2, - ) - await self.disconnect() - async def __aenter__(self) -> CopilotSession: """Enable use as an async context manager.""" return self diff --git a/python/copilot/tools.py b/python/copilot/tools.py index 3f8eb9c1b..c6a29dc61 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -24,7 +24,7 @@ class ToolBinaryResult: data: str = "" mime_type: str = "" - type: str = "" + type: Literal["image", "resource"] = "image" description: str = "" diff --git a/python/e2e/test_agent_and_compact_rpc_e2e.py b/python/e2e/test_agent_and_compact_rpc_e2e.py index f4773a798..14ea01ff2 100644 --- a/python/e2e/test_agent_and_compact_rpc_e2e.py +++ b/python/e2e/test_agent_and_compact_rpc_e2e.py @@ -4,8 +4,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.rpc import AgentSelectRequest from copilot.session import PermissionHandler @@ -18,7 +17,7 @@ class TestAgentSelectionRpc: @pytest.mark.asyncio async def test_should_list_available_custom_agents(self): """Test listing available custom agents via RPC.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -56,7 +55,7 @@ async def test_should_list_available_custom_agents(self): @pytest.mark.asyncio async def test_should_return_null_when_no_agent_is_selected(self): """Test getCurrent returns null when no agent is selected.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -83,7 +82,7 @@ async def test_should_return_null_when_no_agent_is_selected(self): @pytest.mark.asyncio async def test_should_select_and_get_current_agent(self): """Test selecting an agent and verifying getCurrent returns it.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -118,7 +117,7 @@ async def test_should_select_and_get_current_agent(self): @pytest.mark.asyncio async def test_should_deselect_current_agent(self): """Test deselecting the current agent.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -150,7 +149,7 @@ async def test_should_deselect_current_agent(self): @pytest.mark.asyncio async def test_should_return_empty_list_when_no_custom_agents_configured(self): """Test listing agents returns no custom agents when none configured.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -175,7 +174,7 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): @pytest.mark.asyncio async def test_should_call_agent_reload(self): """Test reloading agents via RPC.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) reload_agent = { "name": f"reload-test-agent-{uuid.uuid4().hex}", "display_name": "Reload Agent", diff --git a/python/e2e/test_client_api_e2e.py b/python/e2e/test_client_api_e2e.py index 1699bb8cf..217352817 100644 --- a/python/e2e/test_client_api_e2e.py +++ b/python/e2e/test_client_api_e2e.py @@ -44,6 +44,13 @@ async def test_should_get_null_last_session_id_before_any_sessions_exist( self, ctx: E2ETestContext ): await ctx.client.start() + + # Other tests in this class create sessions, and pytest doesn't + # guarantee test execution order. Clear any leftover sessions so this + # test sees a genuinely empty state regardless of order. + for existing in await ctx.client.list_sessions(): + await ctx.client.delete_session(existing.session_id) + result = await ctx.client.get_last_session_id() assert result is None diff --git a/python/e2e/test_client_e2e.py b/python/e2e/test_client_e2e.py index fc7315a58..1e8ea82e5 100644 --- a/python/e2e/test_client_e2e.py +++ b/python/e2e/test_client_e2e.py @@ -2,14 +2,13 @@ import pytest -from copilot import CopilotClient +from copilot import CopilotClient, RuntimeConnection from copilot.client import ( ModelCapabilities, ModelInfo, ModelLimits, ModelSupports, StopError, - SubprocessConfig, ) from copilot.session import PermissionHandler @@ -19,35 +18,31 @@ class TestClient: @pytest.mark.asyncio async def test_should_start_and_connect_to_server_using_stdio(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() - assert client.get_state() == "connected" pong = await client.ping("test message") assert pong.message == "pong: test message" assert pong.timestamp is not None await client.stop() - assert client.get_state() == "disconnected" finally: await client.force_stop() @pytest.mark.asyncio async def test_should_start_and_connect_to_server_using_tcp(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=False)) + client = CopilotClient(connection=RuntimeConnection.for_tcp(path=CLI_PATH)) try: await client.start() - assert client.get_state() == "connected" pong = await client.ping("test message") assert pong.message == "pong: test message" assert pong.timestamp is not None await client.stop() - assert client.get_state() == "disconnected" finally: await client.force_stop() @@ -55,7 +50,7 @@ async def test_should_start_and_connect_to_server_using_tcp(self): async def test_should_raise_exception_group_on_failed_cleanup(self): import asyncio - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.create_session(on_permission_request=PermissionHandler.approve_all) @@ -72,22 +67,19 @@ async def test_should_raise_exception_group_on_failed_cleanup(self): assert len(exc.exceptions) > 0 assert isinstance(exc.exceptions[0], StopError) assert "Failed to disconnect session" in exc.exceptions[0].message - else: - assert client.get_state() == "disconnected" finally: await client.force_stop() @pytest.mark.asyncio async def test_should_force_stop_without_cleanup(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.create_session(on_permission_request=PermissionHandler.approve_all) await client.force_stop() - assert client.get_state() == "disconnected" @pytest.mark.asyncio async def test_should_get_status_with_version_and_protocol_info(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -95,9 +87,9 @@ async def test_should_get_status_with_version_and_protocol_info(self): status = await client.get_status() assert hasattr(status, "version") assert isinstance(status.version, str) - assert hasattr(status, "protocolVersion") - assert isinstance(status.protocolVersion, int) - assert status.protocolVersion >= 1 + assert hasattr(status, "protocol_version") + assert isinstance(status.protocol_version, int) + assert status.protocol_version >= 1 await client.stop() finally: @@ -105,7 +97,7 @@ async def test_should_get_status_with_version_and_protocol_info(self): @pytest.mark.asyncio async def test_should_get_auth_status(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -123,7 +115,7 @@ async def test_should_get_auth_status(self): @pytest.mark.asyncio async def test_should_list_models_when_authenticated(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -151,7 +143,7 @@ async def test_should_list_models_when_authenticated(self): @pytest.mark.asyncio async def test_should_cache_models_list(self): """Test that list_models caches results to avoid rate limiting""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -196,10 +188,8 @@ async def test_should_cache_models_list(self): async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): """Test that CLI startup errors include stderr output in the error message.""" client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - cli_args=["--nonexistent-flag-for-testing"], - use_stdio=True, + connection=RuntimeConnection.for_stdio( + path=CLI_PATH, args=["--nonexistent-flag-for-testing"] ) ) @@ -231,7 +221,7 @@ async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): @pytest.mark.asyncio async def test_should_not_throw_when_disposing_session_after_stopping_client(self): """Disconnecting a session after the client is stopped must not raise.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -250,7 +240,7 @@ async def test_should_not_throw_when_disposing_session_after_stopping_client(sel @pytest.mark.asyncio async def test_should_create_session_without_permission_handler(self): """`create_session` allows omitting an `on_permission_request` handler.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -265,7 +255,7 @@ async def test_should_create_session_without_permission_handler(self): @pytest.mark.asyncio async def test_should_resume_session_without_permission_handler(self): """`resume_session` allows omitting an `on_permission_request` handler.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -300,7 +290,7 @@ def on_list_models(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH, use_stdio=True), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=on_list_models, ) @@ -338,7 +328,7 @@ def on_list_models(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH, use_stdio=True), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=on_list_models, ) diff --git a/python/e2e/test_client_lifecycle_e2e.py b/python/e2e/test_client_lifecycle_e2e.py index 90b96d822..d5a2fb681 100644 --- a/python/e2e/test_client_lifecycle_e2e.py +++ b/python/e2e/test_client_lifecycle_e2e.py @@ -1,5 +1,5 @@ """ -Client lifecycle tests covering ``client.on(...)`` lifecycle event subscriptions +Client lifecycle tests covering ``client.on_lifecycle(...)`` lifecycle event subscriptions and connection-state transitions across ``start``/``stop``. Mirrors ``dotnet/test/ClientLifecycleTests.cs`` plus the existing ``client_lifecycle`` @@ -14,8 +14,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness import E2ETestContext @@ -60,12 +59,10 @@ def _make_isolated_client(ctx: E2ETestContext) -> CopilotClient: "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) return CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) @@ -84,7 +81,7 @@ async def test_should_return_last_session_id_after_sending_a_message(self, ctx: async def test_should_emit_session_lifecycle_events(self, ctx: E2ETestContext): events: list = [] - unsubscribe = ctx.client.on(events.append) + unsubscribe = ctx.client.on_lifecycle(events.append) try: session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -94,7 +91,7 @@ async def test_should_emit_session_lifecycle_events(self, ctx: E2ETestContext): await _wait_for_condition( lambda: any( - getattr(e, "sessionId", None) == session.session_id for e in events + getattr(e, "session_id", None) == session.session_id for e in events ), timeout=10.0, ) @@ -111,7 +108,7 @@ def handler(event): if event.type == "session.created" and not created.done(): created.set_result(event) - unsubscribe = ctx.client.on(handler) + unsubscribe = ctx.client.on_lifecycle(handler) try: session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -119,7 +116,7 @@ def handler(event): try: event = await asyncio.wait_for(created, 10.0) assert event.type == "session.created" - assert event.sessionId == session.session_id + assert event.session_id == session.session_id finally: await session.disconnect() finally: @@ -133,7 +130,7 @@ def handler(event): if not created.done(): created.set_result(event) - unsubscribe = ctx.client.on("session.created", handler) + unsubscribe = ctx.client.on_lifecycle("session.created", handler) try: session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -141,7 +138,7 @@ def handler(event): try: event = await asyncio.wait_for(created, 10.0) assert event.type == "session.created" - assert event.sessionId == session.session_id + assert event.session_id == session.session_id finally: await session.disconnect() finally: @@ -157,11 +154,11 @@ def disposed_handler(_event): nonlocal unsubscribed_count unsubscribed_count += 1 - unsubscribe_disposed = ctx.client.on(disposed_handler) + unsubscribe_disposed = ctx.client.on_lifecycle(disposed_handler) unsubscribe_disposed() # Immediately dispose first subscription. active_event: asyncio.Future = loop.create_future() - unsubscribe_active = ctx.client.on( + unsubscribe_active = ctx.client.on_lifecycle( "session.created", lambda evt: active_event.set_result(evt) if not active_event.done() else None, ) @@ -171,7 +168,7 @@ def disposed_handler(_event): ) try: event = await asyncio.wait_for(active_event, 10.0) - assert event.sessionId == session.session_id + assert event.session_id == session.session_id assert unsubscribed_count == 0, "Disposed handler should not have fired" finally: await session.disconnect() @@ -181,12 +178,7 @@ def disposed_handler(_event): async def test_stop_disconnects_client_and_disposes_rpc_surface(self, ctx: E2ETestContext): client = _make_isolated_client(ctx) await client.start() - try: - assert client.get_state() == "connected" - finally: - await client.stop() - - assert client.get_state() == "disconnected" + await client.stop() with pytest.raises(RuntimeError): _ = client.rpc @@ -207,17 +199,17 @@ async def test_should_receive_session_updated_lifecycle_event_for_non_ephemeral_ def handler(event): if ( event.type == "session.updated" - and event.sessionId == session.session_id + and event.session_id == session.session_id and not updated.done() ): updated.set_result(event) - unsubscribe = ctx.client.on(handler) + unsubscribe = ctx.client.on_lifecycle(handler) try: await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.PLAN)) event = await asyncio.wait_for(updated, timeout=15.0) assert event.type == "session.updated" - assert event.sessionId == session.session_id + assert event.session_id == session.session_id finally: unsubscribe() await session.disconnect() @@ -242,18 +234,18 @@ async def test_should_receive_session_deleted_lifecycle_event_when_deleted( def handler(event): if ( event.type == "session.deleted" - and event.sessionId == session_id + and event.session_id == session_id and not deleted.done() ): deleted.set_result(event) - unsubscribe = ctx.client.on(handler) + unsubscribe = ctx.client.on_lifecycle(handler) try: await session.disconnect() await ctx.client.delete_session(session_id) event = await asyncio.wait_for(deleted, timeout=15.0) assert event.type == "session.deleted" - assert event.sessionId == session_id + assert event.session_id == session_id finally: unsubscribe() diff --git a/python/e2e/test_client_options_e2e.py b/python/e2e/test_client_options_e2e.py index 80a3bf394..614aec5df 100644 --- a/python/e2e/test_client_options_e2e.py +++ b/python/e2e/test_client_options_e2e.py @@ -1,14 +1,15 @@ """ E2E coverage for ``CopilotClient`` configuration options exposed via -``SubprocessConfig`` and ``CopilotClient(..., auto_start=...)``. +``CopilotClientOptions`` and ``RuntimeConnection``. Mirrors ``dotnet/test/ClientOptionsTests.cs``. The two CliUrl-conflict tests (``Should_Throw_When_GitHubToken_Used_With_CliUrl`` and ``Should_Throw_When_UseLoggedInUser_Used_With_CliUrl``) have no Python -equivalent because Python's ``ExternalServerConfig`` does not accept -``github_token`` / ``use_logged_in_user`` fields at all (so the conflict cannot -be expressed in code), and the configurations are therefore intentionally -omitted. +equivalent because Python's ``RuntimeConnection.for_uri(...)`` does not accept +``github_token`` / ``use_logged_in_user`` fields at all (those live on +``CopilotClientOptions``, but a Uri-connected runtime ignores them), so the +conflict cannot be expressed in code and the configurations are therefore +intentionally omitted. """ from __future__ import annotations @@ -19,8 +20,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.rpc import PingRequest from copilot.session import PermissionHandler @@ -29,9 +29,31 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") -def _make_subprocess_config(ctx: E2ETestContext, **overrides) -> SubprocessConfig: - base = { - "cli_path": ctx.cli_path, +def _make_options( + ctx: E2ETestContext, + *, + use_tcp: bool = False, + port: int = 0, + connection_token: str | None = None, + cli_path: str | None = None, + cli_args: list[str] | None = None, + **overrides, +) -> dict[str, object]: + """Build CopilotClient kwargs pre-populated for the test harness.""" + if use_tcp: + connection: RuntimeConnection = RuntimeConnection.for_tcp( + port=port, + connection_token=connection_token, + path=cli_path if cli_path is not None else ctx.cli_path, + args=tuple(cli_args or []), + ) + else: + connection = RuntimeConnection.for_stdio( + path=cli_path if cli_path is not None else ctx.cli_path, + args=tuple(cli_args or []), + ) + base: dict[str, object] = { + "connection": connection, "working_directory": ctx.work_dir, "env": ctx.get_env(), "github_token": ( @@ -39,7 +61,7 @@ def _make_subprocess_config(ctx: E2ETestContext, **overrides) -> SubprocessConfi ), } base.update(overrides) - return SubprocessConfig(**base) + return base def _get_available_port() -> int: @@ -120,7 +142,7 @@ def _get_available_port() -> int: return; } if (message.method === "session.create") { - const sessionId = message.params?.sessionId ?? "fake-session"; + const sessionId = message.params?.session_id ?? "fake-session"; writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null }); return; } @@ -142,39 +164,12 @@ def _assert_arg_value(args: list[str], name: str, expected_value: str) -> None: class TestClientOptions: - async def test_autostart_false_requires_explicit_start(self, ctx: E2ETestContext): - client = CopilotClient(_make_subprocess_config(ctx), auto_start=False) - try: - assert client.get_state() == "disconnected" - - with pytest.raises(RuntimeError) as exc_info: - await client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - # Python raises "Client not connected" — equivalent intent to C#'s "StartAsync". - assert ( - "not connected" in str(exc_info.value).lower() - or "start" in str(exc_info.value).lower() - ) - - await client.start() - assert client.get_state() == "connected" - - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - assert session.session_id - await session.disconnect() - finally: - await client.stop() - async def test_should_listen_on_configured_tcp_port(self, ctx: E2ETestContext): port = _get_available_port() - client = CopilotClient(_make_subprocess_config(ctx, use_stdio=False, port=port)) + client = CopilotClient(**_make_options(ctx, use_tcp=True, port=port)) try: await client.start() - assert client.get_state() == "connected" - assert client.actual_port == port + assert client.runtime_port == port response = await client.rpc.ping(PingRequest(message="fixed-port")) assert "pong" in response.message @@ -187,7 +182,7 @@ async def test_should_use_client_cwd_for_default_workingdirectory(self, ctx: E2E with open(os.path.join(client_cwd, "marker.txt"), "w") as f: f.write("I am in the client cwd") - client = CopilotClient(_make_subprocess_config(ctx, working_directory=client_cwd)) + client = CopilotClient(**_make_options(ctx, working_directory=client_cwd)) try: session = await client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -212,10 +207,10 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes f.write(FAKE_STDIO_CLI_SCRIPT) client = CopilotClient( - _make_subprocess_config( + **_make_options( ctx, cli_path=cli_path, - copilot_home=copilot_home_from_option, + base_directory=copilot_home_from_option, cli_args=["--capture-file", capture_path], env={**ctx.get_env(), "COPILOT_HOME": copilot_home_from_env}, github_token="process-option-token", @@ -230,7 +225,6 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes }, use_logged_in_user=False, ), - auto_start=False, ) try: await client.start() @@ -278,51 +272,3 @@ async def test_should_propagate_process_options_to_spawned_cli(self, ctx: E2ETes await client.stop() except Exception: await client.force_stop() - - -# --------------------------------------------------------------------------- -# Unit-style tests mirroring the property-only tests in -# dotnet/test/ClientOptionsTests.cs. These exercise the SubprocessConfig -# dataclass shape only — no client / proxy required. -# --------------------------------------------------------------------------- - - -class TestSubprocessConfigOptions: - """Mirrors the unit-style ClientOptions tests in the C# baseline.""" - - async def test_should_accept_github_token_option(self): - # Mirrors: Should_Accept_GitHubToken_Option - config = SubprocessConfig(github_token="gho_test_token") - assert config.github_token == "gho_test_token" - - async def test_should_default_use_logged_in_user_to_none(self): - # Mirrors: Should_Default_UseLoggedInUser_To_Null - config = SubprocessConfig() - assert config.use_logged_in_user is None - - async def test_should_allow_explicit_use_logged_in_user_false(self): - # Mirrors: Should_Allow_Explicit_UseLoggedInUser_False - config = SubprocessConfig(use_logged_in_user=False) - assert config.use_logged_in_user is False - - async def test_should_allow_explicit_use_logged_in_user_true_with_github_token(self): - # Mirrors: Should_Allow_Explicit_UseLoggedInUser_True_With_GitHubToken - config = SubprocessConfig(github_token="gho_test_token", use_logged_in_user=True) - assert config.use_logged_in_user is True - assert config.github_token == "gho_test_token" - - # NOTE: Should_Throw_When_GitHubToken_Used_With_CliUrl and - # Should_Throw_When_UseLoggedInUser_Used_With_CliUrl from the C# baseline - # do not apply to Python: ExternalServerConfig has no github_token / - # use_logged_in_user fields at all (they live only on SubprocessConfig), - # so the conflicting configuration is impossible to express. - - async def test_should_default_session_idle_timeout_seconds_to_none(self): - # Mirrors: Should_Default_SessionIdleTimeoutSeconds_To_Null - config = SubprocessConfig() - assert config.session_idle_timeout_seconds is None - - async def test_should_accept_session_idle_timeout_seconds_option(self): - # Mirrors: Should_Accept_SessionIdleTimeoutSeconds_Option - config = SubprocessConfig(session_idle_timeout_seconds=600) - assert config.session_idle_timeout_seconds == 600 diff --git a/python/e2e/test_commands_e2e.py b/python/e2e/test_commands_e2e.py index 5bf1a274e..e0a0d63f1 100644 --- a/python/e2e/test_commands_e2e.py +++ b/python/e2e/test_commands_e2e.py @@ -15,8 +15,7 @@ import pytest import pytest_asyncio -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import CommandDefinition, PermissionHandler from .testharness.context import SNAPSHOTS_DIR, get_cli_path_for_tests @@ -26,7 +25,7 @@ # --------------------------------------------------------------------------- -# Multi-client context (TCP mode) — same pattern as test_multi_client.py +# Multi-client context (TCP mode) — same pattern as test_multi_client.py # --------------------------------------------------------------------------- @@ -56,14 +55,12 @@ async def setup(self): # Client 1 uses TCP mode so a second client can connect self._client1 = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - working_directory=self.work_dir, - env=self._get_env(), - use_stdio=False, - github_token=github_token, - tcp_connection_token="py-tcp-shared-test-token", - ) + connection=RuntimeConnection.for_tcp( + path=self.cli_path, connection_token="py-tcp-shared-test-token" + ), + working_directory=self.work_dir, + env=self._get_env(), + github_token=github_token, ) # Trigger connection to get the port @@ -72,12 +69,12 @@ async def setup(self): ) await init_session.disconnect() - actual_port = self._client1.actual_port + actual_port = self._client1.runtime_port assert actual_port is not None self._client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{actual_port}", tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + f"localhost:{actual_port}", connection_token="py-tcp-shared-test-token" ) ) diff --git a/python/e2e/test_connection_token.py b/python/e2e/test_connection_token.py index 195baaecc..1c7addbd9 100644 --- a/python/e2e/test_connection_token.py +++ b/python/e2e/test_connection_token.py @@ -11,8 +11,7 @@ import pytest import pytest_asyncio -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness.proxy import CapiProxy @@ -47,14 +46,10 @@ async def setup(self): ) self._client = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - working_directory=self.work_dir, - env=self.get_env(), - use_stdio=False, - tcp_connection_token=self.token, - github_token=github_token, - ) + connection=RuntimeConnection.for_tcp(path=self.cli_path, connection_token=self.token), + working_directory=self.work_dir, + env=self.get_env(), + github_token=github_token, ) # Trigger the spawn + connect handshake so the server is listening. @@ -133,11 +128,11 @@ async def test_auto_generated_token_round_trips(self, auto_token_ctx: Connection async def test_wrong_token_is_rejected(self, explicit_token_ctx: ConnectionTokenContext): """A sibling client connecting with the wrong token is rejected.""" - port = explicit_token_ctx.client.actual_port + port = explicit_token_ctx.client.runtime_port assert port is not None wrong = CopilotClient( - ExternalServerConfig(url=f"localhost:{port}", tcp_connection_token="wrong") + connection=RuntimeConnection.for_uri(f"localhost:{port}", connection_token="wrong") ) try: with pytest.raises(Exception, match="AUTHENTICATION_FAILED"): @@ -152,10 +147,10 @@ async def test_wrong_token_is_rejected(self, explicit_token_ctx: ConnectionToken async def test_missing_token_is_rejected(self, explicit_token_ctx: ConnectionTokenContext): """A sibling client with no token is rejected when the server requires one.""" - port = explicit_token_ctx.client.actual_port + port = explicit_token_ctx.client.runtime_port assert port is not None - no_token = CopilotClient(ExternalServerConfig(url=f"localhost:{port}")) + no_token = CopilotClient(connection=RuntimeConnection.for_uri(f"localhost:{port}")) try: with pytest.raises(Exception, match="AUTHENTICATION_FAILED"): await no_token.start() diff --git a/python/e2e/test_error_resilience_e2e.py b/python/e2e/test_error_resilience_e2e.py index 4afb78a6e..ab031842c 100644 --- a/python/e2e/test_error_resilience_e2e.py +++ b/python/e2e/test_error_resilience_e2e.py @@ -30,7 +30,7 @@ async def test_should_throw_when_getting_messages_from_disconnected_session( await session.disconnect() with pytest.raises(Exception): - await session.get_messages() + await session.get_events() async def test_should_handle_double_abort_without_error(self, ctx: E2ETestContext): session = await ctx.client.create_session( diff --git a/python/e2e/test_event_fidelity_e2e.py b/python/e2e/test_event_fidelity_e2e.py index 17193a308..b85609640 100644 --- a/python/e2e/test_event_fidelity_e2e.py +++ b/python/e2e/test_event_fidelity_e2e.py @@ -207,7 +207,7 @@ async def test_should_preserve_message_order_in_getmessages_after_tool_use( try: await session.send_and_wait("Read the file 'order.txt' and tell me what the number is.") - messages = await session.get_messages() + messages = await session.get_events() types = [m.type.value for m in messages] # Verify complete event ordering contract: diff --git a/python/e2e/test_mode_handlers_e2e.py b/python/e2e/test_mode_handlers_e2e.py index c0e19da13..d9917e9f3 100644 --- a/python/e2e/test_mode_handlers_e2e.py +++ b/python/e2e/test_mode_handlers_e2e.py @@ -35,7 +35,7 @@ async def mode_ctx(ctx: E2ETestContext): """Configure per-token user responses for mode-handler tests.""" proxy_url = ctx.proxy_url - ctx.client._config.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url + ctx.client._options.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url await ctx.set_copilot_user_by_token( MODE_HANDLER_TOKEN, @@ -75,7 +75,7 @@ async def test_should_invoke_exit_plan_mode_handler_when_model_uses_tool( ): exit_plan_mode_requests = [] - async def on_exit_plan_mode(request, invocation): + async def on_exit_plan_mode_request(request, invocation): exit_plan_mode_requests.append(request) assert invocation["session_id"] == session.session_id return { @@ -87,7 +87,7 @@ async def on_exit_plan_mode(request, invocation): session = await mode_ctx.client.create_session( github_token=MODE_HANDLER_TOKEN, on_permission_request=PermissionHandler.approve_all, - on_exit_plan_mode=on_exit_plan_mode, + on_exit_plan_mode_request=on_exit_plan_mode_request, ) try: @@ -139,7 +139,7 @@ async def test_should_invoke_auto_mode_switch_handler_when_rate_limited( ): auto_mode_switch_requests = [] - async def on_auto_mode_switch(request, invocation): + async def on_auto_mode_switch_request(request, invocation): auto_mode_switch_requests.append(request) assert invocation["session_id"] == session.session_id return "yes" @@ -147,7 +147,7 @@ async def on_auto_mode_switch(request, invocation): session = await mode_ctx.client.create_session( github_token=MODE_HANDLER_TOKEN, on_permission_request=PermissionHandler.approve_all, - on_auto_mode_switch=on_auto_mode_switch, + on_auto_mode_switch_request=on_auto_mode_switch_request, ) try: diff --git a/python/e2e/test_multi_client_e2e.py b/python/e2e/test_multi_client_e2e.py index 06f671e94..deadbfc86 100644 --- a/python/e2e/test_multi_client_e2e.py +++ b/python/e2e/test_multi_client_e2e.py @@ -14,9 +14,9 @@ import pytest_asyncio from pydantic import BaseModel, Field -from copilot import CopilotClient, define_tool -from copilot.client import ExternalServerConfig, SubprocessConfig -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot import CopilotClient, RuntimeConnection, define_tool +from copilot.generated.rpc import PermissionDecisionApproveOnce, PermissionDecisionReject +from copilot.session import PermissionHandler, PermissionNoResult from copilot.tools import ToolInvocation from .testharness import get_final_assistant_message @@ -53,14 +53,12 @@ async def setup(self): # Client 1 uses TCP mode so a second client can connect to the same server self._client1 = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - working_directory=self.work_dir, - env=self.get_env(), - use_stdio=False, - github_token=github_token, - tcp_connection_token="py-tcp-shared-test-token", - ) + connection=RuntimeConnection.for_tcp( + path=self.cli_path, connection_token="py-tcp-shared-test-token" + ), + working_directory=self.work_dir, + env=self.get_env(), + github_token=github_token, ) # Trigger connection by creating and disconnecting an init session @@ -70,12 +68,12 @@ async def setup(self): await init_session.disconnect() # Read the actual port from client 1 and create client 2 - actual_port = self._client1.actual_port + actual_port = self._client1.runtime_port assert actual_port is not None, "Client 1 should have an actual port after connecting" self._client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{actual_port}", tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + f"localhost:{actual_port}", connection_token="py-tcp-shared-test-token" ) ) @@ -204,7 +202,7 @@ def magic_number(params: SeedParams, invocation: ToolInvocation) -> str: on_permission_request=PermissionHandler.approve_all, tools=[magic_number] ) - # Client 2 resumes with NO tools — should not overwrite client 1's tools + # 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 ) @@ -242,16 +240,14 @@ 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( on_permission_request=lambda request, invocation: ( - permission_requests.append(request) or PermissionRequestResult(kind="approve-once") + permission_requests.append(request) or PermissionDecisionApproveOnce() ), ) # Client 2 observes the permission request but leaves the decision to client 1. session2 = await mctx.client2.resume_session( session1.session_id, - on_permission_request=lambda request, invocation: PermissionRequestResult( - kind="no-result" - ), + on_permission_request=lambda request, invocation: PermissionNoResult(), ) client1_events = [] @@ -279,7 +275,7 @@ async def test_one_client_approves_permission_and_both_see_the_result( assert len(c1_perm_completed) > 0 assert len(c2_perm_completed) > 0 for event in c1_perm_completed + c2_perm_completed: - assert event.data.result.kind.value == "approved" + assert event.data.result.kind == "approved" await session2.disconnect() @@ -289,17 +285,13 @@ 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( - on_permission_request=lambda request, invocation: PermissionRequestResult( - kind="reject" - ), + on_permission_request=lambda request, invocation: PermissionDecisionReject(), ) # Client 2 observes the permission request but leaves the decision to client 1. session2 = await mctx.client2.resume_session( session1.session_id, - on_permission_request=lambda request, invocation: PermissionRequestResult( - kind="no-result" - ), + on_permission_request=lambda request, invocation: PermissionNoResult(), ) client1_events = [] @@ -332,7 +324,7 @@ async def test_one_client_rejects_permission_and_both_see_the_result( assert len(c1_perm_completed) > 0 assert len(c2_perm_completed) > 0 for event in c1_perm_completed + c2_perm_completed: - assert event.data.result.kind.value == "denied-interactively-by-user" + assert event.data.result.kind == "denied-interactively-by-user" await session2.disconnect() @@ -431,10 +423,10 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: await asyncio.sleep(0.5) # Recreate client2 for future tests (but don't rejoin the session) - actual_port = mctx.client1.actual_port + actual_port = mctx.client1.runtime_port mctx._client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{actual_port}", tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + f"localhost:{actual_port}", connection_token="py-tcp-shared-test-token" ) ) diff --git a/python/e2e/test_pending_work_resume_e2e.py b/python/e2e/test_pending_work_resume_e2e.py index be0e4feec..4b1dfbff8 100644 --- a/python/e2e/test_pending_work_resume_e2e.py +++ b/python/e2e/test_pending_work_resume_e2e.py @@ -16,10 +16,13 @@ import pytest -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig -from copilot.generated.rpc import HandlePendingToolCallRequest, PermissionDecisionRequest -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot import CopilotClient, RuntimeConnection +from copilot.generated.rpc import ( + HandlePendingToolCallRequest, + PermissionDecisionRequest, + PermissionDecisionUserNotAvailable, +) +from copilot.session import PermissionHandler from copilot.tools import Tool, ToolInvocation, ToolResult from .testharness import E2ETestContext, get_final_assistant_message @@ -33,15 +36,17 @@ def _make_subprocess_client(ctx: E2ETestContext, *, use_stdio: bool = True) -> C github_token = ( "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) - return CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - use_stdio=use_stdio, - tcp_connection_token="py-tcp-shared-test-token", + if use_stdio: + connection = RuntimeConnection.for_stdio(path=ctx.cli_path) + else: + connection = RuntimeConnection.for_tcp( + path=ctx.cli_path, connection_token="py-tcp-shared-test-token" ) + return CopilotClient( + connection=connection, + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) @@ -134,7 +139,7 @@ async def test_should_continue_pending_permission_request_after_resume( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" release_original: asyncio.Future = asyncio.get_event_loop().create_future() captured_request: asyncio.Future = asyncio.get_event_loop().create_future() @@ -149,7 +154,9 @@ def original_tool_handler(args): return f"ORIGINAL_SHOULD_NOT_RUN_{args.get('value', '')}" suspended_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) session1 = await suspended_client.create_session( on_permission_request=hold_permission, @@ -175,16 +182,14 @@ def resumed_tool_handler(args): return f"PERMISSION_RESUMED_{args['value'].upper()}" resumed_client = CopilotClient( - ExternalServerConfig( - url=cli_url, tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" ) ) try: session2 = await resumed_client.resume_session( session_id, - on_permission_request=lambda req, inv: PermissionRequestResult( - kind="user-not-available" - ), + on_permission_request=lambda req, inv: PermissionDecisionUserNotAvailable(), continue_pending_work=True, tools=[_make_pending_tool("resume_permission_tool", resumed_tool_handler)], ) @@ -212,7 +217,7 @@ def resumed_tool_handler(args): await _safe_force_stop(resumed_client) finally: if not release_original.done(): - release_original.set_result(PermissionRequestResult(kind="user-not-available")) + release_original.set_result(PermissionDecisionUserNotAvailable()) finally: await _safe_force_stop(server) @@ -222,7 +227,7 @@ async def test_should_continue_pending_external_tool_request_after_resume( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" tool_started: asyncio.Future = asyncio.get_event_loop().create_future() release_original: asyncio.Future = asyncio.get_event_loop().create_future() @@ -234,7 +239,9 @@ async def blocking_external_tool(args): return await release_original suspended_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) session1 = await suspended_client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -255,8 +262,8 @@ async def blocking_external_tool(args): await suspended_client.force_stop() resumed_client = CopilotClient( - ExternalServerConfig( - url=cli_url, tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" ) ) try: @@ -294,7 +301,7 @@ async def test_should_continue_parallel_pending_external_tool_requests_after_res server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" tool_a_started: asyncio.Future = asyncio.get_event_loop().create_future() tool_b_started: asyncio.Future = asyncio.get_event_loop().create_future() @@ -312,7 +319,9 @@ async def tool_b(args): return await release_b suspended_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) session1 = await suspended_client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -343,8 +352,8 @@ async def tool_b(args): await suspended_client.force_stop() resumed_client = CopilotClient( - ExternalServerConfig( - url=cli_url, tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" ) ) try: @@ -386,10 +395,12 @@ async def test_should_resume_successfully_when_no_pending_work_exists( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" first_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: first_session = await first_client.create_session( @@ -405,7 +416,9 @@ async def test_should_resume_successfully_when_no_pending_work_exists( await _safe_force_stop(first_client) resumed_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: resumed_session = await resumed_client.resume_session( @@ -443,10 +456,12 @@ async def blocking_external_tool(args): server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" suspended_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) session1 = await suspended_client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -467,8 +482,8 @@ async def blocking_external_tool(args): await suspended_client.force_stop() resumed_client = CopilotClient( - ExternalServerConfig( - url=cli_url, tcp_connection_token="py-tcp-shared-test-token" + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" ) ) try: @@ -479,7 +494,7 @@ async def blocking_external_tool(args): ) # Verify resume event: continue_pending_work=False and session_was_active=True - messages = await session2.get_messages() + messages = await session2.get_events() resume_events = [m for m in messages if isinstance(m.data, SessionResumeData)] assert len(resume_events) == 1, "Expected exactly one session.resume event" resume_event = resume_events[0] @@ -518,10 +533,12 @@ async def test_should_report_continuependingwork_true_in_resume_event( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" first_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: first_session = await first_client.create_session( @@ -538,7 +555,9 @@ async def test_should_report_continuependingwork_true_in_resume_event( await _safe_force_stop(first_client) resumed_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: resumed_session = await resumed_client.resume_session( @@ -547,7 +566,7 @@ async def test_should_report_continuependingwork_true_in_resume_event( continue_pending_work=True, ) - messages = await resumed_session.get_messages() + messages = await resumed_session.get_events() resume_events = [m for m in messages if isinstance(m.data, SessionResumeData)] assert len(resume_events) == 1, "Expected exactly one session.resume event" resume_event = resume_events[0] diff --git a/python/e2e/test_per_session_auth_e2e.py b/python/e2e/test_per_session_auth_e2e.py index b03945deb..0aa42cdaa 100644 --- a/python/e2e/test_per_session_auth_e2e.py +++ b/python/e2e/test_per_session_auth_e2e.py @@ -2,7 +2,7 @@ import pytest -from copilot.client import CopilotClient, SubprocessConfig +from copilot.client import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness import E2ETestContext @@ -18,7 +18,7 @@ async def auth_ctx(ctx: E2ETestContext): # Redirect GitHub API calls to the proxy so per-session auth token # resolution (fetchCopilotUser) is intercepted. Must be set before the # CLI subprocess is spawned (i.e., before the first create_session call). - ctx.client._config.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url + ctx.client._options.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url await ctx.set_copilot_user_by_token( "token-alice", @@ -97,12 +97,10 @@ async def test_should_return_unauthenticated_when_no_token_provided( env = without_auth_env(auth_ctx.get_env()) env["COPILOT_DEBUG_GITHUB_API_URL"] = auth_ctx.proxy_url no_token_client = CopilotClient( - SubprocessConfig( - cli_path=auth_ctx.cli_path, - working_directory=auth_ctx.work_dir, - env=env, - use_logged_in_user=False, - ) + connection=RuntimeConnection.for_stdio(path=auth_ctx.cli_path), + working_directory=auth_ctx.work_dir, + env=env, + use_logged_in_user=False, ) try: diff --git a/python/e2e/test_permissions_e2e.py b/python/e2e/test_permissions_e2e.py index 46cf2f3d4..dbea6d384 100644 --- a/python/e2e/test_permissions_e2e.py +++ b/python/e2e/test_permissions_e2e.py @@ -6,12 +6,17 @@ import pytest +from copilot.generated.rpc import ( + PermissionDecisionApproveOnce, + PermissionDecisionReject, + PermissionDecisionUserNotAvailable, +) from copilot.generated.session_events import ( PermissionRequest, SessionIdleData, ToolExecutionCompleteData, ) -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session import PermissionHandler, PermissionNoResult, PermissionRequestResult from .testharness import E2ETestContext from .testharness.helper import read_file, write_file @@ -29,7 +34,7 @@ def on_permission_request( ) -> PermissionRequestResult: permission_requests.append(request) assert invocation["session_id"] == session.session_id - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=on_permission_request) @@ -41,7 +46,7 @@ def on_permission_request( assert len(permission_requests) > 0 # Should include write permission request - write_requests = [req for req in permission_requests if req.kind.value == "write"] + write_requests = [req for req in permission_requests if req.kind == "write"] assert len(write_requests) > 0 await session.disconnect() @@ -52,7 +57,7 @@ async def test_should_deny_permission_when_handler_returns_denied(self, ctx: E2E def on_permission_request( request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: - return PermissionRequestResult(kind="reject") + return PermissionDecisionReject() session = await ctx.client.create_session(on_permission_request=on_permission_request) @@ -97,7 +102,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies( """Test that tool operations are denied when handler explicitly denies""" def deny_all(request, invocation): - return PermissionRequestResult() + return PermissionDecisionUserNotAvailable() session = await ctx.client.create_session(on_permission_request=deny_all) @@ -138,7 +143,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ await session1.send_and_wait("What is 1+1?") def deny_all(request, invocation): - return PermissionRequestResult() + return PermissionDecisionUserNotAvailable() session2 = await ctx.client.resume_session(session_id, on_permission_request=deny_all) @@ -190,7 +195,7 @@ async def on_permission_request( ) -> PermissionRequestResult: permission_requests.append(request) await asyncio.sleep(0) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=on_permission_request) @@ -216,7 +221,7 @@ def on_permission_request( request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: permission_requests.append(request) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session2 = await ctx.client.resume_session( session_id, on_permission_request=on_permission_request @@ -260,7 +265,7 @@ def on_permission_request( received_tool_call_id = True assert isinstance(request.tool_call_id, str) assert len(request.tool_call_id) > 0 - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=on_permission_request) @@ -289,7 +294,7 @@ async def slow_permission(request: PermissionRequest, invocation: dict): handler_entered.set_result(True) await asyncio.wait_for(release_handler, timeout=30.0) add_event("permission-complete", tool_call_id) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=slow_permission) @@ -376,7 +381,7 @@ async def test_should_deny_permission_with_noresult_kind(self, ctx: E2ETestConte def deny_noresult(request: PermissionRequest, invocation: dict) -> PermissionRequestResult: if not permission_called.done(): permission_called.set_result(True) - return PermissionRequestResult(kind="no-result") + return PermissionNoResult() session = await ctx.client.create_session(on_permission_request=deny_noresult) try: @@ -399,7 +404,7 @@ def counting_handler( ) -> PermissionRequestResult: nonlocal handler_call_count handler_call_count += 1 - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session(on_permission_request=counting_handler) try: @@ -458,7 +463,7 @@ async def concurrent_permission(request: PermissionRequest, invocation: dict): if permission_request_count >= 2 and not both_started.done(): both_started.set_result(True) await asyncio.wait_for(both_started, timeout=30.0) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() def first_tool_handler(invocation: ToolInvocation) -> ToolResult: nonlocal first_tool_called diff --git a/python/e2e/test_pre_mcp_tool_call_hook_e2e.py b/python/e2e/test_pre_mcp_tool_call_hook_e2e.py index 9a140dd38..c59994437 100644 --- a/python/e2e/test_pre_mcp_tool_call_hook_e2e.py +++ b/python/e2e/test_pre_mcp_tool_call_hook_e2e.py @@ -5,6 +5,7 @@ from __future__ import annotations +from datetime import datetime from pathlib import Path import pytest @@ -58,7 +59,7 @@ async def on_pre_mcp_tool_call(input_data, invocation): assert inputs[0].get("serverName") == "meta-echo" assert inputs[0].get("toolName") == "echo_meta" assert inputs[0].get("workingDirectory") - assert inputs[0].get("timestamp", 0) > 0 + assert isinstance(inputs[0].get("timestamp"), datetime) finally: await session.disconnect() diff --git a/python/e2e/test_rpc_e2e.py b/python/e2e/test_rpc_e2e.py index 511b9d1d1..b825db060 100644 --- a/python/e2e/test_rpc_e2e.py +++ b/python/e2e/test_rpc_e2e.py @@ -2,8 +2,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.rpc import ModelsListRequest, PingRequest from copilot.session import PermissionHandler @@ -16,7 +15,7 @@ class TestRpc: @pytest.mark.asyncio async def test_should_call_rpc_ping_with_typed_params(self): """Test calling rpc.ping with typed params and result""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -32,7 +31,7 @@ async def test_should_call_rpc_ping_with_typed_params(self): @pytest.mark.asyncio async def test_should_call_rpc_models_list(self): """Test calling rpc.models.list with typed result""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -55,7 +54,7 @@ async def test_should_call_rpc_models_list(self): @pytest.mark.asyncio async def test_should_call_rpc_account_get_quota(self): """Test calling rpc.account.getQuota when authenticated""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -116,7 +115,7 @@ async def test_get_and_set_session_mode(self): """Test getting and setting session mode""" from copilot.generated.rpc import ModeSetRequest, SessionMode - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -148,7 +147,7 @@ async def test_read_update_and_delete_plan(self): """Test reading, updating, and deleting plan""" from copilot.generated.rpc import PlanUpdateRequest - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() @@ -191,7 +190,7 @@ async def test_create_list_and_read_workspace_files(self): WorkspacesReadFileRequest, ) - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) try: await client.start() diff --git a/python/e2e/test_rpc_event_side_effects_e2e.py b/python/e2e/test_rpc_event_side_effects_e2e.py index b4a5b2790..9725e211a 100644 --- a/python/e2e/test_rpc_event_side_effects_e2e.py +++ b/python/e2e/test_rpc_event_side_effects_e2e.py @@ -215,7 +215,7 @@ async def test_should_emit_snapshot_rewind_event_and_remove_events_on_truncate( try: await session.send_and_wait("Say SNAPSHOT_REWIND_TARGET exactly.", timeout=60.0) - events = await session.get_messages() + events = await session.get_events() user_msgs = [e for e in events if isinstance(e.data, UserMessageData)] assert len(user_msgs) >= 1 first_user_event_id = str(user_msgs[0].id) @@ -236,7 +236,7 @@ def on_event(event): assert evt.data.events_removed >= 1 assert evt.data.up_to_event_id.lower() == first_user_event_id.lower() - messages_after = await session.get_messages() + messages_after = await session.get_events() assert not any(e.id == user_msgs[0].id for e in messages_after) except Exception as exc: if "unhandled method" in str(exc).lower(): @@ -257,7 +257,7 @@ async def test_should_allow_session_use_after_truncate(self, ctx: E2ETestContext try: await session.send_and_wait("Say SNAPSHOT_REWIND_TARGET exactly.", timeout=60.0) - events = await session.get_messages() + events = await session.get_events() user_msgs = [e for e in events if isinstance(e.data, UserMessageData)] assert len(user_msgs) >= 1 first_user_event_id = str(user_msgs[0].id) diff --git a/python/e2e/test_rpc_server_e2e.py b/python/e2e/test_rpc_server_e2e.py index f5dc9920d..481e50d7b 100644 --- a/python/e2e/test_rpc_server_e2e.py +++ b/python/e2e/test_rpc_server_e2e.py @@ -12,8 +12,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.rpc import ( AccountGetQuotaRequest, MCPDiscoverRequest, @@ -48,7 +47,7 @@ def _create_skill_directory(work_dir: str, skill_name: str, description: str) -> @pytest.fixture(scope="module") async def authed_ctx(ctx: E2ETestContext): """Configure proxy to redirect GitHub user lookups so per-token auth works.""" - ctx.client._config.env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url + ctx.client._options.env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url return ctx @@ -56,12 +55,10 @@ def _make_authed_client(ctx: E2ETestContext, token: str) -> CopilotClient: env = ctx.get_env() env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url return CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=env, - github_token=token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=env, + github_token=token, ) diff --git a/python/e2e/test_rpc_session_state_e2e.py b/python/e2e/test_rpc_session_state_e2e.py index b7329158c..f5b11f6fa 100644 --- a/python/e2e/test_rpc_session_state_e2e.py +++ b/python/e2e/test_rpc_session_state_e2e.py @@ -171,7 +171,7 @@ async def test_should_fork_session_with_persisted_messages(self, ctx: E2ETestCon assert initial_answer is not None assert "FORK_SOURCE_ALPHA" in (initial_answer.data.content or "") - source_messages = await session.get_messages() + source_messages = await session.get_events() source_conversation = _conversation_messages(source_messages) assert any( role == "user" and content == source_prompt for role, content in source_conversation @@ -192,7 +192,7 @@ async def test_should_fork_session_with_persisted_messages(self, ctx: E2ETestCon on_permission_request=PermissionHandler.approve_all, ) try: - forked_messages = await forked_session.get_messages() + forked_messages = await forked_session.get_events() forked_conversation = _conversation_messages(forked_messages) assert forked_conversation[: len(source_conversation)] == source_conversation @@ -200,10 +200,10 @@ async def test_should_fork_session_with_persisted_messages(self, ctx: E2ETestCon assert fork_answer is not None assert "FORK_CHILD_BETA" in (fork_answer.data.content or "") - source_after_fork = _conversation_messages(await session.get_messages()) + source_after_fork = _conversation_messages(await session.get_events()) assert all(content != fork_prompt for _, content in source_after_fork) - fork_after_prompt = _conversation_messages(await forked_session.get_messages()) + fork_after_prompt = _conversation_messages(await forked_session.get_events()) assert any( role == "user" and content == fork_prompt for role, content in fork_after_prompt ) @@ -241,7 +241,7 @@ async def test_should_handle_forking_session_without_persisted_events( on_permission_request=PermissionHandler.approve_all, ) try: - assert _conversation_messages(await forked_session.get_messages()) == [] + assert _conversation_messages(await forked_session.get_events()) == [] finally: await forked_session.disconnect() finally: @@ -506,7 +506,7 @@ async def test_should_fork_session_to_event_id_excluding_boundary_event( await session.send_and_wait(first_prompt, timeout=60.0) await session.send_and_wait(second_prompt, timeout=60.0) - source_events = await session.get_messages() + source_events = await session.get_events() second_user_event = next( ( e @@ -531,7 +531,7 @@ async def test_should_fork_session_to_event_id_excluding_boundary_event( on_permission_request=PermissionHandler.approve_all, ) try: - forked_events = await forked_session.get_messages() + forked_events = await forked_session.get_events() forked_ids = {str(e.id) for e in forked_events} assert boundary_event_id not in forked_ids, ( "toEventId is exclusive — boundary event must not be in forked session" diff --git a/python/e2e/test_rpc_shell_and_fleet_e2e.py b/python/e2e/test_rpc_shell_and_fleet_e2e.py index c5384825b..32177cbbd 100644 --- a/python/e2e/test_rpc_shell_and_fleet_e2e.py +++ b/python/e2e/test_rpc_shell_and_fleet_e2e.py @@ -128,7 +128,7 @@ def record_fleet_completion(invocation: ToolInvocation) -> ToolResult: async def _wait_for_messages(timeout: float = 120.0): deadline = asyncio.get_event_loop().time() + timeout while asyncio.get_event_loop().time() < deadline: - messages = await session.get_messages() + messages = await session.get_events() if any( isinstance(m.data, AssistantMessageData) and "fleet task" in (m.data.content or "").lower() diff --git a/python/e2e/test_rpc_tasks_and_handlers_e2e.py b/python/e2e/test_rpc_tasks_and_handlers_e2e.py index 707c8b781..23b9f9896 100644 --- a/python/e2e/test_rpc_tasks_and_handlers_e2e.py +++ b/python/e2e/test_rpc_tasks_and_handlers_e2e.py @@ -12,14 +12,15 @@ import pytest from copilot.generated.rpc import ( - ApprovalKind, CommandsHandlePendingCommandRequest, HandlePendingToolCallRequest, - PermissionDecision, - PermissionDecisionApproveForIonApproval, - PermissionDecisionKind, + PermissionDecisionApproveForLocation, + PermissionDecisionApproveForLocationApprovalCustomTool, + PermissionDecisionApproveForSession, + PermissionDecisionApproveForSessionApprovalCustomTool, + PermissionDecisionApprovePermanently, + PermissionDecisionReject, PermissionDecisionRequest, - TaskInfoType, TasksCancelRequest, TasksPromoteToBackgroundRequest, TasksRemoveRequest, @@ -137,10 +138,7 @@ async def test_should_return_expected_results_for_missing_pending_handler_reques permission = await session.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id="missing-permission-request", - result=PermissionDecision( - kind=PermissionDecisionKind.REJECT, - feedback="not approved", - ), + result=PermissionDecisionReject(feedback="not approved"), ) ) assert permission.success is False @@ -148,10 +146,7 @@ async def test_should_return_expected_results_for_missing_pending_handler_reques permanent = await session.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id="missing-permanent-permission-request", - result=PermissionDecision( - kind=PermissionDecisionKind.APPROVE_PERMANENTLY, - domain="example.com", - ), + result=PermissionDecisionApprovePermanently(domain="example.com"), ) ) assert permanent.success is False @@ -159,10 +154,8 @@ async def test_should_return_expected_results_for_missing_pending_handler_reques session_approval = await session.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id="missing-session-approval-request", - result=PermissionDecision( - kind=PermissionDecisionKind.APPROVE_FOR_SESSION, - approval=PermissionDecisionApproveForIonApproval( - kind=ApprovalKind.CUSTOM_TOOL, + result=PermissionDecisionApproveForSession( + approval=PermissionDecisionApproveForSessionApprovalCustomTool( tool_name="missing-tool", ), ), @@ -173,11 +166,9 @@ async def test_should_return_expected_results_for_missing_pending_handler_reques location_approval = await session.rpc.permissions.handle_pending_permission_request( PermissionDecisionRequest( request_id="missing-location-approval-request", - result=PermissionDecision( - kind=PermissionDecisionKind.APPROVE_FOR_LOCATION, + result=PermissionDecisionApproveForLocation( location_key="missing-location", - approval=PermissionDecisionApproveForIonApproval( - kind=ApprovalKind.CUSTOM_TOOL, + approval=PermissionDecisionApproveForLocationApprovalCustomTool( tool_name="missing-tool", ), ), @@ -215,7 +206,7 @@ async def test_should_report_implemented_error_for_invalid_task_agent_model( async def test_should_start_background_agent_and_report_task_details(self, ctx: E2ETestContext): """Start a background agent task and verify task details then remove it.""" - from copilot.generated.rpc import TaskInfoExecutionMode, TaskInfoStatus + from copilot.generated.rpc import TaskAgentInfo, TaskInfoExecutionMode, TaskInfoStatus session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -248,7 +239,7 @@ async def test_should_start_background_agent_and_report_task_details(self, ctx: ) assert found_task.id == task_id assert found_task.description == "SDK background agent coverage" - assert found_task.type == TaskInfoType.AGENT + assert isinstance(found_task, TaskAgentInfo) assert found_task.agent_type == "general-purpose" assert found_task.execution_mode == TaskInfoExecutionMode.BACKGROUND assert found_task.prompt == "Reply with TASK_AGENT_DONE exactly." diff --git a/python/e2e/test_session_config_e2e.py b/python/e2e/test_session_config_e2e.py index 1fd2cd0a2..b018ba6f8 100644 --- a/python/e2e/test_session_config_e2e.py +++ b/python/e2e/test_session_config_e2e.py @@ -171,7 +171,7 @@ async def test_should_use_custom_sessionid(self, ctx: E2ETestContext): ) assert session.session_id == requested_session_id - messages = await session.get_messages() + messages = await session.get_events() assert messages start_event = messages[0] assert isinstance(start_event.data, SessionStartData) diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py index d5a0c970e..db9b61e24 100644 --- a/python/e2e/test_session_e2e.py +++ b/python/e2e/test_session_e2e.py @@ -2,11 +2,11 @@ import base64 import os +from datetime import datetime import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.session_events import SessionModelChangeData from copilot.session import PermissionHandler from copilot.tools import Tool, ToolResult @@ -23,7 +23,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): ) assert session.session_id - messages = await session.get_messages() + messages = await session.get_events() assert len(messages) > 0 assert messages[0].type.value == "session.start" assert messages[0].data.session_id == session.session_id @@ -32,7 +32,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): await session.disconnect() with pytest.raises(Exception, match="Session not found"): - await session.get_messages() + await session.get_events() async def test_should_have_stateful_conversation(self, ctx: E2ETestContext): session = await ctx.client.create_session( @@ -194,7 +194,7 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont # All are connected for s in [s1, s2, s3]: - messages = await s.get_messages() + messages = await s.get_events() assert len(messages) > 0 assert messages[0].type.value == "session.start" assert messages[0].data.session_id == s.session_id @@ -203,7 +203,7 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont await asyncio.gather(s1.disconnect(), s2.disconnect(), s3.disconnect()) for s in [s1, s2, s3]: with pytest.raises(Exception, match="Session not found"): - await s.get_messages() + await s.get_events() async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestContext): # Create initial session @@ -243,12 +243,10 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) new_client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) try: @@ -257,7 +255,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont ) assert session2.session_id == session_id - messages = await session2.get_messages() + messages = await session2.get_events() message_types = [m.type.value for m in messages] assert "user.message" in message_types assert "session.resume" in message_types @@ -295,21 +293,21 @@ async def test_should_list_sessions(self, ctx: E2ETestContext): sessions = await ctx.client.list_sessions() assert isinstance(sessions, list) - session_ids = [s.sessionId for s in sessions] + session_ids = [s.session_id for s in sessions] assert session1.session_id in session_ids assert session2.session_id in session_ids # Verify session metadata structure for session_data in sessions: - assert hasattr(session_data, "sessionId") - assert hasattr(session_data, "startTime") - assert hasattr(session_data, "modifiedTime") - assert hasattr(session_data, "isRemote") + assert hasattr(session_data, "session_id") + assert hasattr(session_data, "start_time") + assert hasattr(session_data, "modified_time") + assert hasattr(session_data, "is_remote") # summary is optional - assert isinstance(session_data.sessionId, str) - assert isinstance(session_data.startTime, str) - assert isinstance(session_data.modifiedTime, str) - assert isinstance(session_data.isRemote, bool) + assert isinstance(session_data.session_id, str) + assert isinstance(session_data.start_time, datetime) + assert isinstance(session_data.modified_time, datetime) + assert isinstance(session_data.is_remote, bool) # Verify context field is present for session_data in sessions: @@ -333,7 +331,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Verify session exists in the list sessions = await ctx.client.list_sessions() - session_ids = [s.sessionId for s in sessions] + session_ids = [s.session_id for s in sessions] assert session_id in session_ids # Delete the session @@ -341,7 +339,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Verify session no longer exists in the list sessions_after = await ctx.client.list_sessions() - session_ids_after = [s.sessionId for s in sessions_after] + session_ids_after = [s.session_id for s in sessions_after] assert session_id not in session_ids_after # Verify we cannot resume the deleted session @@ -365,10 +363,10 @@ async def test_should_get_session_metadata(self, ctx: E2ETestContext): # Get metadata for the session we just created metadata = await ctx.client.get_session_metadata(session.session_id) assert metadata is not None - assert metadata.sessionId == session.session_id - assert isinstance(metadata.startTime, str) - assert isinstance(metadata.modifiedTime, str) - assert isinstance(metadata.isRemote, bool) + assert metadata.session_id == session.session_id + assert isinstance(metadata.start_time, datetime) + assert isinstance(metadata.modified_time, datetime) + assert isinstance(metadata.is_remote, bool) # Verify context field is present if metadata.context is not None: @@ -499,7 +497,7 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext): _ = await wait_for_session_idle # The session should still be alive and usable after abort - messages = await session.get_messages() + messages = await session.get_events() assert len(messages) > 0 # Verify an abort event exists in messages @@ -555,7 +553,7 @@ def on_event(event): assert "session.idle" in event_types # Verify the assistant response contains the expected answer. - # session.idle is ephemeral and not in get_messages(), but we already + # session.idle is ephemeral and not in get_events(), but we already # confirmed idle via the live event handler above. assistant_message = await get_final_assistant_message(session, already_idle=True) assert "300" in assistant_message.data.content @@ -696,13 +694,13 @@ async def test_should_send_with_file_attachment(self, ctx: E2ETestContext): ], ) - messages = await session.get_messages() + messages = await session.get_events() user_messages = [m for m in messages if isinstance(m.data, UserMessageData)] assert user_messages attachments = user_messages[-1].data.attachments assert attachments is not None and len(attachments) == 1 attachment = attachments[0] - assert attachment.type.value == "file" + assert attachment.type == "file" assert attachment.display_name == "attached-file.txt" assert attachment.path == file_path assert attachment.line_range is not None @@ -734,13 +732,13 @@ async def test_should_send_with_directory_attachment(self, ctx: E2ETestContext): ], ) - messages = await session.get_messages() + messages = await session.get_events() user_messages = [m for m in messages if isinstance(m.data, UserMessageData)] assert user_messages attachments = user_messages[-1].data.attachments assert attachments is not None and len(attachments) == 1 attachment = attachments[0] - assert attachment.type.value == "directory" + assert attachment.type == "directory" assert attachment.display_name == "attached-directory" assert attachment.path == directory_path @@ -773,13 +771,13 @@ async def test_should_send_with_selection_attachment(self, ctx: E2ETestContext): ], ) - messages = await session.get_messages() + messages = await session.get_events() user_messages = [m for m in messages if isinstance(m.data, UserMessageData)] assert user_messages attachments = user_messages[-1].data.attachments assert attachments is not None and len(attachments) == 1 attachment = attachments[0] - assert attachment.type.value == "selection" + assert attachment.type == "selection" assert attachment.display_name == "selected-file.cs" assert attachment.file_path == file_path assert attachment.text == 'string Value = "SELECTION_SENTINEL";' @@ -822,7 +820,7 @@ async def test_should_list_sessions_with_context(self, ctx: E2ETestContext): our_session = None for _ in range(50): sessions = await ctx.client.list_sessions() - our_session = next((s for s in sessions if s.sessionId == session.session_id), None) + our_session = next((s for s in sessions if s.session_id == session.session_id), None) if our_session is not None: break await asyncio.sleep(0.1) @@ -854,9 +852,9 @@ async def test_should_get_session_metadata_by_id(self, ctx: E2ETestContext): break await asyncio.sleep(0.1) assert metadata is not None - assert metadata.sessionId == session.session_id - assert isinstance(metadata.startTime, str) and metadata.startTime - assert isinstance(metadata.modifiedTime, str) and metadata.modifiedTime + assert metadata.session_id == session.session_id + assert isinstance(metadata.start_time, datetime) + assert isinstance(metadata.modified_time, datetime) not_found = await ctx.client.get_session_metadata("non-existent-session-id") assert not_found is None @@ -1085,7 +1083,7 @@ async def test_should_send_with_mode_property(self, ctx: E2ETestContext): mode="plan", # type: ignore[arg-type] ) - messages = await session.get_messages() + messages = await session.get_events() user_messages = [m for m in messages if isinstance(m.data, UserMessageData)] assert user_messages last = user_messages[-1].data diff --git a/python/e2e/test_session_fs_e2e.py b/python/e2e/test_session_fs_e2e.py index 0afb565ef..9d00057ec 100644 --- a/python/e2e/test_session_fs_e2e.py +++ b/python/e2e/test_session_fs_e2e.py @@ -12,8 +12,12 @@ import pytest import pytest_asyncio -from copilot import CopilotClient, SessionFsConfig, define_tool -from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot import ( + CopilotClient, + RuntimeConnection, + SessionFsConfig, + define_tool, +) from copilot.generated.rpc import ( SessionFSReaddirWithTypesEntry, SessionFSReaddirWithTypesEntryType, @@ -45,13 +49,11 @@ @pytest_asyncio.fixture(scope="module", loop_scope="module") async def session_fs_client(ctx: E2ETestContext): client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=DEFAULT_GITHUB_TOKEN, - session_fs=SESSION_FS_CONFIG, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, + session_fs=SESSION_FS_CONFIG, ) yield client try: @@ -117,13 +119,10 @@ async def test_should_load_session_data_from_fs_provider_on_resume( async def test_should_reject_setprovider_when_sessions_already_exist(self, ctx: E2ETestContext): client1 = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - use_stdio=False, - github_token=DEFAULT_GITHUB_TOKEN, - ) + connection=RuntimeConnection.for_tcp(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, ) session = None client2 = None @@ -132,14 +131,12 @@ async def test_should_reject_setprovider_when_sessions_already_exist(self, ctx: session = await client1.create_session( on_permission_request=PermissionHandler.approve_all, ) - actual_port = client1.actual_port + actual_port = client1.runtime_port assert actual_port is not None client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{actual_port}", - session_fs=SESSION_FS_CONFIG, - ) + connection=RuntimeConnection.for_uri(f"localhost:{actual_port}"), + session_fs=SESSION_FS_CONFIG, ) with pytest.raises(Exception): @@ -171,7 +168,7 @@ def get_big_string() -> str: "Call the get_big_string tool and reply with the word DONE only." ) - messages = await session.get_messages() + messages = await session.get_events() tool_result = find_tool_call_result(messages, "get_big_string") assert tool_result is not None assert f"{SESSION_STATE_PATH}/temp/" in tool_result diff --git a/python/e2e/test_session_fs_sqlite_e2e.py b/python/e2e/test_session_fs_sqlite_e2e.py index 38c15ae08..565c55336 100644 --- a/python/e2e/test_session_fs_sqlite_e2e.py +++ b/python/e2e/test_session_fs_sqlite_e2e.py @@ -12,8 +12,7 @@ import pytest import pytest_asyncio -from copilot import CopilotClient, SessionFsConfig -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection, SessionFsConfig from copilot.generated.rpc import ( SessionFSReaddirWithTypesEntry, SessionFSReaddirWithTypesEntryType, @@ -200,13 +199,11 @@ def factory(session): @pytest_asyncio.fixture(scope="module", loop_scope="module") async def sqlite_client(ctx: E2ETestContext): client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=DEFAULT_GITHUB_TOKEN, - session_fs=SESSION_FS_CONFIG, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, + session_fs=SESSION_FS_CONFIG, ) yield client try: diff --git a/python/e2e/test_streaming_fidelity_e2e.py b/python/e2e/test_streaming_fidelity_e2e.py index e47fb9911..79b34fc91 100644 --- a/python/e2e/test_streaming_fidelity_e2e.py +++ b/python/e2e/test_streaming_fidelity_e2e.py @@ -4,8 +4,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness import E2ETestContext @@ -79,12 +78,10 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) new_client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) try: @@ -131,12 +128,10 @@ async def test_should_not_produce_deltas_after_session_resume_with_streaming_dis # Resume with streaming disabled new_client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) try: session2 = await new_client.resume_session( @@ -184,8 +179,8 @@ async def test_should_emit_streaming_deltas_with_reasoning_effort_configured( assistant_events = [e for e in events if e.type.value == "assistant.message"] assert len(assistant_events) >= 1, "Expected final assistant.message" - # Check session.start event (from get_messages) has reasoning_effort - all_msgs = await session.get_messages() + # Check session.start event (from get_events) has reasoning_effort + all_msgs = await session.get_events() start_event = next((e for e in all_msgs if isinstance(e.data, SessionStartData)), None) assert start_event is not None, "Expected session.start event" assert start_event.data.reasoning_effort == "high" diff --git a/python/e2e/test_subagent_hooks_e2e.py b/python/e2e/test_subagent_hooks_e2e.py index e5262a23c..1ca2a54c1 100644 --- a/python/e2e/test_subagent_hooks_e2e.py +++ b/python/e2e/test_subagent_hooks_e2e.py @@ -7,7 +7,7 @@ import pytest -from copilot.client import CopilotClient, SubprocessConfig +from copilot.client import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler from .testharness import E2ETestContext @@ -50,12 +50,10 @@ async def on_post_tool_use(input_data, invocation): "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=env, - github_token=github_token, - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=env, + github_token=github_token, ) session = await client.create_session( @@ -85,7 +83,7 @@ async def on_post_tool_use(input_data, invocation): assert len(view_pre) > 0, "preToolUse should fire for the sub-agent's 'view' tool call" assert len(view_post) > 0, "postToolUse should fire for the sub-agent's 'view' tool call" - # input.sessionId distinguishes parent from sub-agent + # input.session_id distinguishes parent from sub-agent assert view_pre[0]["sessionId"] != task_pre[0]["sessionId"], ( "Sub-agent tool hooks should have a different sessionId than parent tool hooks" ) diff --git a/python/e2e/test_suspend_e2e.py b/python/e2e/test_suspend_e2e.py index ec34bfc37..b0f74140c 100644 --- a/python/e2e/test_suspend_e2e.py +++ b/python/e2e/test_suspend_e2e.py @@ -14,9 +14,9 @@ import pytest -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot import CopilotClient, RuntimeConnection +from copilot.generated.rpc import PermissionDecisionUserNotAvailable +from copilot.session import PermissionHandler from copilot.tools import Tool, ToolInvocation, ToolResult from .testharness import E2ETestContext @@ -30,15 +30,17 @@ def _make_subprocess_client(ctx: E2ETestContext, *, use_stdio: bool = True) -> C github_token = ( "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) - return CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - use_stdio=use_stdio, - tcp_connection_token="py-tcp-shared-test-token", + if use_stdio: + connection = RuntimeConnection.for_stdio(path=ctx.cli_path) + else: + connection = RuntimeConnection.for_tcp( + path=ctx.cli_path, connection_token="py-tcp-shared-test-token" ) + return CopilotClient( + connection=connection, + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, ) @@ -99,11 +101,13 @@ async def test_should_allow_resume_and_continue_conversation_after_suspend( server = _make_subprocess_client(ctx, use_stdio=False) await server.start() try: - cli_url = f"localhost:{server.actual_port}" + cli_url = f"localhost:{server.runtime_port}" session_id: str first_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: session1 = await first_client.create_session( @@ -120,7 +124,9 @@ async def test_should_allow_resume_and_continue_conversation_after_suspend( await _safe_force_stop(first_client) resumed_client = CopilotClient( - ExternalServerConfig(url=cli_url, tcp_connection_token="py-tcp-shared-test-token") + connection=RuntimeConnection.for_uri( + cli_url, connection_token="py-tcp-shared-test-token" + ) ) try: session2 = await resumed_client.resume_session( @@ -172,9 +178,7 @@ def tool_handler(args): assert not tool_invoked finally: if not release_permission_handler.done(): - release_permission_handler.set_result( - PermissionRequestResult(kind="user-not-available") - ) + release_permission_handler.set_result(PermissionDecisionUserNotAvailable()) await _safe_disconnect(session) async def test_should_reject_pending_external_tool_when_suspending(self, ctx: E2ETestContext): diff --git a/python/e2e/test_telemetry_e2e.py b/python/e2e/test_telemetry_e2e.py index 6b1f7766c..f18a9fb88 100644 --- a/python/e2e/test_telemetry_e2e.py +++ b/python/e2e/test_telemetry_e2e.py @@ -22,9 +22,8 @@ import pytest -from copilot import CopilotClient +from copilot import CopilotClient, RuntimeConnection, TelemetryConfig from copilot._telemetry import get_trace_context, trace_context -from copilot.client import SubprocessConfig, TelemetryConfig from copilot.session import PermissionHandler from copilot.tools import Tool, ToolInvocation, ToolResult @@ -82,18 +81,16 @@ def echo(invocation: ToolInvocation) -> ToolResult: "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) client = CopilotClient( - SubprocessConfig( - cli_path=ctx.cli_path, - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=github_token, - telemetry=TelemetryConfig( - file_path=str(telemetry_path), - exporter_type="file", - source_name=source_name, - capture_content=True, - ), - ) + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + telemetry=TelemetryConfig( + file_path=str(telemetry_path), + exporter_type="file", + source_name=source_name, + capture_content=True, + ), ) try: @@ -209,18 +206,6 @@ async def test_can_set_all_properties(self): assert cfg["capture_content"] is True -class TestSubprocessConfigTelemetry: - """Mirrors CopilotClientOptions_Telemetry_DefaultsToNull.""" - - async def test_telemetry_defaults_to_none(self): - config = SubprocessConfig() - assert config.telemetry is None - - # NOTE: CopilotClientOptions_Clone_CopiesTelemetry from the C# baseline has - # no Python equivalent: SubprocessConfig is a plain dataclass with no - # Clone() method, so there is nothing meaningful to test. - - class TestTelemetryHelpers: """Mirrors TelemetryHelpers_Restores_W3C_Trace_Context.""" diff --git a/python/e2e/test_tools_e2e.py b/python/e2e/test_tools_e2e.py index 4800d97c4..2f121b46d 100644 --- a/python/e2e/test_tools_e2e.py +++ b/python/e2e/test_tools_e2e.py @@ -6,7 +6,8 @@ from pydantic import BaseModel, Field from copilot import define_tool -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionApproveOnce, PermissionDecisionReject +from copilot.session import PermissionHandler, PermissionNoResult from copilot.tools import Tool, ToolInvocation, ToolResult from .testharness import E2ETestContext, get_final_assistant_message @@ -148,7 +149,7 @@ def safe_lookup(params: LookupParams, invocation: ToolInvocation) -> str: def tracking_handler(request, invocation): nonlocal did_run_permission_request did_run_permission_request = True - return PermissionRequestResult(kind="no-result") + return PermissionNoResult() session = await ctx.client.create_session( on_permission_request=tracking_handler, tools=[safe_lookup] @@ -191,7 +192,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: def on_permission_request(request, invocation): permission_requests.append(request) - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() session = await ctx.client.create_session( on_permission_request=on_permission_request, tools=[encrypt_string] @@ -202,7 +203,7 @@ def on_permission_request(request, invocation): assert "HELLO" in assistant_message.data.content # Should have received a custom-tool permission request - custom_tool_requests = [r for r in permission_requests if r.kind.value == "custom-tool"] + custom_tool_requests = [r for r in permission_requests if r.kind == "custom-tool"] assert len(custom_tool_requests) > 0 assert custom_tool_requests[0].tool_name == "encrypt_string" @@ -219,7 +220,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: return params.input.upper() def on_permission_request(request, invocation): - return PermissionRequestResult(kind="reject") + return PermissionDecisionReject() session = await ctx.client.create_session( on_permission_request=on_permission_request, tools=[encrypt_string] diff --git a/python/e2e/test_ui_elicitation_multi_client_e2e.py b/python/e2e/test_ui_elicitation_multi_client_e2e.py index 97f989ac4..398b83ee8 100644 --- a/python/e2e/test_ui_elicitation_multi_client_e2e.py +++ b/python/e2e/test_ui_elicitation_multi_client_e2e.py @@ -1,6 +1,6 @@ """E2E UI Elicitation Tests (multi-client) -Mirrors nodejs/test/e2e/ui_elicitation.test.ts — multi-client scenarios. +Mirrors nodejs/test/e2e/ui_elicitation.test.ts — multi-client scenarios. Tests: - capabilities.changed fires when second client joins with elicitation handler @@ -16,8 +16,7 @@ import pytest import pytest_asyncio -from copilot import CopilotClient -from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.generated.session_events import CapabilitiesChangedData from copilot.session import ( ElicitationContext, @@ -32,7 +31,7 @@ # --------------------------------------------------------------------------- -# Multi-client context (TCP mode) — same pattern as test_multi_client.py +# Multi-client context (TCP mode) — same pattern as test_multi_client.py # --------------------------------------------------------------------------- @@ -63,14 +62,12 @@ async def setup(self): # Client 1 uses TCP mode so additional clients can connect self._client1 = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - working_directory=self.work_dir, - env=self._get_env(), - use_stdio=False, - github_token=github_token, - tcp_connection_token="py-tcp-shared-test-token", - ) + connection=RuntimeConnection.for_tcp( + path=self.cli_path, connection_token="py-tcp-shared-test-token" + ), + working_directory=self.work_dir, + env=self._get_env(), + github_token=github_token, ) # Trigger connection to obtain the TCP port @@ -79,13 +76,12 @@ async def setup(self): ) await init_session.disconnect() - self._actual_port = self._client1.actual_port + self._actual_port = self._client1.runtime_port assert self._actual_port is not None self._client2 = CopilotClient( - ExternalServerConfig( - url=f"localhost:{self._actual_port}", - tcp_connection_token="py-tcp-shared-test-token", + connection=RuntimeConnection.for_uri( + f"localhost:{self._actual_port}", connection_token="py-tcp-shared-test-token" ) ) @@ -139,9 +135,8 @@ def make_external_client(self) -> CopilotClient: """Create a new external client connected to the same CLI server.""" assert self._actual_port is not None return CopilotClient( - ExternalServerConfig( - url=f"localhost:{self._actual_port}", - tcp_connection_token="py-tcp-shared-test-token", + connection=RuntimeConnection.for_uri( + f"localhost:{self._actual_port}", connection_token="py-tcp-shared-test-token" ) ) @@ -263,7 +258,7 @@ def on_event(event): unsubscribe = session1.on(on_event) - # Client 2 joins WITH elicitation handler — triggers capabilities.changed + # Client 2 joins WITH elicitation handler — triggers capabilities.changed async def handler( context: ElicitationContext, ) -> ElicitationResult: @@ -339,7 +334,7 @@ def on_disabled(event): unsub_disabled = session1.on(on_disabled) - # Force-stop client 3 — destroys the socket, triggering server-side cleanup + # Force-stop client 3 — destroys the socket, triggering server-side cleanup await client3.force_stop() await asyncio.wait_for(cap_disabled.wait(), timeout=15.0) diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index d67311598..2ed439fbe 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -12,8 +12,7 @@ from pathlib import Path from typing import Any -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from .proxy import CapiProxy @@ -80,13 +79,13 @@ async def setup(self, cli_args: list[str] | None = None): # Create the shared client (like Node.js/Go do) self._client = CopilotClient( - SubprocessConfig( - cli_path=self.cli_path, - cli_args=cli_args or [], - working_directory=self.work_dir, - env=self.get_env(), - github_token=DEFAULT_GITHUB_TOKEN, - ) + connection=RuntimeConnection.for_stdio( + path=self.cli_path, + args=tuple(cli_args or []), + ), + working_directory=self.work_dir, + env=self.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, ) async def teardown(self, test_failed: bool = False): diff --git a/python/e2e/testharness/helper.py b/python/e2e/testharness/helper.py index c603a8ec5..d64ee00b8 100644 --- a/python/e2e/testharness/helper.py +++ b/python/e2e/testharness/helper.py @@ -65,7 +65,7 @@ def on_event(event): async def _get_existing_final_response(session: CopilotSession, already_idle: bool = False): """Check existing messages for a final response.""" - messages = await session.get_messages() + messages = await session.get_events() # Find last user message final_user_message_index = -1 diff --git a/python/test_client.py b/python/test_client.py index 8add6975b..2ed57657e 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -8,25 +8,28 @@ import pytest -from copilot import CopilotClient, define_tool +from copilot import ( + CopilotClient, + RuntimeConnection, + StdioRuntimeConnection, + define_tool, +) from copilot.client import ( CloudSessionOptions, CloudSessionRepository, - ExternalServerConfig, ModelCapabilities, ModelInfo, ModelLimits, ModelSupports, - SubprocessConfig, ) -from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session import PermissionHandler, PermissionNoResult from e2e.testharness import CLI_PATH class TestPermissionHandlerOptional: @pytest.mark.asyncio async def test_create_session_allows_missing_permission_handler(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: session = await client.create_session() @@ -36,7 +39,7 @@ async def test_create_session_allows_missing_permission_handler(self): @pytest.mark.asyncio async def test_create_session_allows_none_permission_handler(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: session = await client.create_session(on_permission_request=None) @@ -46,19 +49,23 @@ async def test_create_session_allows_none_permission_handler(self): @pytest.mark.asyncio async def test_v2_permission_adapter_rejects_no_result(self): - client = CopilotClient(SubprocessConfig(CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) 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: PermissionNoResult() ) with pytest.raises(ValueError, match="protocol v2 server"): await client._handle_permission_request_v2( { "sessionId": session.session_id, - "permissionRequest": {"kind": "write"}, + "permissionRequest": { + "kind": "write", + "canOfferSessionApproval": True, + "diff": "", + "fileName": "test.txt", + "intention": "test", + }, } ) finally: @@ -66,7 +73,7 @@ async def test_v2_permission_adapter_rejects_no_result(self): @pytest.mark.asyncio async def test_resume_session_allows_none_permission_handler(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: session = await client.create_session( @@ -81,7 +88,7 @@ async def test_resume_session_allows_none_permission_handler(self): class TestCreateSessionConfig: @pytest.mark.asyncio async def test_create_session_forwards_cloud_options(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: captured = {} @@ -117,47 +124,47 @@ async def mock_request(method, params): class TestURLParsing: def test_parse_port_only_url(self): - client = CopilotClient(ExternalServerConfig(url="8080")) - assert client._actual_port == 8080 + client = CopilotClient(connection=RuntimeConnection.for_uri("8080")) + assert client._runtime_port == 8080 assert client._actual_host == "localhost" assert client._is_external_server def test_parse_host_port_url(self): - client = CopilotClient(ExternalServerConfig(url="127.0.0.1:9000")) - assert client._actual_port == 9000 + client = CopilotClient(connection=RuntimeConnection.for_uri("127.0.0.1:9000")) + assert client._runtime_port == 9000 assert client._actual_host == "127.0.0.1" assert client._is_external_server def test_parse_http_url(self): - client = CopilotClient(ExternalServerConfig(url="http://localhost:7000")) - assert client._actual_port == 7000 + client = CopilotClient(connection=RuntimeConnection.for_uri("http://localhost:7000")) + assert client._runtime_port == 7000 assert client._actual_host == "localhost" assert client._is_external_server def test_parse_https_url(self): - client = CopilotClient(ExternalServerConfig(url="https://example.com:443")) - assert client._actual_port == 443 + client = CopilotClient(connection=RuntimeConnection.for_uri("https://example.com:443")) + assert client._runtime_port == 443 assert client._actual_host == "example.com" assert client._is_external_server def test_invalid_url_format(self): with pytest.raises(ValueError, match="Invalid cli_url format"): - CopilotClient(ExternalServerConfig(url="invalid-url")) + CopilotClient(connection=RuntimeConnection.for_uri("invalid-url")) def test_invalid_port_too_high(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient(ExternalServerConfig(url="localhost:99999")) + CopilotClient(connection=RuntimeConnection.for_uri("localhost:99999")) def test_invalid_port_zero(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient(ExternalServerConfig(url="localhost:0")) + CopilotClient(connection=RuntimeConnection.for_uri("localhost:0")) def test_invalid_port_negative(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient(ExternalServerConfig(url="localhost:-1")) + CopilotClient(connection=RuntimeConnection.for_uri("localhost:-1")) def test_is_external_server_true(self): - client = CopilotClient(ExternalServerConfig(url="localhost:8080")) + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:8080")) assert client._is_external_server @@ -165,129 +172,114 @@ class TestSessionFsConfig: def test_missing_initial_cwd(self): with pytest.raises(ValueError, match="session_fs.initial_working_directory is required"): CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - log_level="error", - session_fs={ - "initial_working_directory": "", - "session_state_path": "/session-state", - "conventions": "posix", - }, - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + log_level="error", + session_fs={ + "initial_working_directory": "", + "session_state_path": "/session-state", + "conventions": "posix", + }, ) def test_missing_session_state_path(self): with pytest.raises(ValueError, match="session_fs.session_state_path is required"): CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - log_level="error", - session_fs={ - "initial_working_directory": "/", - "session_state_path": "", - "conventions": "posix", - }, - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + log_level="error", + session_fs={ + "initial_working_directory": "/", + "session_state_path": "", + "conventions": "posix", + }, ) class TestAuthOptions: def test_accepts_github_token(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - github_token="gho_test_token", - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + github_token="gho_test_token", + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.github_token == "gho_test_token" + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.github_token == "gho_test_token" def test_default_use_logged_in_user_true_without_token(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, log_level="error")) - assert isinstance(client._config, SubprocessConfig) - assert client._config.use_logged_in_user is True + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), log_level="error" + ) + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.use_logged_in_user is True def test_default_use_logged_in_user_false_with_token(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - github_token="gho_test_token", - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + github_token="gho_test_token", + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.use_logged_in_user is False + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.use_logged_in_user is False def test_explicit_use_logged_in_user_true_with_token(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - github_token="gho_test_token", - use_logged_in_user=True, - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + github_token="gho_test_token", + use_logged_in_user=True, + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.use_logged_in_user is True + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.use_logged_in_user is True def test_explicit_use_logged_in_user_false_without_token(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - use_logged_in_user=False, - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + use_logged_in_user=False, + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.use_logged_in_user is False + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.use_logged_in_user is False class TestSessionIdleTimeoutSeconds: def test_accepts_session_idle_timeout_seconds(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - session_idle_timeout_seconds=600, - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + session_idle_timeout_seconds=600, + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.session_idle_timeout_seconds == 600 + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.session_idle_timeout_seconds == 600 def test_default_session_idle_timeout_seconds_is_none(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, log_level="error")) - assert isinstance(client._config, SubprocessConfig) - assert client._config.session_idle_timeout_seconds is None + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), log_level="error" + ) + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.session_idle_timeout_seconds is None class TestCopilotHome: def test_accepts_copilot_home(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - copilot_home="/custom/copilot/home", - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + base_directory="/custom/copilot/home", + log_level="error", ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.copilot_home == "/custom/copilot/home" + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.base_directory == "/custom/copilot/home" def test_default_copilot_home_is_none(self): client = CopilotClient( - SubprocessConfig( - cli_path=CLI_PATH, - log_level="error", - ) + connection=RuntimeConnection.for_stdio(path=CLI_PATH), log_level="error" ) - assert isinstance(client._config, SubprocessConfig) - assert client._config.copilot_home is None + assert isinstance(client._options.connection, StdioRuntimeConnection) + assert client._options.base_directory is None class TestOverridesBuiltInTool: @pytest.mark.asyncio async def test_overrides_built_in_tool_sent_in_tool_definition(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -316,7 +308,7 @@ def grep(params) -> str: @pytest.mark.asyncio async def test_resume_session_sends_overrides_built_in_tool(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -353,7 +345,7 @@ def grep(params) -> str: class TestInstructionDirectories: @pytest.mark.asyncio async def test_create_session_sends_instruction_directories(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -381,7 +373,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_sends_instruction_directories(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -430,7 +422,7 @@ def handler(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=handler, ) await client.start() @@ -462,7 +454,7 @@ def handler(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=handler, ) await client.start() @@ -491,7 +483,7 @@ async def handler(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=handler, ) await client.start() @@ -522,7 +514,7 @@ def handler(): return custom_models client = CopilotClient( - SubprocessConfig(cli_path=CLI_PATH), + connection=RuntimeConnection.for_stdio(path=CLI_PATH), on_list_models=handler, ) models = await client.list_models() @@ -533,7 +525,7 @@ def handler(): class TestSessionConfigForwarding: @pytest.mark.asyncio async def test_create_session_forwards_client_name(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -554,7 +546,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_client_name(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -584,7 +576,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_forwards_enable_session_telemetry(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -606,7 +598,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_enable_session_telemetry(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -635,7 +627,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_forwards_provider_headers(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -656,7 +648,7 @@ async def mock_request(method, params): "headers": {"Authorization": "Bearer provider-token"}, "model_id": "gpt-4o", "wire_model": "my-finetune-v3", - "max_input_tokens": 100_000, + "max_prompt_tokens": 100_000, "max_output_tokens": 4096, }, ) @@ -673,7 +665,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_provider_headers(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -699,7 +691,7 @@ async def mock_request(method, params): "headers": {"Authorization": "Bearer resume-token"}, "model_id": "gpt-4o", "wire_model": "my-finetune-v3", - "max_input_tokens": 100_000, + "max_prompt_tokens": 100_000, "max_output_tokens": 4096, }, ) @@ -716,7 +708,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_session_send_forwards_request_headers(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -748,7 +740,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_forwards_agent(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -771,7 +763,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_agent(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -801,7 +793,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_defaults_include_sub_agent_streaming_events_to_true(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -824,7 +816,7 @@ async def mock_request(method, params): async def test_create_session_preserves_explicit_false_include_sub_agent_streaming_events( self, ): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -846,7 +838,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_defaults_include_sub_agent_streaming_events_to_true(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -876,7 +868,7 @@ async def mock_request(method, params): async def test_resume_session_preserves_explicit_false_include_sub_agent_streaming_events( self, ): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -905,7 +897,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_continue_pending_work(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -934,7 +926,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_omits_continue_pending_work_by_default(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -962,7 +954,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_set_model_sends_correct_rpc(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -990,7 +982,7 @@ async def mock_request(method, params): class TestCopilotClientContextManager: @pytest.mark.asyncio async def test_aenter_calls_start_and_returns_self(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) with patch.object(client, "start", new_callable=AsyncMock) as mock_start: result = await client.__aenter__() mock_start.assert_awaited_once() @@ -998,7 +990,7 @@ async def test_aenter_calls_start_and_returns_self(self): @pytest.mark.asyncio async def test_aexit_calls_stop(self): - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) with patch.object(client, "stop", new_callable=AsyncMock) as mock_stop: await client.__aexit__(None, None, None) mock_stop.assert_awaited_once() diff --git a/python/test_commands_and_elicitation.py b/python/test_commands_and_elicitation.py index 470e2f8f3..7f708c74f 100644 --- a/python/test_commands_and_elicitation.py +++ b/python/test_commands_and_elicitation.py @@ -10,8 +10,7 @@ import pytest -from copilot import CopilotClient -from copilot.client import SubprocessConfig +from copilot import CopilotClient, RuntimeConnection from copilot.session import ( AutoModeSwitchRequest, AutoModeSwitchResponse, @@ -50,7 +49,7 @@ class TestCommands: @pytest.mark.asyncio async def test_forwards_commands_in_session_create_rpc(self): """Verifies that commands (name + description) are serialized in session.create payload.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -89,7 +88,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_forwards_commands_in_session_resume_rpc(self): """Verifies that commands are serialized in session.resume payload.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -127,7 +126,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_routes_command_execute_event_to_correct_handler(self): """Verifies the command dispatch works for command.execute events.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -198,7 +197,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_sends_error_when_command_handler_throws(self): """Verifies error is sent via RPC when a command handler raises.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -256,7 +255,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_sends_error_for_unknown_command(self): """Verifies error is sent via RPC for an unrecognized command.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -317,7 +316,7 @@ class TestUiElicitation: @pytest.mark.asyncio async def test_reads_capabilities_from_session_create_response(self): """Verifies capabilities are parsed from session.create response.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -341,7 +340,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_defaults_capabilities_when_not_injected(self): """Verifies capabilities default to empty when server returns none.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -358,7 +357,7 @@ async def test_defaults_capabilities_when_not_injected(self): @pytest.mark.asyncio async def test_elicitation_throws_when_capability_is_missing(self): """Verifies that UI methods throw when elicitation is not supported.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -385,7 +384,7 @@ async def test_elicitation_throws_when_capability_is_missing(self): @pytest.mark.asyncio async def test_confirm_throws_when_capability_is_missing(self): """Verifies confirm throws when elicitation is not supported.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -409,7 +408,7 @@ class TestOnElicitationContext: @pytest.mark.asyncio async def test_sends_request_elicitation_flag_when_handler_provided(self): """Verifies requestElicitation=true is sent when onElicitationContext is provided.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -441,7 +440,7 @@ async def elicitation_handler( @pytest.mark.asyncio async def test_does_not_send_request_elicitation_when_no_handler(self): """Verifies requestElicitation=false when no handler is provided.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -469,7 +468,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_sends_mode_callback_flags_when_handlers_provided(self): """Verifies mode callback flags are sent when handlers are provided.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -494,8 +493,8 @@ def auto_handler( session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - on_exit_plan_mode=exit_handler, - on_auto_mode_switch=auto_handler, + on_exit_plan_mode_request=exit_handler, + on_auto_mode_switch_request=auto_handler, ) assert session is not None @@ -508,7 +507,7 @@ def auto_handler( @pytest.mark.asyncio async def test_sends_mode_callback_flags_on_resume_when_handlers_provided(self): """Verifies mode callback flags are sent on session.resume.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -528,8 +527,8 @@ async def mock_request(method, params): await client.resume_session( session.session_id, on_permission_request=PermissionHandler.approve_all, - on_exit_plan_mode=lambda request, invocation: {"approved": True}, - on_auto_mode_switch=lambda request, invocation: "yes", + on_exit_plan_mode_request=lambda request, invocation: {"approved": True}, + on_auto_mode_switch_request=lambda request, invocation: "yes", ) payload = captured["session.resume"] @@ -541,7 +540,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_dispatches_mode_callback_requests_to_registered_handlers(self): """Verifies direct mode requests are dispatched to registered handlers.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -570,8 +569,8 @@ async def auto_handler( session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - on_exit_plan_mode=exit_handler, - on_auto_mode_switch=auto_handler, + on_exit_plan_mode_request=exit_handler, + on_auto_mode_switch_request=auto_handler, ) exit_result = await client._handle_exit_plan_mode_request( @@ -603,7 +602,7 @@ async def auto_handler( @pytest.mark.asyncio async def test_sends_cancel_when_elicitation_handler_throws(self): """Verifies auto-cancel when the elicitation handler raises.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -648,7 +647,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_dispatches_elicitation_requested_event_to_handler(self): """Verifies that an elicitation.requested event dispatches to the handler.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -709,7 +708,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_elicitation_handler_receives_full_schema(self): """Verifies that requestedSchema passes type, properties, and required to handler.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -785,7 +784,7 @@ class TestCapabilitiesChanged: @pytest.mark.asyncio async def test_capabilities_changed_event_updates_session(self): """Verifies that a capabilities.changed event updates session capabilities.""" - client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 25b5cc2bc..8950f839b 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -17,10 +17,8 @@ ElicitationCompletedAction, ElicitationRequestedMode, ElicitationRequestedSchema, - PermissionPromptRequest, - PermissionPromptRequestKind, - PermissionRequest, - PermissionRequestKind, + PermissionPromptRequestMemory, + PermissionRequestMemory, PermissionRequestMemoryAction, SessionEventType, SessionTaskCompleteData, @@ -149,13 +147,20 @@ def test_missing_optional_fields_remain_none_after_parsing(self): the schema default instead of ``None`` and broke ``from_dict(to_dict(x))`` round-trips for instances where the field was ``None``. """ + from copilot.generated.session_events import ( + _load_PermissionPromptRequest, + _load_PermissionRequest, + ) + # #1141: PermissionRequest.action defaults to None when missing. - request = PermissionRequest.from_dict({"kind": "memory", "fact": "remember this"}) + request = _load_PermissionRequest({"kind": "memory", "fact": "remember this"}) + assert isinstance(request, PermissionRequestMemory) assert request.action is None assert PermissionRequestMemoryAction.STORE.value == "store" # sanity # #1140: PermissionPromptRequest.action defaults to None when missing. - prompt_request = PermissionPromptRequest.from_dict({"kind": "memory"}) + prompt_request = _load_PermissionPromptRequest({"kind": "memory", "fact": "remember this"}) + assert isinstance(prompt_request, PermissionPromptRequestMemory) assert prompt_request.action is None # #1139: SessionTaskCompleteData.summary defaults to None when missing. @@ -175,14 +180,28 @@ def test_optional_fields_round_trip_none(self): task = SessionTaskCompleteData(success=None, summary=None) assert SessionTaskCompleteData.from_dict(task.to_dict()) == task - # #1140: PermissionPromptRequest round-trip with action=None. - prompt = PermissionPromptRequest(kind=PermissionPromptRequestKind.MEMORY) + # #1140: PermissionPromptRequestMemory round-trip with action=None. + prompt = PermissionPromptRequestMemory(fact="test-fact") assert prompt.action is None assert "action" not in prompt.to_dict() - assert PermissionPromptRequest.from_dict(prompt.to_dict()) == prompt + assert PermissionPromptRequestMemory.from_dict(prompt.to_dict()) == prompt - # #1141: PermissionRequest round-trip with action=None. - permission = PermissionRequest(kind=PermissionRequestKind.MEMORY) + # #1141: PermissionRequestMemory round-trip with action=None. + permission = PermissionRequestMemory(fact="test-fact") assert permission.action is None assert "action" not in permission.to_dict() - assert PermissionRequest.from_dict(permission.to_dict()) == permission + assert PermissionRequestMemory.from_dict(permission.to_dict()) == permission + + # PermissionRequest is now a discriminated union; the dispatch loader + # should round-trip via the correct variant class. + from copilot.generated.session_events import _load_PermissionRequest + + round_tripped = _load_PermissionRequest(permission.to_dict()) + assert isinstance(round_tripped, PermissionRequestMemory) + assert round_tripped == permission + # PermissionPromptRequest likewise. + from copilot.generated.session_events import _load_PermissionPromptRequest + + round_tripped_prompt = _load_PermissionPromptRequest(prompt.to_dict()) + assert isinstance(round_tripped_prompt, PermissionPromptRequestMemory) + assert round_tripped_prompt == prompt diff --git a/python/test_rpc_generated.py b/python/test_rpc_generated.py index 5f484add0..5d003da42 100644 --- a/python/test_rpc_generated.py +++ b/python/test_rpc_generated.py @@ -7,7 +7,7 @@ from copilot.generated.rpc import ( CommandsApi, CommandsInvokeRequest, - SlashCommandInvocationResultKind, + SlashCommandTextResult, ) @@ -19,6 +19,6 @@ async def test_commands_invoke_deserializes_slash_command_result(): result = await api.invoke(CommandsInvokeRequest(name="help")) - assert result.kind is SlashCommandInvocationResultKind.TEXT + assert isinstance(result, SlashCommandTextResult) assert result.text == "hello" assert result.markdown is True diff --git a/python/test_telemetry.py b/python/test_telemetry.py index d10ffeb9f..6481fd525 100644 --- a/python/test_telemetry.py +++ b/python/test_telemetry.py @@ -5,7 +5,7 @@ from unittest.mock import patch from copilot._telemetry import get_trace_context, trace_context -from copilot.client import SubprocessConfig, TelemetryConfig +from copilot.client import TelemetryConfig class TestGetTraceContext: @@ -73,17 +73,6 @@ def test_telemetry_config_type(self): assert config["otlp_endpoint"] == "http://localhost:4318" assert config["capture_content"] is True - def test_telemetry_config_in_subprocess_config(self): - """TelemetryConfig can be used in SubprocessConfig.""" - config = SubprocessConfig( - telemetry={ - "otlp_endpoint": "http://localhost:4318", - "exporter_type": "otlp-http", - } - ) - assert config.telemetry is not None - assert config.telemetry["otlp_endpoint"] == "http://localhost:4318" - def test_telemetry_env_var_mapping(self): """TelemetryConfig fields map to expected environment variable names.""" config: TelemetryConfig = { diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 52b11ed59..eae6f09b4 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -263,6 +263,412 @@ function postProcessExternalUnionAliasesForPython(code: string, aliases: Map; +} +function postProcessRefBasedDiscriminatedUnionsForPython( + code: string, + definitions: Record, + definitionCollections: DefinitionCollections +): { code: string; unions: ResolvedRefBasedUnion[] } { + interface UnionInfo { + aliasName: string; + variantNames: string[]; + discriminatorProp: string; + dispatch: Array<{ value: string; typeName: string }>; + description: string | undefined; + } + const unions: UnionInfo[] = []; + + for (const [defName, definition] of Object.entries(definitions)) { + const variants = (definition.anyOf ?? definition.oneOf) as JSONSchema7[] | undefined; + if (!Array.isArray(variants) || variants.length < 2) continue; + if (!variants.every((v) => typeof v === "object" && v !== null && typeof v.$ref === "string")) { + continue; + } + + const variantRefNames = variants.map((v) => refTypeName(v.$ref as string, definitionCollections)); + const resolvedVariants = variants.map( + (v) => + resolveObjectSchema(v, definitionCollections) ?? + resolveSchema(v, definitionCollections) ?? + v + ); + if (resolvedVariants.some((rv) => !rv || rv.properties === undefined)) continue; + + const discriminator = findPyDiscriminator(resolvedVariants as JSONSchema7[]); + if (!discriminator) continue; + + const aliasName = toPascalCase(defName); + const dispatch = variants.map((_, i) => { + const discProp = (resolvedVariants[i].properties as Record)[ + discriminator.property + ]; + return { + value: String(discProp.const), + typeName: toPascalCase(variantRefNames[i]), + }; + }); + + unions.push({ + aliasName, + variantNames: variantRefNames.map(toPascalCase), + discriminatorProp: discriminator.property, + dispatch, + description: typeof definition.description === "string" ? definition.description : undefined, + }); + } + + const resolved: ResolvedRefBasedUnion[] = []; + if (unions.length === 0) return { code, unions: resolved }; + + const emittedClassNames = new Set(); + for (const match of code.matchAll(/^class (\w+)[:\(]/gm)) { + emittedClassNames.add(match[1]); + } + const acronymCandidates = (name: string): string[] => { + const substitutions: Array<[RegExp, string]> = [ + [/Api/g, "API"], + [/Mcp/g, "MCP"], + [/Url/g, "URL"], + [/Json/g, "JSON"], + [/Http/g, "HTTP"], + [/Hmac/g, "HMAC"], + [/Tcp/g, "TCP"], + [/Sql/g, "SQL"], + [/Id\b/g, "ID"], + [/Llm/g, "LLM"], + [/Cli/g, "CLI"], + ]; + const results = new Set([name]); + for (const [pattern, replacement] of substitutions) { + for (const existing of [...results]) { + results.add(existing.replace(pattern, replacement)); + } + } + return [...results]; + }; + const resolveActualName = (expected: string): string | undefined => { + for (const candidate of acronymCandidates(expected)) { + if (emittedClassNames.has(candidate)) return candidate; + } + return undefined; + }; + + for (const union of unions) { + const actualAliasName = resolveActualName(union.aliasName); + const actualVariantNames: string[] = []; + const actualDispatch: Array<{ value: string; typeName: string }> = []; + let allResolved = true; + for (let i = 0; i < union.variantNames.length; i++) { + const actual = resolveActualName(union.variantNames[i]); + if (!actual) { + allResolved = false; + break; + } + actualVariantNames.push(actual); + actualDispatch.push({ value: union.dispatch[i].value, typeName: actual }); + } + if (!allResolved || !actualAliasName) { + continue; + } + resolved.push({ + aliasName: actualAliasName, + discriminatorProp: union.discriminatorProp, + dispatch: actualDispatch, + }); + + const lines = code.split("\n"); + let classStart = -1; + for (let i = 0; i < lines.length; i++) { + if (lines[i] === `class ${actualAliasName}:` || lines[i].startsWith(`class ${actualAliasName}(`)) { + classStart = i; + break; + } + } + if (classStart >= 0) { + let blockStart = classStart; + while ( + blockStart > 0 && + (lines[blockStart - 1] === "@dataclass" || /^# /.test(lines[blockStart - 1])) + ) { + blockStart--; + } + let blockEnd = classStart + 1; + while (blockEnd < lines.length) { + const ln = lines[blockEnd]; + if ( + /^class \w/.test(ln) || + /^def \w/.test(ln) || + ln === "@dataclass" || + /^# (?:Experimental|Deprecated|Internal):/.test(ln) + ) { + break; + } + blockEnd++; + } + lines.splice(blockStart, blockEnd - blockStart); + code = lines.join("\n"); + } + + const aliasLine = union.description + ? `# ${union.description.replace(/\n/g, " ")}\n${actualAliasName} = ${actualVariantNames.join(" | ")}` + : `${actualAliasName} = ${actualVariantNames.join(" | ")}`; + + const dispatcherLines: string[] = []; + dispatcherLines.push(`def _load_${actualAliasName}(obj: Any) -> "${actualAliasName}":`); + dispatcherLines.push(` assert isinstance(obj, dict)`); + dispatcherLines.push(` kind = obj.get(${JSON.stringify(union.discriminatorProp)})`); + dispatcherLines.push(` match kind:`); + for (const m of actualDispatch) { + dispatcherLines.push(` case ${JSON.stringify(m.value)}: return ${m.typeName}.from_dict(obj)`); + } + dispatcherLines.push( + ` case _: raise ValueError(f"Unknown ${actualAliasName} ${union.discriminatorProp}: {kind!r}")` + ); + + code = `${code.trimEnd()}\n\n\n${aliasLine}\n\n\n${dispatcherLines.join("\n")}\n`; + } + + code = applyUnionRewritesToPython(code, resolved); + return { code, unions: resolved }; +} + +/** + * Rewrite occurrences of `Name.from_dict(...)` to `_load_Name(...)` and + * `to_class(Name, x)` to `(x).to_dict()` for each union the caller passes in. + * Safe to apply repeatedly — re-running on already-rewritten code is a no-op. + */ +function applyUnionRewritesToPython(code: string, unions: ResolvedRefBasedUnion[]): string { + for (const union of unions) { + code = code.replace( + new RegExp(`\\b${union.aliasName}\\.from_dict\\b`, "g"), + `_load_${union.aliasName}` + ); + code = code.replace( + new RegExp(`to_class\\(${union.aliasName},\\s*([^,)]+)\\)`, "g"), + `($1).to_dict()` + ); + } + return code; +} + +/** + * For each discriminated-union variant class, replace the dataclass-level + * discriminator field (e.g. ``kind: PermissionDecisionApproveOnceKind``) with + * a class-level constant (e.g. ``kind: ClassVar[str] = "approve-once"``). + * This lets users construct variants without supplying the discriminator + * value (``PermissionDecisionApproveOnce()`` instead of + * ``PermissionDecisionApproveOnce(kind=PermissionDecisionApproveOnceKind.APPROVE_ONCE)``), + * matching the TS / Rust / .NET / Go ergonomics for the same schema. + * + * Also rewrites the generated ``from_dict`` to skip parsing the discriminator + * (the dispatcher routed based on it; the variant class identity carries it) + * and ``to_dict`` to emit the constant directly. + */ +function postProcessDiscriminatorDefaultsForPython( + code: string, + unions: ResolvedRefBasedUnion[] +): string { + // Build variant lookup: variant class name → { prop, value }. + const variantInfo = new Map(); + for (const union of unions) { + for (const d of union.dispatch) { + // First-wins; multiple unions referencing the same variant share a + // discriminator/value pair anyway. + if (!variantInfo.has(d.typeName)) { + variantInfo.set(d.typeName, { prop: union.discriminatorProp, value: d.value }); + } + } + } + if (variantInfo.size === 0) return code; + + const lines = code.split("\n"); + const out: string[] = []; + let usedClassVar = false; + + let i = 0; + while (i < lines.length) { + const line = lines[i]; + const classMatch = line.match(/^class (\w+)[:\(]/); + if (!classMatch) { + out.push(line); + i++; + continue; + } + const className = classMatch[1]; + const info = variantInfo.get(className); + if (!info) { + out.push(line); + i++; + continue; + } + + // Find the bounds of this class block: everything indented under it. + const classStart = i; + let classEnd = i + 1; + while (classEnd < lines.length) { + const ln = lines[classEnd]; + if ( + /^class \w/.test(ln) || + /^def \w/.test(ln) || + ln === "@dataclass" || + /^# (?:Experimental|Deprecated|Internal):/.test(ln) || + ln.startsWith("@dataclass(") + ) { + break; + } + classEnd++; + } + const block = lines.slice(classStart, classEnd); + + // Locate the discriminator field declaration. Quicktype emits + // ` kind: PermissionDecisionApproveOnceKind` while the + // session-events codegen emits ` kind: str` — both match the + // simple `: ` shape (no default value, since the + // field is required in the schema). + const fieldPattern = new RegExp(`^(\\s+)${info.prop}: [\\w\\[\\], ]+$`); + let fieldIdx = -1; + for (let j = 1; j < block.length; j++) { + if (fieldPattern.test(block[j])) { + fieldIdx = j; + break; + } + } + if (fieldIdx < 0) { + // Variant class without an explicit discriminator field — leave alone. + out.push(...block); + i = classEnd; + continue; + } + const fieldIndent = (block[fieldIdx].match(/^(\s+)/) ?? ["", ""])[1]; + const literal = JSON.stringify(info.value); + // Replace the field with a class-level constant. + block[fieldIdx] = `${fieldIndent}${info.prop}: ClassVar[str] = ${literal}`; + usedClassVar = true; + + // Drop any field-trailing docstring lines that immediately followed the + // original field. Quicktype emits """..."""-style block strings; the + // session-events codegen does not emit per-field docstrings. We only + // touch the line at fieldIdx+1 if it's a docstring or blank. + // (Conservative: leave additional lines in place; they don't reference + // the dropped enum.) + + // Rewrite from_dict / to_dict bodies. + for (let j = fieldIdx + 1; j < block.length; j++) { + const ln = block[j]; + + // Drop ` = ...(obj.get(""))` parse line in from_dict. + const propAssignPattern = new RegExp( + `^\\s+${info.prop} = .+\\(obj\\.get\\(${JSON.stringify(info.prop)}\\)\\)` + ); + if (propAssignPattern.test(ln)) { + block[j] = "<<>>"; + continue; + } + + // Drop multi-line constructor kwarg of the form ` kind=kind,` — + // emitted by the session-events codegen when the constructor call + // is broken across lines. + const multilineKwargPattern = new RegExp( + `^\\s+${info.prop}=${info.prop},?\\s*$` + ); + if (multilineKwargPattern.test(ln)) { + block[j] = "<<>>"; + continue; + } + + // Convert `return X(a, prop, b)` (single-line positional) to drop + // the prop arg. Quicktype-emitted constructors are single-line. + const ctorMatch = ln.match(new RegExp(`^(\\s+)return ${className}\\((.*)\\)\\s*$`)); + if (ctorMatch) { + const argList = ctorMatch[2]; + const args = splitTopLevelCommasMulti(argList); + const filtered = args + .map((a) => a.trim()) + .filter((a) => { + const kw = a.match(/^([a-zA-Z_]\w*)\s*=/); + const name = kw ? kw[1] : a; + return name !== info.prop; + }); + block[j] = `${ctorMatch[1]}return ${className}(${filtered.join(", ")})`; + continue; + } + + // Rewrite `result[""] = to_enum(, self.)` to + // emit the class-level constant directly. + const toDictPattern = new RegExp( + `^(\\s+)result\\[${JSON.stringify(info.prop)}\\] = .+` + ); + if (toDictPattern.test(ln)) { + const indent = (ln.match(/^(\s+)/) ?? ["", ""])[1]; + block[j] = `${indent}result[${JSON.stringify(info.prop)}] = self.${info.prop}`; + continue; + } + } + + out.push(...block.filter((l) => l !== "<<>>")); + i = classEnd; + } + + let result = out.join("\n"); + if (usedClassVar) { + result = ensureClassVarImport(result); + } + return result; +} + +function splitTopLevelCommasMulti(s: string): string[] { + const parts: string[] = []; + let depth = 0; + let start = 0; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c === "(" || c === "[" || c === "{") depth++; + else if (c === ")" || c === "]" || c === "}") depth--; + else if (c === "," && depth === 0) { + parts.push(s.slice(start, i)); + start = i + 1; + } + } + parts.push(s.slice(start)); + return parts.filter((p) => p.trim().length > 0); +} + +function ensureClassVarImport(code: string): string { + // Already imported? + if (/\bfrom typing import [^\n]*\bClassVar\b/.test(code)) return code; + return code.replace( + /^from typing import (.+)$/m, + (_match, names) => { + const list = names.split(",").map((n: string) => n.trim()).filter(Boolean); + list.push("ClassVar"); + list.sort(); + return `from typing import ${[...new Set(list)].join(", ")}`; + } + ); +} + function pushPyExperimentalComment(lines: string[], subject: PyExperimentalSubject, indent = ""): void { lines.push(pyExperimentalComment(subject, indent)); } @@ -859,6 +1265,7 @@ interface PyCodegenCtx { usesTimedelta: boolean; usesIntegerTimedelta: boolean; definitions: DefinitionCollections; + refBasedUnions: ResolvedRefBasedUnion[]; } function toEnumMemberName(value: string): string { @@ -969,6 +1376,104 @@ function pyDurationResolvedType(ctx: PyCodegenCtx, isInteger: boolean): PyResolv }; } +/** + * Emit a "$ref-based discriminated union" — a Python equivalent of the + * polymorphic hierarchies that TS / Rust / .NET / Go produce for the same + * schema shape. Given a definition like + * + * "PermissionRequest": { "anyOf": [ {"$ref": "#/.../PermissionRequestShell"}, ... ] } + * + * where every variant is a `$ref` to a sibling definition and the variants + * share a `const` discriminator property (e.g. `kind`), emit each variant as a + * standalone `@dataclass`, plus a union alias and a `from_dict` dispatcher. + * + * Returns the resolved type or `undefined` if the schema doesn't match the + * expected shape (caller falls back to other paths). + */ +function tryEmitPyRefBasedDiscriminatedUnion( + aliasName: string, + resolved: JSONSchema7, + ctx: PyCodegenCtx +): PyResolvedType | undefined { + const variants = (resolved.anyOf ?? resolved.oneOf) as JSONSchema7[] | undefined; + if (!Array.isArray(variants) || variants.length < 2) return undefined; + + const variantRefNames: string[] = []; + for (const v of variants) { + if (!v || typeof v !== "object") return undefined; + const ref = (v as JSONSchema7).$ref; + if (typeof ref !== "string" || !ref.startsWith("#/definitions/")) { + return undefined; + } + variantRefNames.push(refTypeName(ref, ctx.definitions)); + } + + const resolvedVariants = variants.map( + (v) => + resolveObjectSchema(v, ctx.definitions) ?? + resolveSchema(v, ctx.definitions) ?? + (v as JSONSchema7) + ); + if (resolvedVariants.some((rv) => !rv || rv.properties === undefined)) { + return undefined; + } + const discriminator = findPyDiscriminator(resolvedVariants as JSONSchema7[]); + if (!discriminator) return undefined; + + const variantTypeNames: string[] = []; + const dispatch: Array<{ value: string; typeName: string }> = []; + for (let i = 0; i < variants.length; i++) { + const variantTypeName = toPascalCase(variantRefNames[i]); + const variantSchema = resolveObjectSchema(variants[i], ctx.definitions); + if (variantSchema) { + emitPyClass(variantTypeName, variantSchema, ctx, variantSchema.description); + } + variantTypeNames.push(variantTypeName); + const discProp = resolvedVariants[i].properties?.[discriminator.property] as JSONSchema7; + dispatch.push({ value: String(discProp.const), typeName: variantTypeName }); + } + + if (!ctx.aliasesByName.has(aliasName)) { + const lines: string[] = []; + if (resolved.description) { + lines.push(`# ${resolved.description}`); + } + lines.push(`${aliasName} = ${variantTypeNames.join(" | ")}`); + ctx.aliasesByName.add(aliasName); + ctx.aliases.push(lines.join("\n")); + ctx.refBasedUnions.push({ + aliasName, + discriminatorProp: discriminator.property, + dispatch, + }); + } + + const dispatcherName = `_load_${aliasName}`; + if (!ctx.generatedNames.has(dispatcherName)) { + ctx.generatedNames.add(dispatcherName); + const lines: string[] = []; + lines.push(`def ${dispatcherName}(obj: Any) -> "${aliasName}":`); + lines.push(` assert isinstance(obj, dict)`); + lines.push(` kind = obj.get(${JSON.stringify(discriminator.property)})`); + lines.push(` match kind:`); + for (const m of dispatch) { + lines.push( + ` case ${JSON.stringify(m.value)}: return ${m.typeName}.from_dict(obj)` + ); + } + lines.push( + ` case _: raise ValueError(f"Unknown ${aliasName} ${discriminator.property}: {kind!r}")` + ); + ctx.classes.push(lines.join("\n")); + } + + return { + annotation: aliasName, + fromExpr: (expr) => `${dispatcherName}(${expr})`, + toExpr: (expr) => `${expr}.to_dict()`, + }; +} + function isPyBase64StringSchema(schema: JSONSchema7): boolean { return schema.format === "byte" || (schema as Record).contentEncoding === "base64"; } @@ -1228,6 +1733,17 @@ function resolvePyPropertyType( return isRequired ? enumResolved : pyOptionalResolvedType(enumResolved); } + // Emit "$ref"-based discriminated unions as proper Python unions + // (per-variant dataclasses + alias + dispatcher) rather than flat + // merged dataclasses. Matches the polymorphic hierarchies emitted + // by the TS / Rust / .NET / Go SDKs for the same schema shape. + if (resolved.anyOf || resolved.oneOf) { + const unionResolved = tryEmitPyRefBasedDiscriminatedUnion(typeName, resolved, ctx); + if (unionResolved) { + return isRequired ? unionResolved : pyOptionalResolvedType(unionResolved); + } + } + const resolvedObject = resolveObjectSchema(propSchema, ctx.definitions); if (isNamedPyObjectSchema(resolvedObject)) { emitPyClass(typeName, resolvedObject, ctx, resolvedObject.description); @@ -1275,6 +1791,21 @@ function resolvePyPropertyType( if (nonNull.length > 1) { const discriminator = findPyDiscriminator(nonNull); if (discriminator) { + // Prefer the proper per-variant union shape when every variant + // is a `$ref` to a sibling definition. Same rationale as in the + // top-level $ref branch above: matches TS/Rust/.NET/Go. + if (variantSchemas.every((s) => typeof s.$ref === "string")) { + const unionResolved = tryEmitPyRefBasedDiscriminatedUnion( + nestedName, + propSchema, + ctx + ); + if (unionResolved) { + return hasNull || !isRequired + ? pyOptionalResolvedType(unionResolved) + : unionResolved; + } + } emitPyFlatDiscriminatedUnion( nestedName, discriminator.property, @@ -1753,6 +2284,7 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { usesTimedelta: false, usesIntegerTimedelta: false, definitions: collectDefinitionCollections(schema as Record), + refBasedUnions: [], }; for (const variant of variants) { @@ -2082,7 +2614,9 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(``); out.push(``); - return postProcessPythonSessionEventCode(out.join("\n")); + let finalCode = postProcessPythonSessionEventCode(out.join("\n")); + finalCode = postProcessDiscriminatorDefaultsForPython(finalCode, ctx.refBasedUnions); + return finalCode; } async function generateSessionEvents(schemaPath?: string): Promise { @@ -2206,6 +2740,14 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema inputData, lang: "python", rendererOptions: { "python-version": "3.7" }, + // Disable quicktype's structural-equality merging of class types. + // It produces fuzzy synthesized names (e.g. ``PermissionDecisionApproveForIonApproval`` + // as the merge of ``PermissionDecisionApproveFor{Session,Location}Approval``) which + // are unstable: any future divergence between the variants would silently change + // the generated class name. We rely on the schema's named definitions and resolve + // structural unions via :func:`postProcessRefBasedDiscriminatedUnionsForPython`, + // so the merging is also redundant. + inferenceFlags: { combineClasses: false }, }); let typesCode = qtResult.lines.join("\n"); @@ -2221,6 +2763,12 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema typesCode = collapsePlaceholderPythonDataclasses(typesCode, knownDefNames); typesCode = postProcessExternalUnionAliasesForPython(typesCode, externalUnionAliases); typesCode = postProcessExternalRefsForPython(typesCode, externalRefs.placeholderNames, externalEnumNames); + const { code: typesCodeAfterUnions, unions: refBasedUnions } = postProcessRefBasedDiscriminatedUnionsForPython( + typesCode, + allDefinitions, + allDefinitionCollections + ); + typesCode = typesCodeAfterUnions; typesCode = modernizePython(typesCode); // Fix quicktype's Enum-suffix renaming: quicktype sometimes renames "Xyz" to @@ -2370,6 +2918,7 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema AUTO-GENERATED FILE - DO NOT EDIT Generated from: api.schema.json """ +from __future__ import annotations from typing import TYPE_CHECKING @@ -2461,6 +3010,11 @@ def _patch_model_capabilities(data: dict) -> dict: /(_patch_model_capabilities\(await self\._client\.request\("models\.list"[^)]*\)[^)]*\))/, "$1)", ); + // Apply union rewrites to the assembled code so RPC method wrappers + // generated after the types section also route Name.from_dict / to_class + // through the discriminator dispatcher. + finalCode = applyUnionRewritesToPython(finalCode, refBasedUnions); + finalCode = postProcessDiscriminatorDefaultsForPython(finalCode, refBasedUnions); finalCode = unwrapRedundantPythonLambdas(finalCode); const outPath = await writeGeneratedFile("python/copilot/generated/rpc.py", finalCode); diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index 3ad893ba5..5b623ff87 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -1,8 +1,8 @@ import asyncio import os import sys + from copilot import CopilotClient -from copilot.client import SubprocessConfig ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514") @@ -14,29 +14,25 @@ async def main(): - client = CopilotClient(SubprocessConfig( - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": ANTHROPIC_MODEL, - "provider": { + session = await client.create_session( + model=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( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print(response.data.content) diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 1ae214261..031ff47ee 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -1,8 +1,8 @@ import asyncio import os import sys + from copilot import CopilotClient -from copilot.client import SubprocessConfig AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY") @@ -15,14 +15,12 @@ async def main(): - client = CopilotClient(SubprocessConfig( - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": AZURE_OPENAI_MODEL, - "provider": { + session = await client.create_session( + model=AZURE_OPENAI_MODEL, + provider={ "type": "azure", "base_url": AZURE_OPENAI_ENDPOINT, "api_key": AZURE_OPENAI_API_KEY, @@ -30,17 +28,15 @@ 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( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print(response.data.content) diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index 78019acd7..90c4838f8 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -1,8 +1,7 @@ import asyncio import os -import sys + from copilot import CopilotClient -from copilot.client import SubprocessConfig OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1") OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b") @@ -13,28 +12,24 @@ async def main(): - client = CopilotClient(SubprocessConfig( - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": OLLAMA_MODEL, - "provider": { + session = await client.create_session( + model=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( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print(response.data.content) diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index 8362963b2..e9c673aa0 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -1,8 +1,8 @@ import asyncio import os import sys + from copilot import CopilotClient -from copilot.client import SubprocessConfig 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") @@ -14,24 +14,20 @@ async def main(): - client = CopilotClient(SubprocessConfig( - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient() try: - session = await client.create_session({ - "model": OPENAI_MODEL, - "provider": { + session = await client.create_session( + model=OPENAI_MODEL, + provider={ "type": "openai", "base_url": OPENAI_BASE_URL, "api_key": OPENAI_API_KEY, }, - }) - - response = await session.send_and_wait( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print(response.data.content) diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index afba29254..0d5a5ee9d 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -5,8 +5,6 @@ import urllib.request from copilot import CopilotClient -from copilot.client import SubprocessConfig - DEVICE_CODE_URL = "https://github.com/login/device/code" ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token" @@ -61,7 +59,9 @@ def poll_for_access_token(client_id: str, device_code: str, interval: int) -> st if data.get("error") == "slow_down": delay_seconds = int(data.get("interval", delay_seconds + 5)) continue - raise RuntimeError(data.get("error_description") or data.get("error") or "OAuth polling failed") + raise RuntimeError( + data.get("error_description") or data.get("error") or "OAuth polling failed" + ) async def main(): @@ -79,13 +79,10 @@ async def main(): display_name = f" ({user.get('name')})" if user.get("name") else "" print(f"Authenticated as: {user.get('login')}{display_name}") - client = CopilotClient(SubprocessConfig( - github_token=token, - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + client = CopilotClient(github_token=token) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(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 2684a30b8..d53d89854 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -4,9 +4,9 @@ import sys import urllib.request -from flask import Flask, request, jsonify -from copilot import CopilotClient -from copilot.client import ExternalServerConfig +from flask import Flask, jsonify, request + +from copilot import CopilotClient, RuntimeConnection app = Flask(__name__) @@ -14,10 +14,10 @@ async def ask_copilot(prompt: str) -> str: - client = CopilotClient(ExternalServerConfig(url=CLI_URL)) + client = CopilotClient(connection=RuntimeConnection.for_uri(CLI_URL)) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") response = await session.send_and_wait(prompt) @@ -70,6 +70,7 @@ def self_test(port: int): ) server_thread.start() import time + time.sleep(1) self_test(port) else: diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index b441bec51..1bf32b475 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -1,20 +1,20 @@ import asyncio import os -from copilot import CopilotClient -from copilot.client import ExternalServerConfig + +from copilot import CopilotClient, RuntimeConnection async def main(): - client = CopilotClient(ExternalServerConfig( - url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - )) + client = CopilotClient( + connection=RuntimeConnection.for_uri( + os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + ), + ) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/bundling/container-proxy/proxy.py b/test/scenarios/bundling/container-proxy/proxy.py index afe999a4c..688b9f8c1 100644 --- a/test/scenarios/bundling/container-proxy/proxy.py +++ b/test/scenarios/bundling/container-proxy/proxy.py @@ -12,7 +12,7 @@ import json import sys import time -from http.server import HTTPServer, BaseHTTPRequestHandler +from http.server import BaseHTTPRequestHandler, HTTPServer class ProxyHandler(BaseHTTPRequestHandler): diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index b441bec51..1bf32b475 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -1,20 +1,20 @@ import asyncio import os -from copilot import CopilotClient -from copilot.client import ExternalServerConfig + +from copilot import CopilotClient, RuntimeConnection async def main(): - client = CopilotClient(ExternalServerConfig( - url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - )) + client = CopilotClient( + connection=RuntimeConnection.for_uri( + os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + ), + ) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index 39ce2bb81..63c309995 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -1,21 +1,18 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index ba224ef24..3a7b5906c 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -1,15 +1,14 @@ import asyncio import os -from copilot import CopilotClient -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult +from copilot import CopilotClient +from copilot.generated.rpc import PermissionDecisionApproveOnce hook_log: list[str] = [] async def auto_approve_permission(request, invocation): - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() async def on_session_start(input_data, invocation): @@ -42,25 +41,22 @@ async def on_error_occurred(input_data, invocation): async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) 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, - }, - } + 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, + }, ) 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 677ca58d0..138d6310a 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -1,8 +1,8 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionApproveOnce # Track which tools requested permission permission_log: list[str] = [] @@ -10,7 +10,7 @@ async def log_permission(request, invocation): permission_log.append(f"approved:{request.tool_name}") - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() async def auto_approve_tool(input_data, invocation): @@ -18,18 +18,15 @@ async def auto_approve_tool(input_data, invocation): async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": log_permission, - "hooks": {"on_pre_tool_use": auto_approve_tool}, - } + model="claude-haiku-4.5", + on_permission_request=log_permission, + 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 07a7eb40e..9eff3c7cc 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -1,15 +1,14 @@ import asyncio import os -from copilot import CopilotClient -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult +from copilot import CopilotClient +from copilot.generated.rpc import PermissionDecisionApproveOnce input_log: list[str] = [] async def auto_approve_permission(request, invocation): - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() async def auto_approve_tool(input_data, invocation): @@ -22,19 +21,16 @@ async def handle_user_input(request, invocation): async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) 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}, - } + 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}, ) 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 ece50a662..3bb6e10a3 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -1,21 +1,20 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - }) + session = await client.create_session(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.") + response = await session.send_and_wait( + "Use the grep tool to search for the word 'SDK' in README.md and show the matching lines." + ) if response: print(f"Response: {response.data.content}") diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 722c1e5e1..71811d377 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -1,26 +1,27 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - "available_tools": [], - "system_message": { + session = await client.create_session( + model="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("Use the grep tool to search for 'SDK' in README.md.") + response = await session.send_and_wait( + "Use the grep tool to search for 'SDK' in README.md." + ) if response: print(f"Response: {response.data.content}") diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index fdf259c6a..998770298 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -1,24 +1,21 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig SYSTEM_PROMPT = """You are a helpful assistant. Answer questions about attached files concisely.""" async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": [], - } + model="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 122f44895..ae4b0264f 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -1,30 +1,27 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: - session = await client.create_session({ - "model": "claude-opus-4.6", - "reasoning_effort": "low", - "available_tools": [], - "system_message": { + session = await client.create_session( + model="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( - "What is the capital of France?" ) + response = await session.send_and_wait("What is the capital of France?") + if response: print("Reasoning effort: low") print(f"Response: {response.data.content}") diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index b77c1e4a1..490347234 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -1,29 +1,24 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig PIRATE_PROMPT = """You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.""" async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, - "available_tools": [], - } + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": PIRATE_PROMPT}, + available_tools=[], ) - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index a32dc5e10..beee2ba06 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -1,43 +1,34 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig PIRATE_PROMPT = "You are a pirate. Always say Arrr!" ROBOT_PROMPT = "You are a robot. Always say BEEP BOOP!" async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: session1, session2 = await asyncio.gather( client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, - "available_tools": [], - } + model="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": [], - } + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": ROBOT_PROMPT}, + available_tools=[], ), ) response1, response2 = await asyncio.gather( - session1.send_and_wait( - "What is the capital of France?" - ), - session2.send_and_wait( - "What is the capital of France?" - ), + session1.send_and_wait("What is the capital of France?"), + session2.send_and_wait("What is the capital of France?"), ) if response1: diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 724dc155d..a41b2b4fa 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -1,29 +1,28 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - "available_tools": [], - "system_message": { + session = await client.create_session( + model="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 ccb9c69f0..6d7ae02a2 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -1,28 +1,20 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: # 1. Create a session - session = await client.create_session( - { - "model": "claude-haiku-4.5", - "available_tools": [], - } - ) + session = await client.create_session(model="claude-haiku-4.5", available_tools=[]) # 2. Send the secret word - await session.send_and_wait( - "Remember this: the secret word is PINEAPPLE." - ) + await session.send_and_wait("Remember this: the secret word is PINEAPPLE.") # 3. Get the session ID (don't disconnect — resume needs the session to persist) session_id = session.session_id @@ -32,9 +24,7 @@ async def main(): print("Session resumed") # 5. Ask for the secret word - response = await resumed.send_and_wait( - "What was the secret word I told you?" - ) + response = await resumed.send_and_wait("What was the secret word I told you?") if response: print(response.data.content) diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index e2312cd14..6c1c19f7b 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -1,22 +1,16 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: - session = await client.create_session( - { - "model": "claude-haiku-4.5", - "streaming": True, - } - ) + session = await client.create_session(model="claude-haiku-4.5", streaming=True) chunk_count = 0 @@ -27,9 +21,7 @@ def on_event(event): session.on(on_event) - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index bf6e3978c..aa5e254ae 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -1,7 +1,7 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig from copilot.tools import Tool @@ -10,10 +10,9 @@ async def analyze_handler(args): async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: session = await client.create_session( diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index 2fa81b82d..80319e79a 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -1,14 +1,13 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: # MCP server config — demonstrates the configuration pattern. @@ -16,7 +15,11 @@ async def main(): # Otherwise, runs without MCP tools as a build/integration test. mcp_servers = {} if os.environ.get("MCP_SERVER_CMD"): - args = os.environ.get("MCP_SERVER_ARGS", "").split() if os.environ.get("MCP_SERVER_ARGS") else [] + args = ( + os.environ.get("MCP_SERVER_ARGS", "").split() + if os.environ.get("MCP_SERVER_ARGS") + else [] + ) mcp_servers["example"] = { "type": "stdio", "command": os.environ["MCP_SERVER_CMD"], @@ -36,9 +39,7 @@ async def main(): session = await client.create_session(session_config) - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index c3eeb6a17..61fa98ee1 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -1,7 +1,7 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig SYSTEM_PROMPT = """You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -10,23 +10,18 @@ async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": [], - } + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=[], ) - response = await session.send_and_wait( - "Use the bash tool to run 'echo hello'." - ) + response = await session.send_and_wait("Use the bash tool to run 'echo hello'.") if response: print(response.data.content) diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index a6d6bf2c0..30b82fc1f 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -3,21 +3,19 @@ from pathlib import Path from copilot import CopilotClient -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionApproveOnce async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") session = await client.create_session( - on_permission_request=lambda _, __: PermissionRequestResult(kind="approve-once"), + on_permission_request=lambda _, __: PermissionDecisionApproveOnce(), model="claude-haiku-4.5", skill_directories=[skills_dir], hooks={ diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 9da4ca571..711e8301e 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -1,24 +1,21 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig 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.""" async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": ["grep", "glob", "view"], - } + model="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/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 687933973..9aaaa9022 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -4,7 +4,6 @@ from pydantic import BaseModel, Field from copilot import CopilotClient, define_tool -from copilot.client import SubprocessConfig from copilot.session import PermissionHandler @@ -12,25 +11,28 @@ class GrepParams(BaseModel): query: str = Field(description="Search query") -@define_tool("grep", description="A custom grep implementation that overrides the built-in", overrides_built_in_tool=True) +@define_tool( + "grep", + description="A custom grep implementation that overrides the built-in", + overrides_built_in_tool=True, +) def custom_grep(params: GrepParams) -> str: return f"CUSTOM_GREP_RESULT: {params.query}" async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, model="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( - "Use grep to search for the word 'hello'" - ) + response = await session.send_and_wait("Use grep to search for the word 'hello'") if response: print(response.data.content) diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index 57b197509..048ba1fd1 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -1,10 +1,11 @@ import asyncio import os -from copilot import CopilotClient, define_tool -from copilot.client import SubprocessConfig -from copilot.session import PermissionRequestResult + from pydantic import BaseModel, Field +from copilot import CopilotClient, define_tool +from copilot.generated.rpc import PermissionDecisionApproveOnce + # In-memory virtual filesystem virtual_fs: dict[str, str] = {} @@ -40,7 +41,7 @@ def list_files() -> str: async def auto_approve_permission(request, invocation): - return PermissionRequestResult(kind="approve-once") + return PermissionDecisionApproveOnce() async def auto_approve_tool(input_data, invocation): @@ -48,10 +49,9 @@ async def auto_approve_tool(input_data, invocation): async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: session = await client.create_session( diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index d1d4505a8..cc79f9721 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -1,23 +1,23 @@ import asyncio import os import sys -from copilot import CopilotClient -from copilot.client import ExternalServerConfig + +from copilot import CopilotClient, RuntimeConnection async def main(): - client = CopilotClient(ExternalServerConfig( - url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - )) + client = CopilotClient( + connection=RuntimeConnection.for_uri( + os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + ), + ) try: # First session print("--- Session 1 ---") - session1 = await client.create_session({"model": "claude-haiku-4.5"}) + session1 = await client.create_session(model="claude-haiku-4.5") - response1 = await session1.send_and_wait( - "What is the capital of France?" - ) + response1 = await session1.send_and_wait("What is the capital of France?") if response1 and response1.data.content: print(response1.data.content) @@ -30,11 +30,9 @@ 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(model="claude-haiku-4.5") - response2 = await session2.send_and_wait( - "What is the capital of France?" - ) + response2 = await session2.send_and_wait("What is the capital of France?") if response2 and response2.data.content: print(response2.data.content) diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index 39ce2bb81..63c309995 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -1,21 +1,18 @@ import asyncio import os + from copilot import CopilotClient -from copilot.client import SubprocessConfig async def main(): - client = CopilotClient(SubprocessConfig( + client = CopilotClient( github_token=os.environ.get("GITHUB_TOKEN"), - cli_path=os.environ.get("COPILOT_CLI_PATH"), - )) + ) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index b441bec51..1bf32b475 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -1,20 +1,20 @@ import asyncio import os -from copilot import CopilotClient -from copilot.client import ExternalServerConfig + +from copilot import CopilotClient, RuntimeConnection async def main(): - client = CopilotClient(ExternalServerConfig( - url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - )) + client = CopilotClient( + connection=RuntimeConnection.for_uri( + os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + ), + ) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(model="claude-haiku-4.5") - response = await session.send_and_wait( - "What is the capital of France?" - ) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content)