diff --git a/docs/auth/byok.md b/docs/auth/byok.md index 8d9650280..4099f212e 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -335,7 +335,7 @@ const client = new CopilotClient({ ```python from copilot import CopilotClient -from copilot.types import ModelInfo, ModelCapabilities, ModelSupports, ModelLimits +from copilot.client import ModelInfo, ModelCapabilities, ModelSupports, ModelLimits client = CopilotClient({ "on_list_models": lambda: [ diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index 47712d9cf..60cbebef1 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -65,7 +65,7 @@ const session = await client.createSession({ ```python from copilot import CopilotClient -from copilot.types import PermissionRequestResult +from copilot.session import PermissionRequestResult client = CopilotClient() await client.start() diff --git a/docs/features/image-input.md b/docs/features/image-input.md index acec80d4a..c114f7558 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -69,7 +69,7 @@ await session.send({ ```python from copilot import CopilotClient -from copilot.types import PermissionRequestResult +from copilot.session import PermissionRequestResult client = CopilotClient() await client.start() diff --git a/docs/features/skills.md b/docs/features/skills.md index 466c637ff..9065697c5 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -43,7 +43,7 @@ await session.sendAndWait({ prompt: "Review this code for security issues" }); ```python from copilot import CopilotClient -from copilot.types import PermissionRequestResult +from copilot.session import PermissionRequestResult async def main(): client = CopilotClient() diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md index 7da349e1c..a3e1b6d2b 100644 --- a/docs/features/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -70,7 +70,7 @@ await session.send({ ```python from copilot import CopilotClient -from copilot.types import PermissionRequestResult +from copilot.session import PermissionRequestResult async def main(): client = CopilotClient() @@ -229,7 +229,7 @@ await session.send({ ```python from copilot import CopilotClient -from copilot.types import PermissionRequestResult +from copilot.session import PermissionRequestResult async def main(): client = CopilotClient() diff --git a/docs/getting-started.md b/docs/getting-started.md index 6c0aee72e..f737c0276 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -129,7 +129,8 @@ Create `main.py`: ```python import asyncio -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient +from copilot.session import PermissionHandler async def main(): client = CopilotClient() @@ -275,7 +276,8 @@ Update `main.py`: ```python import asyncio import sys -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient +from copilot.session import PermissionHandler from copilot.generated.session_events import SessionEventType async def main(): @@ -651,7 +653,8 @@ Update `main.py`: import asyncio import random import sys -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient +from copilot.session import PermissionHandler from copilot.tools import define_tool from copilot.generated.session_events import SessionEventType from pydantic import BaseModel, Field @@ -919,7 +922,8 @@ Create `weather_assistant.py`: import asyncio import random import sys -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient +from copilot.session import PermissionHandler from copilot.tools import define_tool from copilot.generated.session_events import SessionEventType from pydantic import BaseModel, Field @@ -1290,7 +1294,8 @@ const session = await client.createSession({ onPermissionRequest: approveAll }); Python ```python -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient +from copilot.session import PermissionHandler client = CopilotClient({ "cli_url": "localhost:4321" diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md index a67906ac9..da97d5107 100644 --- a/docs/hooks/error-handling.md +++ b/docs/hooks/error-handling.md @@ -35,7 +35,7 @@ type ErrorOccurredHandler = ( ```python -from copilot.types import ErrorOccurredHookInput, HookInvocation, ErrorOccurredHookOutput +from copilot.session import ErrorOccurredHookInput, ErrorOccurredHookOutput from typing import Callable, Awaitable ErrorOccurredHandler = Callable[ diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md index 029e9eb2f..fed1af727 100644 --- a/docs/hooks/post-tool-use.md +++ b/docs/hooks/post-tool-use.md @@ -35,7 +35,7 @@ type PostToolUseHandler = ( ```python -from copilot.types import PostToolUseHookInput, HookInvocation, PostToolUseHookOutput +from copilot.session import PostToolUseHookInput, PostToolUseHookOutput from typing import Callable, Awaitable PostToolUseHandler = Callable[ diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md index e1bb97495..df23bbd55 100644 --- a/docs/hooks/pre-tool-use.md +++ b/docs/hooks/pre-tool-use.md @@ -35,7 +35,7 @@ type PreToolUseHandler = ( ```python -from copilot.types import PreToolUseHookInput, HookInvocation, PreToolUseHookOutput +from copilot.session import PreToolUseHookInput, PreToolUseHookOutput from typing import Callable, Awaitable PreToolUseHandler = Callable[ diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index 4efd33ccc..e86f5d7a9 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -39,7 +39,7 @@ type SessionStartHandler = ( ```python -from copilot.types import SessionStartHookInput, HookInvocation, SessionStartHookOutput +from copilot.session import SessionStartHookInput, SessionStartHookOutput from typing import Callable, Awaitable SessionStartHandler = Callable[ @@ -249,7 +249,7 @@ type SessionEndHandler = ( ```python -from copilot.types import SessionEndHookInput, HookInvocation +from copilot.session import SessionEndHookInput from typing import Callable, Awaitable SessionEndHandler = Callable[ diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md index 2aca7f1ce..89831e34b 100644 --- a/docs/hooks/user-prompt-submitted.md +++ b/docs/hooks/user-prompt-submitted.md @@ -35,7 +35,7 @@ type UserPromptSubmittedHandler = ( ```python -from copilot.types import UserPromptSubmittedHookInput, HookInvocation, UserPromptSubmittedHookOutput +from copilot.session import UserPromptSubmittedHookInput, UserPromptSubmittedHookOutput from typing import Callable, Awaitable UserPromptSubmittedHandler = Callable[ diff --git a/docs/setup/azure-managed-identity.md b/docs/setup/azure-managed-identity.md index 40d87c5ba..71ffe49b1 100644 --- a/docs/setup/azure-managed-identity.md +++ b/docs/setup/azure-managed-identity.md @@ -42,7 +42,8 @@ import asyncio import os from azure.identity import DefaultAzureCredential -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient +from copilot.session import PermissionHandler COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default" @@ -83,7 +84,8 @@ Bearer tokens expire (typically after ~1 hour). For servers or long-running agen ```python from azure.identity import DefaultAzureCredential -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient +from copilot.session import PermissionHandler COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default" diff --git a/python/README.md b/python/README.md index 2394c351a..b92142a5f 100644 --- a/python/README.md +++ b/python/README.md @@ -247,7 +247,9 @@ session = await client.create_session( For users who prefer manual schema definition: ```python -from copilot import CopilotClient, Tool, PermissionHandler +from copilot import CopilotClient +from copilot.tools import Tool +from copilot.session import PermissionHandler async def lookup_issue(invocation): issue_id = invocation["arguments"]["id"] diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index e1fdf9253..92764c0e8 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -4,84 +4,16 @@ JSON-RPC based SDK for programmatic control of GitHub Copilot CLI """ -from .client import CopilotClient +from .client import CopilotClient, ExternalServerConfig, SubprocessConfig from .session import CopilotSession from .tools import define_tool -from .types import ( - Attachment, - AzureProviderOptions, - BlobAttachment, - ConnectionState, - CustomAgentConfig, - DirectoryAttachment, - ExternalServerConfig, - FileAttachment, - GetAuthStatusResponse, - GetStatusResponse, - MCPLocalServerConfig, - MCPRemoteServerConfig, - MCPServerConfig, - ModelBilling, - ModelCapabilities, - ModelInfo, - ModelPolicy, - PermissionHandler, - PermissionRequest, - PermissionRequestResult, - PingResponse, - ProviderConfig, - SelectionAttachment, - SessionContext, - SessionEvent, - SessionListFilter, - SessionMetadata, - StopError, - SubprocessConfig, - TelemetryConfig, - Tool, - ToolHandler, - ToolInvocation, - ToolResult, -) __version__ = "0.1.0" __all__ = [ - "Attachment", - "AzureProviderOptions", - "BlobAttachment", "CopilotClient", "CopilotSession", - "ConnectionState", - "CustomAgentConfig", - "DirectoryAttachment", "ExternalServerConfig", - "FileAttachment", - "GetAuthStatusResponse", - "GetStatusResponse", - "MCPLocalServerConfig", - "MCPRemoteServerConfig", - "MCPServerConfig", - "ModelBilling", - "ModelCapabilities", - "ModelInfo", - "ModelPolicy", - "PermissionHandler", - "PermissionRequest", - "PermissionRequestResult", - "PingResponse", - "ProviderConfig", - "SelectionAttachment", - "SessionContext", - "SessionEvent", - "SessionListFilter", - "SessionMetadata", - "StopError", "SubprocessConfig", - "TelemetryConfig", - "Tool", - "ToolHandler", - "ToolInvocation", - "ToolResult", "define_tool", ] diff --git a/python/copilot/client.py b/python/copilot/client.py index 28050088e..e39da1907 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -5,7 +5,8 @@ to the Copilot CLI server and provides session management capabilities. Example: - >>> from copilot import CopilotClient, PermissionHandler + >>> from copilot import CopilotClient + >>> from copilot.session import PermissionHandler >>> >>> async with CopilotClient() as client: ... session = await client.create_session( @@ -14,6 +15,8 @@ ... await session.send("Hello!") """ +from __future__ import annotations + import asyncio import inspect import os @@ -24,43 +27,625 @@ import threading import uuid from collections.abc import Awaitable, Callable +from dataclasses import KW_ONLY, dataclass, field from pathlib import Path -from typing import Any, cast, overload +from typing import Any, Literal, TypedDict, cast, overload from ._jsonrpc import JsonRpcClient, ProcessExitedError from ._sdk_protocol_version import get_sdk_protocol_version from ._telemetry import get_trace_context, trace_context from .generated.rpc import ServerRpc from .generated.session_events import PermissionRequest, session_event_from_dict -from .session import CopilotSession -from .types import ( - ConnectionState, +from .session import ( + CopilotSession, CustomAgentConfig, - ExternalServerConfig, - GetAuthStatusResponse, - GetStatusResponse, InfiniteSessionConfig, MCPServerConfig, - ModelInfo, - PingResponse, ProviderConfig, ReasoningEffort, SessionEvent, SessionHooks, - SessionLifecycleEvent, - SessionLifecycleEventType, - SessionLifecycleHandler, - SessionListFilter, - SessionMetadata, - StopError, - SubprocessConfig, SystemMessageConfig, - Tool, - ToolInvocation, - ToolResult, UserInputHandler, _PermissionHandlerFn, ) +from .tools import Tool, ToolInvocation, ToolResult + +# ============================================================================ +# Connection Types +# ============================================================================ + +ConnectionState = Literal["disconnected", "connecting", "connected", "error"] + +LogLevel = Literal["none", "error", "warning", "info", "debug", "all"] + + +class TelemetryConfig(TypedDict, total=False): + """Configuration for OpenTelemetry integration with the Copilot CLI.""" + + otlp_endpoint: str + """OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT.""" + file_path: str + """File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH.""" + exporter_type: str + """Exporter backend type: "otlp-http" or "file". Sets COPILOT_OTEL_EXPORTER_TYPE.""" + source_name: str + """Instrumentation scope name. Sets COPILOT_OTEL_SOURCE_NAME.""" + capture_content: bool + """Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501 + + +@dataclass +class SubprocessConfig: + """Config for spawning a local Copilot CLI subprocess. + + 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", + ... ) + """ + + cli_path: str | None = None + """Path to the Copilot CLI executable. ``None`` uses the bundled binary.""" + + cli_args: list[str] = field(default_factory=list) + """Extra arguments passed to the CLI executable (inserted before SDK-managed args).""" + + _: KW_ONLY + + cwd: str | None = None + """Working directory for the CLI process. ``None`` uses the current directory.""" + + use_stdio: bool = True + """Use stdio transport (``True``, default) or TCP (``False``).""" + + 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.""" + + env: dict[str, str] | None = None + """Environment variables for the CLI process. ``None`` inherits the current env.""" + + github_token: str | None = None + """GitHub token for authentication. Takes priority over other auth methods.""" + + use_logged_in_user: bool | None = None + """Use the logged-in user for authentication. + + ``None`` (default) resolves to ``True`` unless ``github_token`` is set. + """ + + telemetry: TelemetryConfig | None = None + """OpenTelemetry configuration. Providing this enables telemetry — no separate flag needed.""" + + +@dataclass +class ExternalServerConfig: + """Config for connecting to an existing Copilot CLI server over TCP. + + Example: + >>> config = ExternalServerConfig(url="localhost:3000") + >>> client = CopilotClient(config) + """ + + url: str + """Server URL. Supports ``"host:port"``, ``"http://host:port"``, or just ``"port"``.""" + + +# ============================================================================ +# Response Types +# ============================================================================ + + +@dataclass +class PingResponse: + """Response from ping""" + + message: str # Echo message with "pong: " prefix + timestamp: int # Server timestamp in milliseconds + protocolVersion: 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: + raise ValueError( + f"Missing required fields in PingResponse: message={message}, " + f"timestamp={timestamp}, protocolVersion={protocolVersion}" + ) + return PingResponse(str(message), int(timestamp), int(protocolVersion)) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = self.message + result["timestamp"] = self.timestamp + result["protocolVersion"] = self.protocolVersion + return result + + +@dataclass +class StopError(Exception): + """Error that occurred during client stop cleanup.""" + + message: str # Error message describing what failed during cleanup + + def __post_init__(self) -> None: + Exception.__init__(self, self.message) + + @staticmethod + def from_dict(obj: Any) -> StopError: + assert isinstance(obj, dict) + message = obj.get("message") + if message is None: + raise ValueError("Missing required field 'message' in StopError") + return StopError(str(message)) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = self.message + return result + + +@dataclass +class GetStatusResponse: + """Response from status.get""" + + version: str # Package version (e.g., "1.0.0") + protocolVersion: 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: + raise ValueError( + f"Missing required fields in GetStatusResponse: version={version}, " + f"protocolVersion={protocolVersion}" + ) + return GetStatusResponse(str(version), int(protocolVersion)) + + def to_dict(self) -> dict: + result: dict = {} + result["version"] = self.version + result["protocolVersion"] = self.protocolVersion + return result + + +@dataclass +class GetAuthStatusResponse: + """Response from auth.getStatus""" + + isAuthenticated: bool # Whether the user is authenticated + authType: str | None = None # Authentication type + host: str | None = None # GitHub host URL + login: str | None = None # User login name + statusMessage: str | None = None # Human-readable status message + + @staticmethod + def from_dict(obj: Any) -> GetAuthStatusResponse: + assert isinstance(obj, dict) + isAuthenticated = obj.get("isAuthenticated") + if isAuthenticated is None: + raise ValueError("Missing required field 'isAuthenticated' in GetAuthStatusResponse") + authType = obj.get("authType") + host = obj.get("host") + login = obj.get("login") + statusMessage = obj.get("statusMessage") + return GetAuthStatusResponse( + isAuthenticated=bool(isAuthenticated), + authType=authType, + host=host, + login=login, + statusMessage=statusMessage, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["isAuthenticated"] = self.isAuthenticated + if self.authType is not None: + result["authType"] = self.authType + if self.host is not None: + result["host"] = self.host + if self.login is not None: + result["login"] = self.login + if self.statusMessage is not None: + result["statusMessage"] = self.statusMessage + return result + + +# ============================================================================ +# Model Types +# ============================================================================ + + +@dataclass +class ModelVisionLimits: + """Vision-specific limits""" + + supported_media_types: list[str] | None = None + max_prompt_images: int | None = None + max_prompt_image_size: int | None = None + + @staticmethod + def from_dict(obj: Any) -> ModelVisionLimits: + assert isinstance(obj, dict) + supported_media_types = obj.get("supported_media_types") + max_prompt_images = obj.get("max_prompt_images") + max_prompt_image_size = obj.get("max_prompt_image_size") + return ModelVisionLimits( + supported_media_types=supported_media_types, + max_prompt_images=max_prompt_images, + max_prompt_image_size=max_prompt_image_size, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.supported_media_types is not None: + result["supported_media_types"] = self.supported_media_types + if self.max_prompt_images is not None: + result["max_prompt_images"] = self.max_prompt_images + if self.max_prompt_image_size is not None: + result["max_prompt_image_size"] = self.max_prompt_image_size + return result + + +@dataclass +class ModelLimits: + """Model limits""" + + max_prompt_tokens: int | None = None + max_context_window_tokens: int | None = None + vision: ModelVisionLimits | None = None + + @staticmethod + def from_dict(obj: Any) -> ModelLimits: + assert isinstance(obj, dict) + max_prompt_tokens = obj.get("max_prompt_tokens") + max_context_window_tokens = obj.get("max_context_window_tokens") + vision_dict = obj.get("vision") + vision = ModelVisionLimits.from_dict(vision_dict) if vision_dict else None + return ModelLimits( + max_prompt_tokens=max_prompt_tokens, + max_context_window_tokens=max_context_window_tokens, + vision=vision, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.max_prompt_tokens is not None: + result["max_prompt_tokens"] = self.max_prompt_tokens + if self.max_context_window_tokens is not None: + result["max_context_window_tokens"] = self.max_context_window_tokens + if self.vision is not None: + result["vision"] = self.vision.to_dict() + return result + + +@dataclass +class ModelSupports: + """Model support flags""" + + vision: bool + reasoning_effort: bool = False # Whether this model supports reasoning effort + + @staticmethod + def from_dict(obj: Any) -> ModelSupports: + assert isinstance(obj, dict) + vision = obj.get("vision") + if vision is None: + raise ValueError("Missing required field 'vision' in ModelSupports") + reasoning_effort = obj.get("reasoningEffort", False) + return ModelSupports(vision=bool(vision), reasoning_effort=bool(reasoning_effort)) + + def to_dict(self) -> dict: + result: dict = {} + result["vision"] = self.vision + result["reasoningEffort"] = self.reasoning_effort + return result + + +@dataclass +class ModelCapabilities: + """Model capabilities and limits""" + + supports: ModelSupports + limits: ModelLimits + + @staticmethod + def from_dict(obj: Any) -> ModelCapabilities: + assert isinstance(obj, dict) + supports_dict = obj.get("supports") + limits_dict = obj.get("limits") + if supports_dict is None or limits_dict is None: + raise ValueError( + f"Missing required fields in ModelCapabilities: supports={supports_dict}, " + f"limits={limits_dict}" + ) + supports = ModelSupports.from_dict(supports_dict) + limits = ModelLimits.from_dict(limits_dict) + return ModelCapabilities(supports=supports, limits=limits) + + def to_dict(self) -> dict: + result: dict = {} + result["supports"] = self.supports.to_dict() + result["limits"] = self.limits.to_dict() + return result + + +@dataclass +class ModelPolicy: + """Model policy state""" + + state: str # "enabled", "disabled", or "unconfigured" + terms: str + + @staticmethod + def from_dict(obj: Any) -> ModelPolicy: + assert isinstance(obj, dict) + state = obj.get("state") + terms = obj.get("terms") + if state is None or terms is None: + raise ValueError( + f"Missing required fields in ModelPolicy: state={state}, terms={terms}" + ) + return ModelPolicy(state=str(state), terms=str(terms)) + + def to_dict(self) -> dict: + result: dict = {} + result["state"] = self.state + result["terms"] = self.terms + return result + + +@dataclass +class ModelBilling: + """Model billing information""" + + multiplier: float + + @staticmethod + def from_dict(obj: Any) -> ModelBilling: + assert isinstance(obj, dict) + multiplier = obj.get("multiplier") + if multiplier is None: + raise ValueError("Missing required field 'multiplier' in ModelBilling") + return ModelBilling(multiplier=float(multiplier)) + + def to_dict(self) -> dict: + result: dict = {} + result["multiplier"] = self.multiplier + return result + + +@dataclass +class ModelInfo: + """Information about an available model""" + + id: str # Model identifier (e.g., "claude-sonnet-4.5") + name: str # Display name + capabilities: ModelCapabilities # Model capabilities and limits + policy: ModelPolicy | None = None # Policy state + billing: ModelBilling | None = None # Billing information + # Supported reasoning effort levels (only present if model supports reasoning effort) + supported_reasoning_efforts: list[str] | None = None + # Default reasoning effort level (only present if model supports reasoning effort) + default_reasoning_effort: str | None = None + + @staticmethod + def from_dict(obj: Any) -> ModelInfo: + assert isinstance(obj, dict) + id = obj.get("id") + name = obj.get("name") + capabilities_dict = obj.get("capabilities") + if id is None or name is None or capabilities_dict is None: + raise ValueError( + f"Missing required fields in ModelInfo: id={id}, name={name}, " + f"capabilities={capabilities_dict}" + ) + capabilities = ModelCapabilities.from_dict(capabilities_dict) + policy_dict = obj.get("policy") + policy = ModelPolicy.from_dict(policy_dict) if policy_dict else None + billing_dict = obj.get("billing") + billing = ModelBilling.from_dict(billing_dict) if billing_dict else None + supported_reasoning_efforts = obj.get("supportedReasoningEfforts") + default_reasoning_effort = obj.get("defaultReasoningEffort") + return ModelInfo( + id=str(id), + name=str(name), + capabilities=capabilities, + policy=policy, + billing=billing, + supported_reasoning_efforts=supported_reasoning_efforts, + default_reasoning_effort=default_reasoning_effort, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = self.id + result["name"] = self.name + result["capabilities"] = self.capabilities.to_dict() + if self.policy is not None: + result["policy"] = self.policy.to_dict() + if self.billing is not None: + result["billing"] = self.billing.to_dict() + if self.supported_reasoning_efforts is not None: + result["supportedReasoningEfforts"] = self.supported_reasoning_efforts + if self.default_reasoning_effort is not None: + result["defaultReasoningEffort"] = self.default_reasoning_effort + return result + + +# ============================================================================ +# Session Metadata Types +# ============================================================================ + + +@dataclass +class SessionContext: + """Working directory context for a session""" + + cwd: str # Working directory where the session was created + gitRoot: 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 + + @staticmethod + def from_dict(obj: Any) -> SessionContext: + assert isinstance(obj, dict) + cwd = obj.get("cwd") + if cwd is None: + raise ValueError("Missing required field 'cwd' in SessionContext") + return SessionContext( + cwd=str(cwd), + gitRoot=obj.get("gitRoot"), + repository=obj.get("repository"), + branch=obj.get("branch"), + ) + + def to_dict(self) -> dict: + result: dict = {"cwd": self.cwd} + if self.gitRoot is not None: + result["gitRoot"] = self.gitRoot + if self.repository is not None: + result["repository"] = self.repository + if self.branch is not None: + result["branch"] = self.branch + return result + + +@dataclass +class SessionListFilter: + """Filter options for listing sessions""" + + cwd: str | None = None # Filter by exact cwd match + gitRoot: str | None = None # Filter by git root + repository: str | None = None # Filter by repository (owner/repo format) + branch: str | None = None # Filter by branch + + def to_dict(self) -> dict: + result: dict = {} + if self.cwd is not None: + result["cwd"] = self.cwd + if self.gitRoot is not None: + result["gitRoot"] = self.gitRoot + if self.repository is not None: + result["repository"] = self.repository + if self.branch is not None: + result["branch"] = self.branch + return result + + +@dataclass +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 + 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: + raise ValueError( + f"Missing required fields in SessionMetadata: sessionId={sessionId}, " + f"startTime={startTime}, modifiedTime={modifiedTime}, isRemote={isRemote}" + ) + 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), + 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 + if self.summary is not None: + result["summary"] = self.summary + if self.context is not None: + result["context"] = self.context.to_dict() + return result + + +# ============================================================================ +# Session Lifecycle Types (for TUI+server mode) +# ============================================================================ + +SessionLifecycleEventType = Literal[ + "session.created", + "session.deleted", + "session.updated", + "session.foreground", + "session.background", +] + + +@dataclass +class SessionLifecycleEventMetadata: + """Metadata for session lifecycle events.""" + + startTime: str + modifiedTime: str + summary: str | None = None + + @staticmethod + def from_dict(data: dict) -> SessionLifecycleEventMetadata: + return SessionLifecycleEventMetadata( + startTime=data.get("startTime", ""), + modifiedTime=data.get("modifiedTime", ""), + summary=data.get("summary"), + ) + + +@dataclass +class SessionLifecycleEvent: + """Session lifecycle event notification.""" + + type: SessionLifecycleEventType + sessionId: 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, + ) + + +SessionLifecycleHandler = Callable[[SessionLifecycleEvent], None] HandlerUnsubcribe = Callable[[], None] @@ -868,7 +1453,7 @@ def get_state(self) -> ConnectionState: """ return self._state - async def ping(self, message: str | None = None) -> "PingResponse": + async def ping(self, message: str | None = None) -> PingResponse: """ Send a ping request to the server to verify connectivity. @@ -891,7 +1476,7 @@ async def ping(self, message: str | None = None) -> "PingResponse": result = await self._client.request("ping", {"message": message}) return PingResponse.from_dict(result) - async def get_status(self) -> "GetStatusResponse": + async def get_status(self) -> GetStatusResponse: """ Get CLI status including version and protocol information. @@ -911,7 +1496,7 @@ async def get_status(self) -> "GetStatusResponse": result = await self._client.request("status.get", {}) return GetStatusResponse.from_dict(result) - async def get_auth_status(self) -> "GetAuthStatusResponse": + async def get_auth_status(self) -> GetAuthStatusResponse: """ Get current authentication status. @@ -932,7 +1517,7 @@ async def get_auth_status(self) -> "GetAuthStatusResponse": result = await self._client.request("auth.getStatus", {}) return GetAuthStatusResponse.from_dict(result) - async def list_models(self) -> list["ModelInfo"]: + async def list_models(self) -> list[ModelInfo]: """ List available models with their metadata. @@ -982,9 +1567,7 @@ async def list_models(self) -> list["ModelInfo"]: return list(models) # Return a copy to prevent cache mutation - async def list_sessions( - self, filter: "SessionListFilter | None" = None - ) -> list["SessionMetadata"]: + async def list_sessions(self, filter: SessionListFilter | None = None) -> list[SessionMetadata]: """ List all available sessions known to the server. @@ -1005,7 +1588,7 @@ async def list_sessions( >>> for session in sessions: ... print(f"Session: {session.sessionId}") >>> # Filter sessions by repository - >>> from copilot import SessionListFilter + >>> from copilot.client import SessionListFilter >>> filtered = await client.list_sessions(SessionListFilter(repository="owner/repo")) """ if not self._client: diff --git a/python/copilot/session.py b/python/copilot/session.py index 7a8b9f05d..602c31cb7 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -2,14 +2,18 @@ Copilot Session - represents a single conversation session with the Copilot CLI. This module provides the CopilotSession class for managing individual -conversation sessions with the Copilot CLI. +conversation sessions with the Copilot CLI, along with all session-related +configuration and handler types. """ +from __future__ import annotations + import asyncio import inspect import threading -from collections.abc import Callable -from typing import Any, Literal, cast +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any, Literal, NotRequired, TypedDict, cast from ._jsonrpc import JsonRpcError, ProcessExitedError from ._telemetry import get_trace_context, trace_context @@ -24,24 +28,532 @@ SessionRpc, SessionToolsHandlePendingToolCallParams, ) -from .generated.session_events import SessionEvent, SessionEventType, session_event_from_dict -from .types import ( - Attachment, +from .generated.session_events import ( PermissionRequest, - PermissionRequestResult, - SessionHooks, - Tool, - ToolHandler, - ToolInvocation, - ToolResult, - UserInputHandler, - UserInputRequest, - UserInputResponse, - _PermissionHandlerFn, -) -from .types import ( - SessionEvent as SessionEventTypeAlias, + SessionEvent, + SessionEventType, + session_event_from_dict, ) +from .tools import Tool, ToolHandler, ToolInvocation, ToolResult + +# Re-export SessionEvent under an alias used internally +SessionEventTypeAlias = SessionEvent + +# ============================================================================ +# Reasoning Effort +# ============================================================================ + +ReasoningEffort = Literal["low", "medium", "high", "xhigh"] + +# ============================================================================ +# Attachment Types +# ============================================================================ + + +class SelectionRange(TypedDict): + line: int + character: int + + +class Selection(TypedDict): + start: SelectionRange + end: SelectionRange + + +class FileAttachment(TypedDict): + """File attachment.""" + + type: Literal["file"] + path: str + displayName: NotRequired[str] + + +class DirectoryAttachment(TypedDict): + """Directory attachment.""" + + type: Literal["directory"] + path: str + displayName: NotRequired[str] + + +class SelectionAttachment(TypedDict): + """Selection attachment with text from a file.""" + + type: Literal["selection"] + filePath: str + displayName: str + selection: NotRequired[Selection] + text: NotRequired[str] + + +class BlobAttachment(TypedDict): + """Inline base64-encoded content attachment (e.g. images).""" + + type: Literal["blob"] + data: str + """Base64-encoded content""" + mimeType: str + """MIME type of the inline data""" + displayName: NotRequired[str] + + +Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment | BlobAttachment + +# ============================================================================ +# System Message Configuration +# ============================================================================ + + +class SystemMessageAppendConfig(TypedDict, total=False): + """ + Append mode: Use CLI foundation with optional appended content. + """ + + mode: NotRequired[Literal["append"]] + content: NotRequired[str] + + +class SystemMessageReplaceConfig(TypedDict): + """ + Replace mode: Use caller-provided system message entirely. + Removes all SDK guardrails including security restrictions. + """ + + mode: Literal["replace"] + content: str + + +SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig + +# ============================================================================ +# Permission Types +# ============================================================================ + +PermissionRequestResultKind = Literal[ + "approved", + "denied-by-rules", + "denied-by-content-exclusion-policy", + "denied-no-approval-rule-and-could-not-request-from-user", + "denied-interactively-by-user", + "no-result", +] + + +@dataclass +class PermissionRequestResult: + """Result of a permission request.""" + + kind: PermissionRequestResultKind = "denied-no-approval-rule-and-could-not-request-from-user" + rules: list[Any] | None = None + feedback: str | None = None + message: str | None = None + path: str | None = None + + +_PermissionHandlerFn = Callable[ + [PermissionRequest, dict[str, str]], + PermissionRequestResult | Awaitable[PermissionRequestResult], +] + + +class PermissionHandler: + @staticmethod + def approve_all( + request: PermissionRequest, invocation: dict[str, str] + ) -> PermissionRequestResult: + return PermissionRequestResult(kind="approved") + + +# ============================================================================ +# User Input Request Types +# ============================================================================ + + +class UserInputRequest(TypedDict, total=False): + """Request for user input from the agent (enables ask_user tool)""" + + question: str + choices: list[str] + allowFreeform: bool + + +class UserInputResponse(TypedDict): + """Response to a user input request""" + + answer: str + wasFreeform: bool + + +UserInputHandler = Callable[ + [UserInputRequest, dict[str, str]], + UserInputResponse | Awaitable[UserInputResponse], +] + +# ============================================================================ +# Hook Types +# ============================================================================ + + +class BaseHookInput(TypedDict): + """Base interface for all hook inputs""" + + timestamp: int + cwd: str + + +class PreToolUseHookInput(TypedDict): + """Input for pre-tool-use hook""" + + timestamp: int + cwd: str + toolName: str + toolArgs: Any + + +class PreToolUseHookOutput(TypedDict, total=False): + """Output for pre-tool-use hook""" + + permissionDecision: Literal["allow", "deny", "ask"] + permissionDecisionReason: str + modifiedArgs: Any + additionalContext: str + suppressOutput: bool + + +PreToolUseHandler = Callable[ + [PreToolUseHookInput, dict[str, str]], + PreToolUseHookOutput | None | Awaitable[PreToolUseHookOutput | None], +] + + +class PostToolUseHookInput(TypedDict): + """Input for post-tool-use hook""" + + timestamp: int + cwd: str + toolName: str + toolArgs: Any + toolResult: Any + + +class PostToolUseHookOutput(TypedDict, total=False): + """Output for post-tool-use hook""" + + modifiedResult: Any + additionalContext: str + suppressOutput: bool + + +PostToolUseHandler = Callable[ + [PostToolUseHookInput, dict[str, str]], + PostToolUseHookOutput | None | Awaitable[PostToolUseHookOutput | None], +] + + +class UserPromptSubmittedHookInput(TypedDict): + """Input for user-prompt-submitted hook""" + + timestamp: int + cwd: str + prompt: str + + +class UserPromptSubmittedHookOutput(TypedDict, total=False): + """Output for user-prompt-submitted hook""" + + modifiedPrompt: str + additionalContext: str + suppressOutput: bool + + +UserPromptSubmittedHandler = Callable[ + [UserPromptSubmittedHookInput, dict[str, str]], + UserPromptSubmittedHookOutput | None | Awaitable[UserPromptSubmittedHookOutput | None], +] + + +class SessionStartHookInput(TypedDict): + """Input for session-start hook""" + + timestamp: int + cwd: str + source: Literal["startup", "resume", "new"] + initialPrompt: NotRequired[str] + + +class SessionStartHookOutput(TypedDict, total=False): + """Output for session-start hook""" + + additionalContext: str + modifiedConfig: dict[str, Any] + + +SessionStartHandler = Callable[ + [SessionStartHookInput, dict[str, str]], + SessionStartHookOutput | None | Awaitable[SessionStartHookOutput | None], +] + + +class SessionEndHookInput(TypedDict): + """Input for session-end hook""" + + timestamp: int + cwd: str + reason: Literal["complete", "error", "abort", "timeout", "user_exit"] + finalMessage: NotRequired[str] + error: NotRequired[str] + + +class SessionEndHookOutput(TypedDict, total=False): + """Output for session-end hook""" + + suppressOutput: bool + cleanupActions: list[str] + sessionSummary: str + + +SessionEndHandler = Callable[ + [SessionEndHookInput, dict[str, str]], + SessionEndHookOutput | None | Awaitable[SessionEndHookOutput | None], +] + + +class ErrorOccurredHookInput(TypedDict): + """Input for error-occurred hook""" + + timestamp: int + cwd: str + error: str + errorContext: Literal["model_call", "tool_execution", "system", "user_input"] + recoverable: bool + + +class ErrorOccurredHookOutput(TypedDict, total=False): + """Output for error-occurred hook""" + + suppressOutput: bool + errorHandling: Literal["retry", "skip", "abort"] + retryCount: int + userNotification: str + + +ErrorOccurredHandler = Callable[ + [ErrorOccurredHookInput, dict[str, str]], + ErrorOccurredHookOutput | None | Awaitable[ErrorOccurredHookOutput | None], +] + + +class SessionHooks(TypedDict, total=False): + """Configuration for session hooks""" + + on_pre_tool_use: PreToolUseHandler + on_post_tool_use: PostToolUseHandler + on_user_prompt_submitted: UserPromptSubmittedHandler + on_session_start: SessionStartHandler + on_session_end: SessionEndHandler + on_error_occurred: ErrorOccurredHandler + + +# ============================================================================ +# MCP Server Configuration Types +# ============================================================================ + + +class MCPLocalServerConfig(TypedDict, total=False): + """Configuration for a local/stdio MCP server.""" + + tools: list[str] # List of tools to include. [] means none. "*" means all. + type: NotRequired[Literal["local", "stdio"]] # Server type + timeout: NotRequired[int] # Timeout in milliseconds + command: str # Command to run + args: list[str] # Command arguments + env: NotRequired[dict[str, str]] # Environment variables + cwd: NotRequired[str] # Working directory + + +class MCPRemoteServerConfig(TypedDict, total=False): + """Configuration for a remote MCP server (HTTP or SSE).""" + + tools: list[str] # List of tools to include. [] means none. "*" means all. + type: Literal["http", "sse"] # Server type + timeout: NotRequired[int] # Timeout in milliseconds + url: str # URL of the remote server + headers: NotRequired[dict[str, str]] # HTTP headers + + +MCPServerConfig = MCPLocalServerConfig | MCPRemoteServerConfig + +# ============================================================================ +# Custom Agent Configuration Types +# ============================================================================ + + +class CustomAgentConfig(TypedDict, total=False): + """Configuration for a custom agent.""" + + name: str # Unique name of the custom agent + display_name: NotRequired[str] # Display name for UI purposes + description: NotRequired[str] # Description of what the agent does + # List of tool names the agent can use + tools: NotRequired[list[str] | None] + prompt: str # The prompt content for the agent + # MCP servers specific to agent + mcp_servers: NotRequired[dict[str, MCPServerConfig]] + infer: NotRequired[bool] # Whether agent is available for model inference + + +class InfiniteSessionConfig(TypedDict, total=False): + """ + Configuration for infinite sessions with automatic context compaction + and workspace persistence. + + When enabled, sessions automatically manage context window limits through + background compaction and persist state to a workspace directory. + """ + + # Whether infinite sessions are enabled (default: True) + enabled: bool + # Context utilization threshold (0.0-1.0) at which background compaction starts. + # Compaction runs asynchronously, allowing the session to continue processing. + # Default: 0.80 + background_compaction_threshold: float + # Context utilization threshold (0.0-1.0) at which the session blocks until + # compaction completes. This prevents context overflow when compaction hasn't + # finished in time. Default: 0.95 + buffer_exhaustion_threshold: float + + +# ============================================================================ +# Session Configuration +# ============================================================================ + + +class AzureProviderOptions(TypedDict, total=False): + """Azure-specific provider configuration""" + + api_version: str # Azure API version. Defaults to "2024-10-21". + + +class ProviderConfig(TypedDict, total=False): + """Configuration for a custom API provider""" + + type: Literal["openai", "azure", "anthropic"] + wire_api: Literal["completions", "responses"] + base_url: str + api_key: str + # Bearer token for authentication. Sets the Authorization header directly. + # Use this for services requiring bearer token auth instead of API key. + # Takes precedence over api_key when both are set. + bearer_token: str + azure: AzureProviderOptions # Azure-specific options + + +class SessionConfig(TypedDict, total=False): + """Configuration for creating a session""" + + session_id: str # Optional custom session ID + # Client name to identify the application using the SDK. + # Included in the User-Agent header for API requests. + client_name: str + model: str # Model to use for this session. Use client.list_models() to see available models. + # Reasoning effort level for models that support it. + # Only valid for models where capabilities.supports.reasoning_effort is True. + reasoning_effort: ReasoningEffort + tools: list[Tool] + system_message: SystemMessageConfig # System message configuration + # List of tool names to allow (takes precedence over excluded_tools) + available_tools: list[str] + # List of tool names to disable (ignored if available_tools is set) + excluded_tools: list[str] + # Handler for permission requests from the server + on_permission_request: _PermissionHandlerFn + # Handler for user input requests from the agent (enables ask_user tool) + on_user_input_request: UserInputHandler + # Hook handlers for intercepting session lifecycle events + hooks: SessionHooks + # Working directory for the session. Tool operations will be relative to this directory. + working_directory: str + # Custom provider configuration (BYOK - Bring Your Own Key) + provider: ProviderConfig + # Enable streaming of assistant message and reasoning chunks + # When True, assistant.message_delta and assistant.reasoning_delta events + # with delta_content are sent as the response is generated + streaming: bool + # MCP server configurations for the session + mcp_servers: dict[str, MCPServerConfig] + # Custom agent configurations for the session + custom_agents: list[CustomAgentConfig] + # 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] + # 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] + + +class ResumeSessionConfig(TypedDict, total=False): + """Configuration for resuming a session""" + + # Client name to identify the application using the SDK. + # Included in the User-Agent header for API requests. + client_name: str + # Model to use for this session. Can change the model when resuming. + model: str + tools: list[Tool] + system_message: SystemMessageConfig # System message configuration + # List of tool names to allow (takes precedence over excluded_tools) + available_tools: list[str] + # List of tool names to disable (ignored if available_tools is set) + excluded_tools: list[str] + provider: ProviderConfig + # Reasoning effort level for models that support it. + reasoning_effort: ReasoningEffort + on_permission_request: _PermissionHandlerFn + # Handler for user input requestsfrom the agent (enables ask_user tool) + on_user_input_request: UserInputHandler + # Hook handlers for intercepting session lifecycle events + hooks: SessionHooks + # Working directory for the session. Tool operations will be relative to this directory. + working_directory: str + # Override the default configuration directory location. + config_dir: str + # Enable streaming of assistant message chunks + streaming: bool + # MCP server configurations for the session + mcp_servers: dict[str, MCPServerConfig] + # Custom agent configurations for the session + custom_agents: list[CustomAgentConfig] + # Name of the custom agent to activate when the session starts. + # Must match the name of one of the agents in custom_agents. + agent: str + # Directories to load skills from + skill_directories: list[str] + # List of skill names to disable + disabled_skills: list[str] + # Infinite session configuration for persistent workspaces and automatic compaction. + infinite_sessions: InfiniteSessionConfig + # When True, skips emitting the session.resume event. + # Useful for reconnecting to a session without triggering resume-related side effects. + disable_resume: bool + # Optional event handler registered before the session.resume RPC is issued, + # ensuring early events are delivered. See SessionConfig.on_event. + on_event: Callable[[SessionEvent], None] + + +SessionEventHandler = Callable[[SessionEvent], None] class CopilotSession: @@ -706,7 +1218,7 @@ async def destroy(self) -> None: ) await self.disconnect() - async def __aenter__(self) -> "CopilotSession": + 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 58e58d97e..f559cfefe 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -9,12 +9,59 @@ import inspect import json -from collections.abc import Callable -from typing import Any, TypeVar, get_type_hints, overload +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any, Literal, TypeVar, get_type_hints, overload from pydantic import BaseModel -from .types import Tool, ToolInvocation, ToolResult +ToolResultType = Literal["success", "failure", "rejected", "denied"] + + +@dataclass +class ToolBinaryResult: + """Binary content returned by a tool.""" + + data: str = "" + mime_type: str = "" + type: str = "" + description: str = "" + + +@dataclass +class ToolResult: + """Result of a tool invocation.""" + + text_result_for_llm: str = "" + result_type: ToolResultType = "success" + error: str | None = None + binary_results_for_llm: list[ToolBinaryResult] | None = None + session_log: str | None = None + tool_telemetry: dict[str, Any] | None = None + + +@dataclass +class ToolInvocation: + """Context passed to a tool handler when invoked.""" + + session_id: str = "" + tool_call_id: str = "" + tool_name: str = "" + arguments: Any = None + + +ToolHandler = Callable[[ToolInvocation], ToolResult | Awaitable[ToolResult]] + + +@dataclass +class Tool: + name: str + description: str + handler: ToolHandler + parameters: dict[str, Any] | None = None + overrides_built_in_tool: bool = False + skip_permission: bool = False + T = TypeVar("T", bound=BaseModel) R = TypeVar("R") diff --git a/python/copilot/types.py b/python/copilot/types.py deleted file mode 100644 index 17be065bc..000000000 --- a/python/copilot/types.py +++ /dev/null @@ -1,1060 +0,0 @@ -""" -Type definitions for the Copilot SDK -""" - -from __future__ import annotations - -from collections.abc import Awaitable, Callable -from dataclasses import KW_ONLY, dataclass, field -from typing import Any, Literal, NotRequired, TypedDict - -# Import generated SessionEvent types -from .generated.session_events import ( - PermissionRequest, - SessionEvent, -) - -# SessionEvent is now imported from generated types -# It provides proper type discrimination for all event types - -# Valid reasoning effort levels for models that support it -ReasoningEffort = Literal["low", "medium", "high", "xhigh"] - -# Connection state -ConnectionState = Literal["disconnected", "connecting", "connected", "error"] - -# Log level type -LogLevel = Literal["none", "error", "warning", "info", "debug", "all"] - - -# Selection range for text attachments -class SelectionRange(TypedDict): - line: int - character: int - - -class Selection(TypedDict): - start: SelectionRange - end: SelectionRange - - -# Attachment types - discriminated union based on 'type' field -class FileAttachment(TypedDict): - """File attachment.""" - - type: Literal["file"] - path: str - displayName: NotRequired[str] - - -class DirectoryAttachment(TypedDict): - """Directory attachment.""" - - type: Literal["directory"] - path: str - displayName: NotRequired[str] - - -class SelectionAttachment(TypedDict): - """Selection attachment with text from a file.""" - - type: Literal["selection"] - filePath: str - displayName: str - selection: NotRequired[Selection] - text: NotRequired[str] - - -class BlobAttachment(TypedDict): - """Inline base64-encoded content attachment (e.g. images).""" - - type: Literal["blob"] - data: str - """Base64-encoded content""" - mimeType: str - """MIME type of the inline data""" - displayName: NotRequired[str] - - -# Attachment type - union of all attachment types -Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment | BlobAttachment - - -# Configuration for OpenTelemetry integration with the Copilot CLI. -class TelemetryConfig(TypedDict, total=False): - """Configuration for OpenTelemetry integration with the Copilot CLI.""" - - otlp_endpoint: str - """OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT.""" - file_path: str - """File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH.""" - exporter_type: str - """Exporter backend type: "otlp-http" or "file". Sets COPILOT_OTEL_EXPORTER_TYPE.""" - source_name: str - """Instrumentation scope name. Sets COPILOT_OTEL_SOURCE_NAME.""" - capture_content: bool - """Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501 - - -# Configuration for CopilotClient connection modes - - -@dataclass -class SubprocessConfig: - """Config for spawning a local Copilot CLI subprocess. - - 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", - ... ) - """ - - cli_path: str | None = None - """Path to the Copilot CLI executable. ``None`` uses the bundled binary.""" - - cli_args: list[str] = field(default_factory=list) - """Extra arguments passed to the CLI executable (inserted before SDK-managed args).""" - - _: KW_ONLY - - cwd: str | None = None - """Working directory for the CLI process. ``None`` uses the current directory.""" - - use_stdio: bool = True - """Use stdio transport (``True``, default) or TCP (``False``).""" - - 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.""" - - env: dict[str, str] | None = None - """Environment variables for the CLI process. ``None`` inherits the current env.""" - - github_token: str | None = None - """GitHub token for authentication. Takes priority over other auth methods.""" - - use_logged_in_user: bool | None = None - """Use the logged-in user for authentication. - - ``None`` (default) resolves to ``True`` unless ``github_token`` is set. - """ - - telemetry: TelemetryConfig | None = None - """OpenTelemetry configuration. Providing this enables telemetry — no separate flag needed.""" - - -@dataclass -class ExternalServerConfig: - """Config for connecting to an existing Copilot CLI server over TCP. - - Example: - >>> config = ExternalServerConfig(url="localhost:3000") - >>> client = CopilotClient(config) - """ - - url: str - """Server URL. Supports ``"host:port"``, ``"http://host:port"``, or just ``"port"``.""" - - -ToolResultType = Literal["success", "failure", "rejected", "denied"] - - -@dataclass -class ToolBinaryResult: - """Binary content returned by a tool.""" - - data: str = "" - mime_type: str = "" - type: str = "" - description: str = "" - - -@dataclass -class ToolResult: - """Result of a tool invocation.""" - - text_result_for_llm: str = "" - result_type: ToolResultType = "success" - error: str | None = None - binary_results_for_llm: list[ToolBinaryResult] | None = None - session_log: str | None = None - tool_telemetry: dict[str, Any] | None = None - - -@dataclass -class ToolInvocation: - """Context passed to a tool handler when invoked.""" - - session_id: str = "" - tool_call_id: str = "" - tool_name: str = "" - arguments: Any = None - - -ToolHandler = Callable[[ToolInvocation], ToolResult | Awaitable[ToolResult]] - - -@dataclass -class Tool: - name: str - description: str - handler: ToolHandler - parameters: dict[str, Any] | None = None - overrides_built_in_tool: bool = False - skip_permission: bool = False - - -# System message configuration (discriminated union) -# Use SystemMessageAppendConfig for default behavior, SystemMessageReplaceConfig for full control - - -class SystemMessageAppendConfig(TypedDict, total=False): - """ - Append mode: Use CLI foundation with optional appended content. - """ - - mode: NotRequired[Literal["append"]] - content: NotRequired[str] - - -class SystemMessageReplaceConfig(TypedDict): - """ - Replace mode: Use caller-provided system message entirely. - Removes all SDK guardrails including security restrictions. - """ - - mode: Literal["replace"] - content: str - - -# Union type - use one or the other -SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig - - -# Permission result types - -PermissionRequestResultKind = Literal[ - "approved", - "denied-by-rules", - "denied-by-content-exclusion-policy", - "denied-no-approval-rule-and-could-not-request-from-user", - "denied-interactively-by-user", - "no-result", -] - - -@dataclass -class PermissionRequestResult: - """Result of a permission request.""" - - kind: PermissionRequestResultKind = "denied-no-approval-rule-and-could-not-request-from-user" - rules: list[Any] | None = None - feedback: str | None = None - message: str | None = None - path: str | None = None - - -_PermissionHandlerFn = Callable[ - [PermissionRequest, dict[str, str]], - PermissionRequestResult | Awaitable[PermissionRequestResult], -] - - -class PermissionHandler: - @staticmethod - def approve_all( - request: PermissionRequest, invocation: dict[str, str] - ) -> PermissionRequestResult: - return PermissionRequestResult(kind="approved") - - -# ============================================================================ -# User Input Request Types -# ============================================================================ - - -class UserInputRequest(TypedDict, total=False): - """Request for user input from the agent (enables ask_user tool)""" - - question: str - choices: list[str] - allowFreeform: bool - - -class UserInputResponse(TypedDict): - """Response to a user input request""" - - answer: str - wasFreeform: bool - - -UserInputHandler = Callable[ - [UserInputRequest, dict[str, str]], - UserInputResponse | Awaitable[UserInputResponse], -] - - -# ============================================================================ -# Hook Types -# ============================================================================ - - -class BaseHookInput(TypedDict): - """Base interface for all hook inputs""" - - timestamp: int - cwd: str - - -class PreToolUseHookInput(TypedDict): - """Input for pre-tool-use hook""" - - timestamp: int - cwd: str - toolName: str - toolArgs: Any - - -class PreToolUseHookOutput(TypedDict, total=False): - """Output for pre-tool-use hook""" - - permissionDecision: Literal["allow", "deny", "ask"] - permissionDecisionReason: str - modifiedArgs: Any - additionalContext: str - suppressOutput: bool - - -PreToolUseHandler = Callable[ - [PreToolUseHookInput, dict[str, str]], - PreToolUseHookOutput | None | Awaitable[PreToolUseHookOutput | None], -] - - -class PostToolUseHookInput(TypedDict): - """Input for post-tool-use hook""" - - timestamp: int - cwd: str - toolName: str - toolArgs: Any - toolResult: Any - - -class PostToolUseHookOutput(TypedDict, total=False): - """Output for post-tool-use hook""" - - modifiedResult: Any - additionalContext: str - suppressOutput: bool - - -PostToolUseHandler = Callable[ - [PostToolUseHookInput, dict[str, str]], - PostToolUseHookOutput | None | Awaitable[PostToolUseHookOutput | None], -] - - -class UserPromptSubmittedHookInput(TypedDict): - """Input for user-prompt-submitted hook""" - - timestamp: int - cwd: str - prompt: str - - -class UserPromptSubmittedHookOutput(TypedDict, total=False): - """Output for user-prompt-submitted hook""" - - modifiedPrompt: str - additionalContext: str - suppressOutput: bool - - -UserPromptSubmittedHandler = Callable[ - [UserPromptSubmittedHookInput, dict[str, str]], - UserPromptSubmittedHookOutput | None | Awaitable[UserPromptSubmittedHookOutput | None], -] - - -class SessionStartHookInput(TypedDict): - """Input for session-start hook""" - - timestamp: int - cwd: str - source: Literal["startup", "resume", "new"] - initialPrompt: NotRequired[str] - - -class SessionStartHookOutput(TypedDict, total=False): - """Output for session-start hook""" - - additionalContext: str - modifiedConfig: dict[str, Any] - - -SessionStartHandler = Callable[ - [SessionStartHookInput, dict[str, str]], - SessionStartHookOutput | None | Awaitable[SessionStartHookOutput | None], -] - - -class SessionEndHookInput(TypedDict): - """Input for session-end hook""" - - timestamp: int - cwd: str - reason: Literal["complete", "error", "abort", "timeout", "user_exit"] - finalMessage: NotRequired[str] - error: NotRequired[str] - - -class SessionEndHookOutput(TypedDict, total=False): - """Output for session-end hook""" - - suppressOutput: bool - cleanupActions: list[str] - sessionSummary: str - - -SessionEndHandler = Callable[ - [SessionEndHookInput, dict[str, str]], - SessionEndHookOutput | None | Awaitable[SessionEndHookOutput | None], -] - - -class ErrorOccurredHookInput(TypedDict): - """Input for error-occurred hook""" - - timestamp: int - cwd: str - error: str - errorContext: Literal["model_call", "tool_execution", "system", "user_input"] - recoverable: bool - - -class ErrorOccurredHookOutput(TypedDict, total=False): - """Output for error-occurred hook""" - - suppressOutput: bool - errorHandling: Literal["retry", "skip", "abort"] - retryCount: int - userNotification: str - - -ErrorOccurredHandler = Callable[ - [ErrorOccurredHookInput, dict[str, str]], - ErrorOccurredHookOutput | None | Awaitable[ErrorOccurredHookOutput | None], -] - - -class SessionHooks(TypedDict, total=False): - """Configuration for session hooks""" - - on_pre_tool_use: PreToolUseHandler - on_post_tool_use: PostToolUseHandler - on_user_prompt_submitted: UserPromptSubmittedHandler - on_session_start: SessionStartHandler - on_session_end: SessionEndHandler - on_error_occurred: ErrorOccurredHandler - - -# ============================================================================ -# MCP Server Configuration Types -# ============================================================================ - - -class MCPLocalServerConfig(TypedDict, total=False): - """Configuration for a local/stdio MCP server.""" - - tools: list[str] # List of tools to include. [] means none. "*" means all. - type: NotRequired[Literal["local", "stdio"]] # Server type - timeout: NotRequired[int] # Timeout in milliseconds - command: str # Command to run - args: list[str] # Command arguments - env: NotRequired[dict[str, str]] # Environment variables - cwd: NotRequired[str] # Working directory - - -class MCPRemoteServerConfig(TypedDict, total=False): - """Configuration for a remote MCP server (HTTP or SSE).""" - - tools: list[str] # List of tools to include. [] means none. "*" means all. - type: Literal["http", "sse"] # Server type - timeout: NotRequired[int] # Timeout in milliseconds - url: str # URL of the remote server - headers: NotRequired[dict[str, str]] # HTTP headers - - -MCPServerConfig = MCPLocalServerConfig | MCPRemoteServerConfig - - -# ============================================================================ -# Custom Agent Configuration Types -# ============================================================================ - - -class CustomAgentConfig(TypedDict, total=False): - """Configuration for a custom agent.""" - - name: str # Unique name of the custom agent - display_name: NotRequired[str] # Display name for UI purposes - description: NotRequired[str] # Description of what the agent does - # List of tool names the agent can use - tools: NotRequired[list[str] | None] - prompt: str # The prompt content for the agent - # MCP servers specific to agent - mcp_servers: NotRequired[dict[str, MCPServerConfig]] - infer: NotRequired[bool] # Whether agent is available for model inference - - -class InfiniteSessionConfig(TypedDict, total=False): - """ - Configuration for infinite sessions with automatic context compaction - and workspace persistence. - - When enabled, sessions automatically manage context window limits through - background compaction and persist state to a workspace directory. - """ - - # Whether infinite sessions are enabled (default: True) - enabled: bool - # Context utilization threshold (0.0-1.0) at which background compaction starts. - # Compaction runs asynchronously, allowing the session to continue processing. - # Default: 0.80 - background_compaction_threshold: float - # Context utilization threshold (0.0-1.0) at which the session blocks until - # compaction completes. This prevents context overflow when compaction hasn't - # finished in time. Default: 0.95 - buffer_exhaustion_threshold: float - - -# Azure-specific provider options -class AzureProviderOptions(TypedDict, total=False): - """Azure-specific provider configuration""" - - api_version: str # Azure API version. Defaults to "2024-10-21". - - -# Configuration for a custom API provider -class ProviderConfig(TypedDict, total=False): - """Configuration for a custom API provider""" - - type: Literal["openai", "azure", "anthropic"] - wire_api: Literal["completions", "responses"] - base_url: str - api_key: str - # Bearer token for authentication. Sets the Authorization header directly. - # Use this for services requiring bearer token auth instead of API key. - # Takes precedence over api_key when both are set. - bearer_token: str - azure: AzureProviderOptions # Azure-specific options - - -# Event handler type -SessionEventHandler = Callable[[SessionEvent], None] - - -# Response from ping -@dataclass -class PingResponse: - """Response from ping""" - - message: str # Echo message with "pong: " prefix - timestamp: int # Server timestamp in milliseconds - protocolVersion: 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: - raise ValueError( - f"Missing required fields in PingResponse: message={message}, " - f"timestamp={timestamp}, protocolVersion={protocolVersion}" - ) - return PingResponse(str(message), int(timestamp), int(protocolVersion)) - - def to_dict(self) -> dict: - result: dict = {} - result["message"] = self.message - result["timestamp"] = self.timestamp - result["protocolVersion"] = self.protocolVersion - return result - - -# Error information from client stop -@dataclass -class StopError(Exception): - """Error that occurred during client stop cleanup.""" - - message: str # Error message describing what failed during cleanup - - def __post_init__(self) -> None: - Exception.__init__(self, self.message) - - @staticmethod - def from_dict(obj: Any) -> StopError: - assert isinstance(obj, dict) - message = obj.get("message") - if message is None: - raise ValueError("Missing required field 'message' in StopError") - return StopError(str(message)) - - def to_dict(self) -> dict: - result: dict = {} - result["message"] = self.message - return result - - -# Response from status.get -@dataclass -class GetStatusResponse: - """Response from status.get""" - - version: str # Package version (e.g., "1.0.0") - protocolVersion: 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: - raise ValueError( - f"Missing required fields in GetStatusResponse: version={version}, " - f"protocolVersion={protocolVersion}" - ) - return GetStatusResponse(str(version), int(protocolVersion)) - - def to_dict(self) -> dict: - result: dict = {} - result["version"] = self.version - result["protocolVersion"] = self.protocolVersion - return result - - -# Response from auth.getStatus -@dataclass -class GetAuthStatusResponse: - """Response from auth.getStatus""" - - isAuthenticated: bool # Whether the user is authenticated - authType: str | None = None # Authentication type - host: str | None = None # GitHub host URL - login: str | None = None # User login name - statusMessage: str | None = None # Human-readable status message - - @staticmethod - def from_dict(obj: Any) -> GetAuthStatusResponse: - assert isinstance(obj, dict) - isAuthenticated = obj.get("isAuthenticated") - if isAuthenticated is None: - raise ValueError("Missing required field 'isAuthenticated' in GetAuthStatusResponse") - authType = obj.get("authType") - host = obj.get("host") - login = obj.get("login") - statusMessage = obj.get("statusMessage") - return GetAuthStatusResponse( - isAuthenticated=bool(isAuthenticated), - authType=authType, - host=host, - login=login, - statusMessage=statusMessage, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["isAuthenticated"] = self.isAuthenticated - if self.authType is not None: - result["authType"] = self.authType - if self.host is not None: - result["host"] = self.host - if self.login is not None: - result["login"] = self.login - if self.statusMessage is not None: - result["statusMessage"] = self.statusMessage - return result - - -# Model capabilities -@dataclass -class ModelVisionLimits: - """Vision-specific limits""" - - supported_media_types: list[str] | None = None - max_prompt_images: int | None = None - max_prompt_image_size: int | None = None - - @staticmethod - def from_dict(obj: Any) -> ModelVisionLimits: - assert isinstance(obj, dict) - supported_media_types = obj.get("supported_media_types") - max_prompt_images = obj.get("max_prompt_images") - max_prompt_image_size = obj.get("max_prompt_image_size") - return ModelVisionLimits( - supported_media_types=supported_media_types, - max_prompt_images=max_prompt_images, - max_prompt_image_size=max_prompt_image_size, - ) - - def to_dict(self) -> dict: - result: dict = {} - if self.supported_media_types is not None: - result["supported_media_types"] = self.supported_media_types - if self.max_prompt_images is not None: - result["max_prompt_images"] = self.max_prompt_images - if self.max_prompt_image_size is not None: - result["max_prompt_image_size"] = self.max_prompt_image_size - return result - - -@dataclass -class ModelLimits: - """Model limits""" - - max_prompt_tokens: int | None = None - max_context_window_tokens: int | None = None - vision: ModelVisionLimits | None = None - - @staticmethod - def from_dict(obj: Any) -> ModelLimits: - assert isinstance(obj, dict) - max_prompt_tokens = obj.get("max_prompt_tokens") - max_context_window_tokens = obj.get("max_context_window_tokens") - vision_dict = obj.get("vision") - vision = ModelVisionLimits.from_dict(vision_dict) if vision_dict else None - return ModelLimits( - max_prompt_tokens=max_prompt_tokens, - max_context_window_tokens=max_context_window_tokens, - vision=vision, - ) - - def to_dict(self) -> dict: - result: dict = {} - if self.max_prompt_tokens is not None: - result["max_prompt_tokens"] = self.max_prompt_tokens - if self.max_context_window_tokens is not None: - result["max_context_window_tokens"] = self.max_context_window_tokens - if self.vision is not None: - result["vision"] = self.vision.to_dict() - return result - - -@dataclass -class ModelSupports: - """Model support flags""" - - vision: bool - reasoning_effort: bool = False # Whether this model supports reasoning effort - - @staticmethod - def from_dict(obj: Any) -> ModelSupports: - assert isinstance(obj, dict) - vision = obj.get("vision") - if vision is None: - raise ValueError("Missing required field 'vision' in ModelSupports") - reasoning_effort = obj.get("reasoningEffort", False) - return ModelSupports(vision=bool(vision), reasoning_effort=bool(reasoning_effort)) - - def to_dict(self) -> dict: - result: dict = {} - result["vision"] = self.vision - result["reasoningEffort"] = self.reasoning_effort - return result - - -@dataclass -class ModelCapabilities: - """Model capabilities and limits""" - - supports: ModelSupports - limits: ModelLimits - - @staticmethod - def from_dict(obj: Any) -> ModelCapabilities: - assert isinstance(obj, dict) - supports_dict = obj.get("supports") - limits_dict = obj.get("limits") - if supports_dict is None or limits_dict is None: - raise ValueError( - f"Missing required fields in ModelCapabilities: supports={supports_dict}, " - f"limits={limits_dict}" - ) - supports = ModelSupports.from_dict(supports_dict) - limits = ModelLimits.from_dict(limits_dict) - return ModelCapabilities(supports=supports, limits=limits) - - def to_dict(self) -> dict: - result: dict = {} - result["supports"] = self.supports.to_dict() - result["limits"] = self.limits.to_dict() - return result - - -@dataclass -class ModelPolicy: - """Model policy state""" - - state: str # "enabled", "disabled", or "unconfigured" - terms: str - - @staticmethod - def from_dict(obj: Any) -> ModelPolicy: - assert isinstance(obj, dict) - state = obj.get("state") - terms = obj.get("terms") - if state is None or terms is None: - raise ValueError( - f"Missing required fields in ModelPolicy: state={state}, terms={terms}" - ) - return ModelPolicy(state=str(state), terms=str(terms)) - - def to_dict(self) -> dict: - result: dict = {} - result["state"] = self.state - result["terms"] = self.terms - return result - - -@dataclass -class ModelBilling: - """Model billing information""" - - multiplier: float - - @staticmethod - def from_dict(obj: Any) -> ModelBilling: - assert isinstance(obj, dict) - multiplier = obj.get("multiplier") - if multiplier is None: - raise ValueError("Missing required field 'multiplier' in ModelBilling") - return ModelBilling(multiplier=float(multiplier)) - - def to_dict(self) -> dict: - result: dict = {} - result["multiplier"] = self.multiplier - return result - - -@dataclass -class ModelInfo: - """Information about an available model""" - - id: str # Model identifier (e.g., "claude-sonnet-4.5") - name: str # Display name - capabilities: ModelCapabilities # Model capabilities and limits - policy: ModelPolicy | None = None # Policy state - billing: ModelBilling | None = None # Billing information - # Supported reasoning effort levels (only present if model supports reasoning effort) - supported_reasoning_efforts: list[str] | None = None - # Default reasoning effort level (only present if model supports reasoning effort) - default_reasoning_effort: str | None = None - - @staticmethod - def from_dict(obj: Any) -> ModelInfo: - assert isinstance(obj, dict) - id = obj.get("id") - name = obj.get("name") - capabilities_dict = obj.get("capabilities") - if id is None or name is None or capabilities_dict is None: - raise ValueError( - f"Missing required fields in ModelInfo: id={id}, name={name}, " - f"capabilities={capabilities_dict}" - ) - capabilities = ModelCapabilities.from_dict(capabilities_dict) - policy_dict = obj.get("policy") - policy = ModelPolicy.from_dict(policy_dict) if policy_dict else None - billing_dict = obj.get("billing") - billing = ModelBilling.from_dict(billing_dict) if billing_dict else None - supported_reasoning_efforts = obj.get("supportedReasoningEfforts") - default_reasoning_effort = obj.get("defaultReasoningEffort") - return ModelInfo( - id=str(id), - name=str(name), - capabilities=capabilities, - policy=policy, - billing=billing, - supported_reasoning_efforts=supported_reasoning_efforts, - default_reasoning_effort=default_reasoning_effort, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["id"] = self.id - result["name"] = self.name - result["capabilities"] = self.capabilities.to_dict() - if self.policy is not None: - result["policy"] = self.policy.to_dict() - if self.billing is not None: - result["billing"] = self.billing.to_dict() - if self.supported_reasoning_efforts is not None: - result["supportedReasoningEfforts"] = self.supported_reasoning_efforts - if self.default_reasoning_effort is not None: - result["defaultReasoningEffort"] = self.default_reasoning_effort - return result - - -@dataclass -class SessionContext: - """Working directory context for a session""" - - cwd: str # Working directory where the session was created - gitRoot: 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 - - @staticmethod - def from_dict(obj: Any) -> SessionContext: - assert isinstance(obj, dict) - cwd = obj.get("cwd") - if cwd is None: - raise ValueError("Missing required field 'cwd' in SessionContext") - return SessionContext( - cwd=str(cwd), - gitRoot=obj.get("gitRoot"), - repository=obj.get("repository"), - branch=obj.get("branch"), - ) - - def to_dict(self) -> dict: - result: dict = {"cwd": self.cwd} - if self.gitRoot is not None: - result["gitRoot"] = self.gitRoot - if self.repository is not None: - result["repository"] = self.repository - if self.branch is not None: - result["branch"] = self.branch - return result - - -@dataclass -class SessionListFilter: - """Filter options for listing sessions""" - - cwd: str | None = None # Filter by exact cwd match - gitRoot: str | None = None # Filter by git root - repository: str | None = None # Filter by repository (owner/repo format) - branch: str | None = None # Filter by branch - - def to_dict(self) -> dict: - result: dict = {} - if self.cwd is not None: - result["cwd"] = self.cwd - if self.gitRoot is not None: - result["gitRoot"] = self.gitRoot - if self.repository is not None: - result["repository"] = self.repository - if self.branch is not None: - result["branch"] = self.branch - return result - - -@dataclass -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 - 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: - raise ValueError( - f"Missing required fields in SessionMetadata: sessionId={sessionId}, " - f"startTime={startTime}, modifiedTime={modifiedTime}, isRemote={isRemote}" - ) - 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), - 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 - if self.summary is not None: - result["summary"] = self.summary - if self.context is not None: - result["context"] = self.context.to_dict() - return result - - -# Session Lifecycle Types (for TUI+server mode) - -SessionLifecycleEventType = Literal[ - "session.created", - "session.deleted", - "session.updated", - "session.foreground", - "session.background", -] - - -@dataclass -class SessionLifecycleEventMetadata: - """Metadata for session lifecycle events.""" - - startTime: str - modifiedTime: str - summary: str | None = None - - @staticmethod - def from_dict(data: dict) -> SessionLifecycleEventMetadata: - return SessionLifecycleEventMetadata( - startTime=data.get("startTime", ""), - modifiedTime=data.get("modifiedTime", ""), - summary=data.get("summary"), - ) - - -@dataclass -class SessionLifecycleEvent: - """Session lifecycle event notification.""" - - type: SessionLifecycleEventType - sessionId: 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, - ) - - -# Handler types for session lifecycle events -SessionLifecycleHandler = Callable[[SessionLifecycleEvent], None] diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index 63d3e7322..e376b6045 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -2,8 +2,10 @@ import pytest -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig from copilot.generated.rpc import SessionAgentSelectParams +from copilot.session import PermissionHandler from .testharness import CLI_PATH, E2ETestContext diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py index fc4cc60b5..0a764029c 100644 --- a/python/e2e/test_ask_user.py +++ b/python/e2e/test_ask_user.py @@ -4,7 +4,7 @@ import pytest -from copilot import PermissionHandler +from copilot.session import PermissionHandler from .testharness import E2ETestContext diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index d266991f7..4ea3fc843 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -2,7 +2,9 @@ import pytest -from copilot import CopilotClient, PermissionHandler, StopError, SubprocessConfig +from copilot import CopilotClient +from copilot.client import StopError, SubprocessConfig +from copilot.session import PermissionHandler from .testharness import CLI_PATH diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index beb51e74b..c6df2bffa 100644 --- a/python/e2e/test_compaction.py +++ b/python/e2e/test_compaction.py @@ -2,8 +2,8 @@ import pytest -from copilot import PermissionHandler from copilot.generated.session_events import SessionEventType +from copilot.session import PermissionHandler from .testharness import E2ETestContext diff --git a/python/e2e/test_hooks.py b/python/e2e/test_hooks.py index 2858d40f2..e355f3a80 100644 --- a/python/e2e/test_hooks.py +++ b/python/e2e/test_hooks.py @@ -4,7 +4,7 @@ import pytest -from copilot import PermissionHandler +from copilot.session import PermissionHandler from .testharness import E2ETestContext from .testharness.helper import write_file diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index c4bd89414..c6a590d6c 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -6,7 +6,7 @@ import pytest -from copilot import CustomAgentConfig, MCPServerConfig, PermissionHandler +from copilot.session import CustomAgentConfig, MCPServerConfig, PermissionHandler from .testharness import E2ETestContext, get_final_assistant_message diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index c77ae86e1..2d866e8aa 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -13,15 +13,10 @@ import pytest_asyncio from pydantic import BaseModel, Field -from copilot import ( - CopilotClient, - ExternalServerConfig, - PermissionHandler, - PermissionRequestResult, - SubprocessConfig, - ToolInvocation, - define_tool, -) +from copilot import CopilotClient, define_tool +from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.tools import ToolInvocation from .testharness import get_final_assistant_message from .testharness.proxy import CapiProxy diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index a673d63b5..692c600e0 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -6,7 +6,7 @@ import pytest -from copilot import PermissionHandler, PermissionRequest, PermissionRequestResult +from copilot.session import PermissionHandler, PermissionRequest, PermissionRequestResult from .testharness import E2ETestContext from .testharness.helper import read_file, write_file diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index 814da067d..a86f874db 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -2,8 +2,10 @@ import pytest -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig from copilot.generated.rpc import PingParams +from copilot.session import PermissionHandler from .testharness import CLI_PATH, E2ETestContext diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index ffb0cd2bc..8c155f8c2 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -4,8 +4,10 @@ import pytest -from copilot import CopilotClient, PermissionHandler, SubprocessConfig -from copilot.types import Tool, ToolResult +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler +from copilot.tools import Tool, ToolResult from .testharness import E2ETestContext, get_final_assistant_message, get_next_event_of_type diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index 9b0599975..feacae73b 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -7,7 +7,7 @@ import pytest -from copilot import PermissionHandler +from copilot.session import PermissionHandler from .testharness import E2ETestContext diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index 05e977e12..c2e79814a 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -4,7 +4,9 @@ import pytest -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler from .testharness import E2ETestContext diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index 458897d49..4bb853976 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -5,12 +5,9 @@ import pytest from pydantic import BaseModel, Field -from copilot import ( - PermissionHandler, - PermissionRequestResult, - ToolInvocation, - define_tool, -) +from copilot import define_tool +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.tools import ToolInvocation from .testharness import E2ETestContext, get_final_assistant_message diff --git a/python/e2e/test_tools_unit.py b/python/e2e/test_tools_unit.py index c1a9163e1..c9c996f0e 100644 --- a/python/e2e/test_tools_unit.py +++ b/python/e2e/test_tools_unit.py @@ -5,8 +5,8 @@ import pytest from pydantic import BaseModel, Field -from copilot import ToolInvocation, ToolResult, define_tool -from copilot.tools import _normalize_result +from copilot import define_tool +from copilot.tools import ToolInvocation, ToolResult, _normalize_result class TestDefineTool: diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index 27dce38a1..6a4bac6d2 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -10,7 +10,8 @@ import tempfile from pathlib import Path -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig from .proxy import CapiProxy diff --git a/python/samples/chat.py b/python/samples/chat.py index ee94c21fe..890191b19 100644 --- a/python/samples/chat.py +++ b/python/samples/chat.py @@ -1,6 +1,7 @@ import asyncio -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient +from copilot.session import PermissionHandler BLUE = "\033[34m" RESET = "\033[0m" diff --git a/python/test_client.py b/python/test_client.py index 9f8f38423..41f536d28 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -6,15 +6,16 @@ import pytest -from copilot import ( - CopilotClient, +from copilot import CopilotClient, define_tool +from copilot.client import ( ExternalServerConfig, - PermissionHandler, - PermissionRequestResult, + ModelCapabilities, + ModelInfo, + ModelLimits, + ModelSupports, SubprocessConfig, - define_tool, ) -from copilot.types import ModelCapabilities, ModelInfo, ModelLimits, ModelSupports +from copilot.session import PermissionHandler, PermissionRequestResult from e2e.testharness import CLI_PATH diff --git a/python/test_telemetry.py b/python/test_telemetry.py index aec38f816..d10ffeb9f 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.types import SubprocessConfig, TelemetryConfig +from copilot.client import SubprocessConfig, TelemetryConfig class TestGetTraceContext: diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index 995002070..f7cb96d35 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -1,7 +1,9 @@ import asyncio import os import sys -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514") diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 57a49f2a5..4717155cb 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -1,7 +1,9 @@ import asyncio import os import sys -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY") diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index 87dad5866..71ce04a1e 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -1,7 +1,9 @@ import asyncio import os import sys -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1") OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b") diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index fadd1c79d..5920e5356 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -1,7 +1,9 @@ import asyncio import os import sys -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "claude-haiku-4.5") diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index e7f640ae9..df1980a50 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -4,7 +4,9 @@ import time import urllib.request -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler DEVICE_CODE_URL = "https://github.com/login/device/code" 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 c9ab35bce..7d5585adc 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -5,7 +5,9 @@ import urllib.request from flask import Flask, request, jsonify -from copilot import CopilotClient, PermissionHandler, ExternalServerConfig +from copilot import CopilotClient +from copilot.client import ExternalServerConfig +from copilot.session import PermissionHandler app = Flask(__name__) diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index 07eb74e20..f051b312f 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, ExternalServerConfig +from copilot import CopilotClient +from copilot.client import ExternalServerConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index 07eb74e20..f051b312f 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, ExternalServerConfig +from copilot import CopilotClient +from copilot.client import ExternalServerConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index 382f9c4f9..5799bbfc8 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index bc9782b6b..3b6cd70a8 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -1,6 +1,7 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig hook_log: list[str] = [] diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index e4de98a9a..c50e4dd94 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -1,6 +1,7 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig # Track which tools requested permission permission_log: list[str] = [] diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index 92981861d..971530c01 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -1,6 +1,7 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig input_log: list[str] = [] diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index 55f1cb394..37cb0e26d 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 22f321b22..f92f0b09e 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index 37654e269..0de559cda 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler SYSTEM_PROMPT = """You are a helpful assistant. Answer questions about attached files concisely.""" diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index 8baed649d..76011fe05 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index 15d354258..e0529ecf8 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler PIRATE_PROMPT = """You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.""" diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index 5c3994c4c..a0f7f83c4 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler PIRATE_PROMPT = "You are a pirate. Always say Arrr!" ROBOT_PROMPT = "You are a robot. Always say BEEP BOOP!" diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 30aa40cd1..9169c4e5f 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index 049ca1f83..091cb7fe5 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index 20fd4902e..a2bee47c5 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index c30107a2f..3d4aa2989 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index 9edd04115..07fd76e1c 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index c9a8047ec..02cd3d103 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler SYSTEM_PROMPT = """You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index afa871d83..3ec9fb2ee 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -2,7 +2,8 @@ import os from pathlib import Path -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig async def main(): diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 668bca197..e945a4449 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler SYSTEM_PROMPT = """You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.""" diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 73c539fe1..687933973 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -3,7 +3,9 @@ from pydantic import BaseModel, Field -from copilot import CopilotClient, PermissionHandler, SubprocessConfig, define_tool +from copilot import CopilotClient, define_tool +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler class GrepParams(BaseModel): diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index 92f2593a6..f7635c6c6 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -1,6 +1,7 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig, define_tool +from copilot import CopilotClient, define_tool +from copilot.client import SubprocessConfig from pydantic import BaseModel, Field # In-memory virtual filesystem diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index d1b8a5696..d90b0343a 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -1,7 +1,9 @@ import asyncio import os import sys -from copilot import CopilotClient, PermissionHandler, ExternalServerConfig +from copilot import CopilotClient +from copilot.client import ExternalServerConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index 382f9c4f9..5799bbfc8 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, SubprocessConfig +from copilot import CopilotClient +from copilot.client import SubprocessConfig +from copilot.session import PermissionHandler async def main(): diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index 07eb74e20..f051b312f 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -1,6 +1,8 @@ import asyncio import os -from copilot import CopilotClient, PermissionHandler, ExternalServerConfig +from copilot import CopilotClient +from copilot.client import ExternalServerConfig +from copilot.session import PermissionHandler async def main():