From 68c84672a5afda2a92c3b6e88f55a6476ce1b10b Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 11 Apr 2026 14:51:05 -0400 Subject: [PATCH 01/14] Generate dedicated Python session event types Align Python session event generation with the newer Go-style dedicated per-event payload model instead of the old merged quicktype Data shape. This updates the runtime/tests for typed payloads while preserving compatibility aliases and legacy Data behavior for existing callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/copilot/generated/session_events.py | 6901 ++++++++++++-------- python/copilot/session.py | 62 +- python/test_commands_and_elicitation.py | 33 +- python/test_event_forward_compatibility.py | 68 +- scripts/codegen/python.ts | 1166 +++- 5 files changed, 5339 insertions(+), 2891 deletions(-) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 2c1dbffb6..4957d2df7 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -3,13 +3,16 @@ Generated from: session-events.schema.json """ -from enum import Enum +from __future__ import annotations + +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, TypeVar, Callable, cast from datetime import datetime +from enum import Enum +from typing import Any, TypeVar, cast from uuid import UUID -import dateutil.parser +import dateutil.parser T = TypeVar("T") EnumT = TypeVar("EnumT", bound=Enum) @@ -20,9 +23,24 @@ def from_str(x: Any) -> str: return x -def from_list(f: Callable[[Any], T], x: Any) -> list[T]: - assert isinstance(x, list) - return [f(y) for y in x] +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + return x + + +def to_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + return x + + +def from_float(x: Any) -> float: + assert isinstance(x, (float, int)) and not isinstance(x, bool) + return float(x) + + +def to_float(x: Any) -> float: + assert isinstance(x, (float, int)) and not isinstance(x, bool) + return float(x) def from_bool(x: Any) -> bool: @@ -35,7 +53,7 @@ def from_none(x: Any) -> Any: return x -def from_union(fs, x): +def from_union(fs: list[Callable[[Any], T]], x: Any) -> T: for f in fs: try: return f(x) @@ -44,14 +62,35 @@ def from_union(fs, x): assert False -def from_float(x: Any) -> float: - assert isinstance(x, (float, int)) and not isinstance(x, bool) - return float(x) +def from_list(f: Callable[[Any], T], x: Any) -> list[T]: + assert isinstance(x, list) + return [f(item) for item in x] -def to_float(x: Any) -> float: - assert isinstance(x, (int, float)) - return x +def from_dict(f: Callable[[Any], T], x: Any) -> dict[str, T]: + assert isinstance(x, dict) + return {key: f(value) for key, value in x.items()} + + +def from_datetime(x: Any) -> datetime: + return dateutil.parser.parse(from_str(x)) + + +def to_datetime(x: datetime) -> str: + return x.isoformat() + + +def from_uuid(x: Any) -> UUID: + return UUID(from_str(x)) + + +def to_uuid(x: UUID) -> str: + return str(x) + + +def parse_enum(c: type[EnumT], x: Any) -> EnumT: + assert isinstance(x, str) + return c(x) def to_class(c: type[T], x: Any) -> dict: @@ -59,3366 +98,4385 @@ def to_class(c: type[T], x: Any) -> dict: return cast(Any, x).to_dict() -def to_enum(c: type[EnumT], x: Any) -> EnumT: +def to_enum(c: type[EnumT], x: Any) -> str: assert isinstance(x, c) - return x.value + return cast(str, x.value) -def from_dict(f: Callable[[Any], T], x: Any) -> dict[str, T]: - assert isinstance(x, dict) - return { k: f(v) for (k, v) in x.items() } +class SessionEventType(Enum): + SESSION_START = "session.start" + SESSION_RESUME = "session.resume" + SESSION_REMOTE_STEERABLE_CHANGED = "session.remote_steerable_changed" + SESSION_ERROR = "session.error" + SESSION_IDLE = "session.idle" + SESSION_TITLE_CHANGED = "session.title_changed" + SESSION_INFO = "session.info" + SESSION_WARNING = "session.warning" + SESSION_MODEL_CHANGE = "session.model_change" + SESSION_MODE_CHANGED = "session.mode_changed" + SESSION_PLAN_CHANGED = "session.plan_changed" + SESSION_WORKSPACE_FILE_CHANGED = "session.workspace_file_changed" + SESSION_IMPORT_LEGACY = "session.import_legacy" + SESSION_HANDOFF = "session.handoff" + SESSION_TRUNCATION = "session.truncation" + SESSION_SNAPSHOT_REWIND = "session.snapshot_rewind" + SESSION_SHUTDOWN = "session.shutdown" + SESSION_CONTEXT_CHANGED = "session.context_changed" + SESSION_USAGE_INFO = "session.usage_info" + SESSION_COMPACTION_START = "session.compaction_start" + SESSION_COMPACTION_COMPLETE = "session.compaction_complete" + SESSION_TASK_COMPLETE = "session.task_complete" + USER_MESSAGE = "user.message" + PENDING_MESSAGES_MODIFIED = "pending_messages.modified" + ASSISTANT_TURN_START = "assistant.turn_start" + ASSISTANT_INTENT = "assistant.intent" + ASSISTANT_REASONING = "assistant.reasoning" + ASSISTANT_REASONING_DELTA = "assistant.reasoning_delta" + ASSISTANT_STREAMING_DELTA = "assistant.streaming_delta" + ASSISTANT_MESSAGE = "assistant.message" + ASSISTANT_MESSAGE_DELTA = "assistant.message_delta" + ASSISTANT_TURN_END = "assistant.turn_end" + ASSISTANT_USAGE = "assistant.usage" + ABORT = "abort" + TOOL_USER_REQUESTED = "tool.user_requested" + TOOL_EXECUTION_START = "tool.execution_start" + TOOL_EXECUTION_PARTIAL_RESULT = "tool.execution_partial_result" + TOOL_EXECUTION_PROGRESS = "tool.execution_progress" + TOOL_EXECUTION_COMPLETE = "tool.execution_complete" + SKILL_INVOKED = "skill.invoked" + SUBAGENT_STARTED = "subagent.started" + SUBAGENT_COMPLETED = "subagent.completed" + SUBAGENT_FAILED = "subagent.failed" + SUBAGENT_SELECTED = "subagent.selected" + SUBAGENT_DESELECTED = "subagent.deselected" + HOOK_START = "hook.start" + HOOK_END = "hook.end" + SYSTEM_MESSAGE = "system.message" + SYSTEM_NOTIFICATION = "system.notification" + PERMISSION_REQUESTED = "permission.requested" + PERMISSION_COMPLETED = "permission.completed" + USER_INPUT_REQUESTED = "user_input.requested" + USER_INPUT_COMPLETED = "user_input.completed" + ELICITATION_REQUESTED = "elicitation.requested" + ELICITATION_COMPLETED = "elicitation.completed" + SAMPLING_REQUESTED = "sampling.requested" + SAMPLING_COMPLETED = "sampling.completed" + MCP_OAUTH_REQUIRED = "mcp.oauth_required" + MCP_OAUTH_COMPLETED = "mcp.oauth_completed" + EXTERNAL_TOOL_REQUESTED = "external_tool.requested" + EXTERNAL_TOOL_COMPLETED = "external_tool.completed" + COMMAND_QUEUED = "command.queued" + COMMAND_EXECUTE = "command.execute" + COMMAND_COMPLETED = "command.completed" + COMMANDS_CHANGED = "commands.changed" + CAPABILITIES_CHANGED = "capabilities.changed" + EXIT_PLAN_MODE_REQUESTED = "exit_plan_mode.requested" + EXIT_PLAN_MODE_COMPLETED = "exit_plan_mode.completed" + SESSION_TOOLS_UPDATED = "session.tools_updated" + SESSION_BACKGROUND_TASKS_CHANGED = "session.background_tasks_changed" + SESSION_SKILLS_LOADED = "session.skills_loaded" + SESSION_CUSTOM_AGENTS_UPDATED = "session.custom_agents_updated" + SESSION_MCP_SERVERS_LOADED = "session.mcp_servers_loaded" + SESSION_MCP_SERVER_STATUS_CHANGED = "session.mcp_server_status_changed" + SESSION_EXTENSIONS_LOADED = "session.extensions_loaded" + UNKNOWN = "unknown" + + @classmethod + def _missing_(cls, value: object) -> "SessionEventType": + return cls.UNKNOWN -def from_datetime(x: Any) -> datetime: - return dateutil.parser.parse(x) +@dataclass +class RawSessionEventData: + raw: Any + @staticmethod + def from_dict(obj: Any) -> "RawSessionEventData": + return RawSessionEventData(obj) -def from_int(x: Any) -> int: - assert isinstance(x, int) and not isinstance(x, bool) - return x + def to_dict(self) -> Any: + return self.raw -class ElicitationCompletedAction(Enum): - """The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" - (dismissed) - """ - ACCEPT = "accept" - CANCEL = "cancel" - DECLINE = "decline" +def _compat_to_python_key(name: str) -> str: + result: list[str] = [] + for index, char in enumerate(name.replace(".", "_")): + if char.isupper() and index > 0 and (not name[index - 1].isupper() or (index + 1 < len(name) and name[index + 1].islower())): + result.append("_") + result.append(char.lower()) + return "".join(result) -class UserMessageAgentMode(Enum): - """The agent mode that was active when this message was sent""" +def _compat_to_json_key(name: str) -> str: + parts = name.split("_") + if not parts: + return name + return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:]) - AUTOPILOT = "autopilot" - INTERACTIVE = "interactive" - PLAN = "plan" - SHELL = "shell" +def _compat_to_json_value(value: Any) -> Any: + if hasattr(value, "to_dict"): + return cast(Any, value).to_dict() + if isinstance(value, Enum): + return value.value + if isinstance(value, datetime): + return value.isoformat() + if isinstance(value, UUID): + return str(value) + if isinstance(value, list): + return [_compat_to_json_value(item) for item in value] + if isinstance(value, dict): + return {key: _compat_to_json_value(item) for key, item in value.items()} + return value -@dataclass -class CustomAgentsUpdatedAgent: - description: str - """Description of what the agent does""" - display_name: str - """Human-readable display name""" +def _compat_from_json_value(value: Any) -> Any: + return value - id: str - """Unique identifier for the agent""" - name: str - """Internal name of the agent""" +class Data: + """Backward-compatible shim for manually constructed event payloads.""" - source: str - """Source location: user, project, inherited, remote, or plugin""" + def __init__(self, **kwargs: Any): + self._values = {key: _compat_from_json_value(value) for key, value in kwargs.items()} + for key, value in self._values.items(): + setattr(self, key, value) - tools: list[str] - """List of tool names available to this agent""" + @staticmethod + def from_dict(obj: Any) -> "Data": + assert isinstance(obj, dict) + return Data(**{_compat_to_python_key(key): _compat_from_json_value(value) for key, value in obj.items()}) - user_invocable: bool - """Whether the agent can be selected by the user""" + def to_dict(self) -> dict: + return {_compat_to_json_key(key): _compat_to_json_value(value) for key, value in self._values.items() if value is not None} - model: str | None = None - """Model override for this agent, if set""" + +@dataclass +class SessionStartDataContext: + """Working directory and git context at session start""" + cwd: str + git_root: str | None = None + repository: str | None = None + host_type: SessionStartDataContextHostType | None = None + branch: str | None = None + head_commit: str | None = None + base_commit: str | None = None @staticmethod - def from_dict(obj: Any) -> 'CustomAgentsUpdatedAgent': + def from_dict(obj: Any) -> "SessionStartDataContext": assert isinstance(obj, dict) - description = from_str(obj.get("description")) - display_name = from_str(obj.get("displayName")) - id = from_str(obj.get("id")) - name = from_str(obj.get("name")) - source = from_str(obj.get("source")) - tools = from_list(from_str, obj.get("tools")) - user_invocable = from_bool(obj.get("userInvocable")) - model = from_union([from_str, from_none], obj.get("model")) - return CustomAgentsUpdatedAgent(description, display_name, id, name, source, tools, user_invocable, model) + cwd = from_str(obj.get("cwd")) + git_root = from_union([from_none, lambda x: from_str(x)], obj.get("gitRoot")) + repository = from_union([from_none, lambda x: from_str(x)], obj.get("repository")) + host_type = from_union([from_none, lambda x: parse_enum(SessionStartDataContextHostType, x)], obj.get("hostType")) + branch = from_union([from_none, lambda x: from_str(x)], obj.get("branch")) + head_commit = from_union([from_none, lambda x: from_str(x)], obj.get("headCommit")) + base_commit = from_union([from_none, lambda x: from_str(x)], obj.get("baseCommit")) + return SessionStartDataContext( + cwd=cwd, + git_root=git_root, + repository=repository, + host_type=host_type, + branch=branch, + head_commit=head_commit, + base_commit=base_commit, + ) def to_dict(self) -> dict: result: dict = {} - result["description"] = from_str(self.description) - result["displayName"] = from_str(self.display_name) - result["id"] = from_str(self.id) - result["name"] = from_str(self.name) - result["source"] = from_str(self.source) - result["tools"] = from_list(from_str, self.tools) - result["userInvocable"] = from_bool(self.user_invocable) - if self.model is not None: - result["model"] = from_union([from_str, from_none], self.model) + result["cwd"] = from_str(self.cwd) + if self.git_root is not None: + result["gitRoot"] = from_union([from_none, lambda x: from_str(x)], self.git_root) + if self.repository is not None: + result["repository"] = from_union([from_none, lambda x: from_str(x)], self.repository) + if self.host_type is not None: + result["hostType"] = from_union([from_none, lambda x: to_enum(SessionStartDataContextHostType, x)], self.host_type) + if self.branch is not None: + result["branch"] = from_union([from_none, lambda x: from_str(x)], self.branch) + if self.head_commit is not None: + result["headCommit"] = from_union([from_none, lambda x: from_str(x)], self.head_commit) + if self.base_commit is not None: + result["baseCommit"] = from_union([from_none, lambda x: from_str(x)], self.base_commit) return result @dataclass -class UserMessageAttachmentFileLineRange: - """Optional line range to scope the attachment to a specific section of the file""" - - end: float - """End line number (1-based, inclusive)""" - - start: float - """Start line number (1-based)""" +class SessionStartData: + """Session initialization metadata including context and configuration""" + session_id: str + version: float + producer: str + copilot_version: str + start_time: datetime + selected_model: str | None = None + reasoning_effort: str | None = None + context: SessionStartDataContext | None = None + already_in_use: bool | None = None + remote_steerable: bool | None = None @staticmethod - def from_dict(obj: Any) -> 'UserMessageAttachmentFileLineRange': + def from_dict(obj: Any) -> "SessionStartData": assert isinstance(obj, dict) - end = from_float(obj.get("end")) - start = from_float(obj.get("start")) - return UserMessageAttachmentFileLineRange(end, start) + session_id = from_str(obj.get("sessionId")) + version = from_float(obj.get("version")) + producer = from_str(obj.get("producer")) + copilot_version = from_str(obj.get("copilotVersion")) + start_time = from_datetime(obj.get("startTime")) + selected_model = from_union([from_none, lambda x: from_str(x)], obj.get("selectedModel")) + reasoning_effort = from_union([from_none, lambda x: from_str(x)], obj.get("reasoningEffort")) + context = from_union([from_none, lambda x: SessionStartDataContext.from_dict(x)], obj.get("context")) + already_in_use = from_union([from_none, lambda x: from_bool(x)], obj.get("alreadyInUse")) + remote_steerable = from_union([from_none, lambda x: from_bool(x)], obj.get("remoteSteerable")) + return SessionStartData( + session_id=session_id, + version=version, + producer=producer, + copilot_version=copilot_version, + start_time=start_time, + selected_model=selected_model, + reasoning_effort=reasoning_effort, + context=context, + already_in_use=already_in_use, + remote_steerable=remote_steerable, + ) def to_dict(self) -> dict: result: dict = {} - result["end"] = to_float(self.end) - result["start"] = to_float(self.start) + result["sessionId"] = from_str(self.session_id) + result["version"] = to_float(self.version) + result["producer"] = from_str(self.producer) + result["copilotVersion"] = from_str(self.copilot_version) + result["startTime"] = to_datetime(self.start_time) + if self.selected_model is not None: + result["selectedModel"] = from_union([from_none, lambda x: from_str(x)], self.selected_model) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_none, lambda x: from_str(x)], self.reasoning_effort) + if self.context is not None: + result["context"] = from_union([from_none, lambda x: to_class(SessionStartDataContext, x)], self.context) + if self.already_in_use is not None: + result["alreadyInUse"] = from_union([from_none, lambda x: from_bool(x)], self.already_in_use) + if self.remote_steerable is not None: + result["remoteSteerable"] = from_union([from_none, lambda x: from_bool(x)], self.remote_steerable) return result -class UserMessageAttachmentGithubReferenceType(Enum): - """Type of GitHub reference""" - - DISCUSSION = "discussion" - ISSUE = "issue" - PR = "pr" - - @dataclass -class UserMessageAttachmentSelectionDetailsEnd: - """End position of the selection""" - - character: float - """End character offset within the line (0-based)""" - - line: float - """End line number (0-based)""" +class SessionResumeDataContext: + """Updated working directory and git context at resume time""" + cwd: str + git_root: str | None = None + repository: str | None = None + host_type: SessionStartDataContextHostType | None = None + branch: str | None = None + head_commit: str | None = None + base_commit: str | None = None @staticmethod - def from_dict(obj: Any) -> 'UserMessageAttachmentSelectionDetailsEnd': + def from_dict(obj: Any) -> "SessionResumeDataContext": assert isinstance(obj, dict) - character = from_float(obj.get("character")) - line = from_float(obj.get("line")) - return UserMessageAttachmentSelectionDetailsEnd(character, line) + cwd = from_str(obj.get("cwd")) + git_root = from_union([from_none, lambda x: from_str(x)], obj.get("gitRoot")) + repository = from_union([from_none, lambda x: from_str(x)], obj.get("repository")) + host_type = from_union([from_none, lambda x: parse_enum(SessionStartDataContextHostType, x)], obj.get("hostType")) + branch = from_union([from_none, lambda x: from_str(x)], obj.get("branch")) + head_commit = from_union([from_none, lambda x: from_str(x)], obj.get("headCommit")) + base_commit = from_union([from_none, lambda x: from_str(x)], obj.get("baseCommit")) + return SessionResumeDataContext( + cwd=cwd, + git_root=git_root, + repository=repository, + host_type=host_type, + branch=branch, + head_commit=head_commit, + base_commit=base_commit, + ) def to_dict(self) -> dict: result: dict = {} - result["character"] = to_float(self.character) - result["line"] = to_float(self.line) + result["cwd"] = from_str(self.cwd) + if self.git_root is not None: + result["gitRoot"] = from_union([from_none, lambda x: from_str(x)], self.git_root) + if self.repository is not None: + result["repository"] = from_union([from_none, lambda x: from_str(x)], self.repository) + if self.host_type is not None: + result["hostType"] = from_union([from_none, lambda x: to_enum(SessionStartDataContextHostType, x)], self.host_type) + if self.branch is not None: + result["branch"] = from_union([from_none, lambda x: from_str(x)], self.branch) + if self.head_commit is not None: + result["headCommit"] = from_union([from_none, lambda x: from_str(x)], self.head_commit) + if self.base_commit is not None: + result["baseCommit"] = from_union([from_none, lambda x: from_str(x)], self.base_commit) return result @dataclass -class UserMessageAttachmentSelectionDetailsStart: - """Start position of the selection""" - - character: float - """Start character offset within the line (0-based)""" - - line: float - """Start line number (0-based)""" +class SessionResumeData: + """Session resume metadata including current context and event count""" + resume_time: datetime + event_count: float + selected_model: str | None = None + reasoning_effort: str | None = None + context: SessionResumeDataContext | None = None + already_in_use: bool | None = None + remote_steerable: bool | None = None @staticmethod - def from_dict(obj: Any) -> 'UserMessageAttachmentSelectionDetailsStart': + def from_dict(obj: Any) -> "SessionResumeData": assert isinstance(obj, dict) - character = from_float(obj.get("character")) - line = from_float(obj.get("line")) - return UserMessageAttachmentSelectionDetailsStart(character, line) + resume_time = from_datetime(obj.get("resumeTime")) + event_count = from_float(obj.get("eventCount")) + selected_model = from_union([from_none, lambda x: from_str(x)], obj.get("selectedModel")) + reasoning_effort = from_union([from_none, lambda x: from_str(x)], obj.get("reasoningEffort")) + context = from_union([from_none, lambda x: SessionResumeDataContext.from_dict(x)], obj.get("context")) + already_in_use = from_union([from_none, lambda x: from_bool(x)], obj.get("alreadyInUse")) + remote_steerable = from_union([from_none, lambda x: from_bool(x)], obj.get("remoteSteerable")) + return SessionResumeData( + resume_time=resume_time, + event_count=event_count, + selected_model=selected_model, + reasoning_effort=reasoning_effort, + context=context, + already_in_use=already_in_use, + remote_steerable=remote_steerable, + ) def to_dict(self) -> dict: result: dict = {} - result["character"] = to_float(self.character) - result["line"] = to_float(self.line) + result["resumeTime"] = to_datetime(self.resume_time) + result["eventCount"] = to_float(self.event_count) + if self.selected_model is not None: + result["selectedModel"] = from_union([from_none, lambda x: from_str(x)], self.selected_model) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_none, lambda x: from_str(x)], self.reasoning_effort) + if self.context is not None: + result["context"] = from_union([from_none, lambda x: to_class(SessionResumeDataContext, x)], self.context) + if self.already_in_use is not None: + result["alreadyInUse"] = from_union([from_none, lambda x: from_bool(x)], self.already_in_use) + if self.remote_steerable is not None: + result["remoteSteerable"] = from_union([from_none, lambda x: from_bool(x)], self.remote_steerable) return result @dataclass -class UserMessageAttachmentSelectionDetails: - """Position range of the selection within the file""" - - end: UserMessageAttachmentSelectionDetailsEnd - """End position of the selection""" - - start: UserMessageAttachmentSelectionDetailsStart - """Start position of the selection""" +class SessionRemoteSteerableChangedData: + """Notifies Mission Control that the session's remote steering capability has changed""" + remote_steerable: bool @staticmethod - def from_dict(obj: Any) -> 'UserMessageAttachmentSelectionDetails': + def from_dict(obj: Any) -> "SessionRemoteSteerableChangedData": assert isinstance(obj, dict) - end = UserMessageAttachmentSelectionDetailsEnd.from_dict(obj.get("end")) - start = UserMessageAttachmentSelectionDetailsStart.from_dict(obj.get("start")) - return UserMessageAttachmentSelectionDetails(end, start) + remote_steerable = from_bool(obj.get("remoteSteerable")) + return SessionRemoteSteerableChangedData( + remote_steerable=remote_steerable, + ) def to_dict(self) -> dict: result: dict = {} - result["end"] = to_class(UserMessageAttachmentSelectionDetailsEnd, self.end) - result["start"] = to_class(UserMessageAttachmentSelectionDetailsStart, self.start) + result["remoteSteerable"] = from_bool(self.remote_steerable) return result -class UserMessageAttachmentType(Enum): - BLOB = "blob" - DIRECTORY = "directory" - FILE = "file" - GITHUB_REFERENCE = "github_reference" - SELECTION = "selection" - - @dataclass -class UserMessageAttachment: - """A user message attachment — a file, directory, code selection, blob, or GitHub reference - - File attachment - - Directory attachment - - Code selection attachment from an editor - - GitHub issue, pull request, or discussion reference - - Blob attachment with inline base64-encoded data - """ - type: UserMessageAttachmentType - """Attachment type discriminator""" - - display_name: str | None = None - """User-facing display name for the attachment - - User-facing display name for the selection - """ - line_range: UserMessageAttachmentFileLineRange | None = None - """Optional line range to scope the attachment to a specific section of the file""" - - path: str | None = None - """Absolute file path - - Absolute directory path - """ - file_path: str | None = None - """Absolute path to the file containing the selection""" - - selection: UserMessageAttachmentSelectionDetails | None = None - """Position range of the selection within the file""" - - text: str | None = None - """The selected text content""" - - number: float | None = None - """Issue, pull request, or discussion number""" - - reference_type: UserMessageAttachmentGithubReferenceType | None = None - """Type of GitHub reference""" - - state: str | None = None - """Current state of the referenced item (e.g., open, closed, merged)""" - - title: str | None = None - """Title of the referenced item""" - +class SessionErrorData: + """Error details for timeline display including message and optional diagnostic information""" + error_type: str + message: str + stack: str | None = None + status_code: int | None = None + provider_call_id: str | None = None url: str | None = None - """URL to the referenced item on GitHub""" - - data: str | None = None - """Base64-encoded content""" - - mime_type: str | None = None - """MIME type of the inline data""" @staticmethod - def from_dict(obj: Any) -> 'UserMessageAttachment': + def from_dict(obj: Any) -> "SessionErrorData": assert isinstance(obj, dict) - type = UserMessageAttachmentType(obj.get("type")) - display_name = from_union([from_str, from_none], obj.get("displayName")) - line_range = from_union([UserMessageAttachmentFileLineRange.from_dict, from_none], obj.get("lineRange")) - path = from_union([from_str, from_none], obj.get("path")) - file_path = from_union([from_str, from_none], obj.get("filePath")) - selection = from_union([UserMessageAttachmentSelectionDetails.from_dict, from_none], obj.get("selection")) - text = from_union([from_str, from_none], obj.get("text")) - number = from_union([from_float, from_none], obj.get("number")) - reference_type = from_union([UserMessageAttachmentGithubReferenceType, from_none], obj.get("referenceType")) - state = from_union([from_str, from_none], obj.get("state")) - title = from_union([from_str, from_none], obj.get("title")) - url = from_union([from_str, from_none], obj.get("url")) - data = from_union([from_str, from_none], obj.get("data")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - return UserMessageAttachment(type, display_name, line_range, path, file_path, selection, text, number, reference_type, state, title, url, data, mime_type) + error_type = from_str(obj.get("errorType")) + message = from_str(obj.get("message")) + stack = from_union([from_none, lambda x: from_str(x)], obj.get("stack")) + status_code = from_union([from_none, lambda x: from_int(x)], obj.get("statusCode")) + provider_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("providerCallId")) + url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) + return SessionErrorData( + error_type=error_type, + message=message, + stack=stack, + status_code=status_code, + provider_call_id=provider_call_id, + url=url, + ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(UserMessageAttachmentType, self.type) - if self.display_name is not None: - result["displayName"] = from_union([from_str, from_none], self.display_name) - if self.line_range is not None: - result["lineRange"] = from_union([lambda x: to_class(UserMessageAttachmentFileLineRange, x), from_none], self.line_range) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.file_path is not None: - result["filePath"] = from_union([from_str, from_none], self.file_path) - if self.selection is not None: - result["selection"] = from_union([lambda x: to_class(UserMessageAttachmentSelectionDetails, x), from_none], self.selection) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - if self.number is not None: - result["number"] = from_union([to_float, from_none], self.number) - if self.reference_type is not None: - result["referenceType"] = from_union([lambda x: to_enum(UserMessageAttachmentGithubReferenceType, x), from_none], self.reference_type) - if self.state is not None: - result["state"] = from_union([from_str, from_none], self.state) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) + result["errorType"] = from_str(self.error_type) + result["message"] = from_str(self.message) + if self.stack is not None: + result["stack"] = from_union([from_none, lambda x: from_str(x)], self.stack) + if self.status_code is not None: + result["statusCode"] = from_union([from_none, lambda x: to_int(x)], self.status_code) + if self.provider_call_id is not None: + result["providerCallId"] = from_union([from_none, lambda x: from_str(x)], self.provider_call_id) if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) - if self.data is not None: - result["data"] = from_union([from_str, from_none], self.data) - if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) + result["url"] = from_union([from_none, lambda x: from_str(x)], self.url) return result @dataclass -class ShutdownCodeChanges: - """Aggregate code change metrics for the session""" - - files_modified: list[str] - """List of file paths that were modified during the session""" - - lines_added: float - """Total number of lines added during the session""" - - lines_removed: float - """Total number of lines removed during the session""" +class SessionIdleData: + """Payload indicating the session is idle with no background agents in flight""" + aborted: bool | None = None @staticmethod - def from_dict(obj: Any) -> 'ShutdownCodeChanges': + def from_dict(obj: Any) -> "SessionIdleData": assert isinstance(obj, dict) - files_modified = from_list(from_str, obj.get("filesModified")) - lines_added = from_float(obj.get("linesAdded")) - lines_removed = from_float(obj.get("linesRemoved")) - return ShutdownCodeChanges(files_modified, lines_added, lines_removed) + aborted = from_union([from_none, lambda x: from_bool(x)], obj.get("aborted")) + return SessionIdleData( + aborted=aborted, + ) def to_dict(self) -> dict: result: dict = {} - result["filesModified"] = from_list(from_str, self.files_modified) - result["linesAdded"] = to_float(self.lines_added) - result["linesRemoved"] = to_float(self.lines_removed) + if self.aborted is not None: + result["aborted"] = from_union([from_none, lambda x: from_bool(x)], self.aborted) return result @dataclass -class CommandsChangedCommand: - name: str - description: str | None = None +class SessionTitleChangedData: + """Session title change payload containing the new display title""" + title: str @staticmethod - def from_dict(obj: Any) -> 'CommandsChangedCommand': + def from_dict(obj: Any) -> "SessionTitleChangedData": assert isinstance(obj, dict) - name = from_str(obj.get("name")) - description = from_union([from_str, from_none], obj.get("description")) - return CommandsChangedCommand(name, description) + title = from_str(obj.get("title")) + return SessionTitleChangedData( + title=title, + ) def to_dict(self) -> dict: result: dict = {} - result["name"] = from_str(self.name) - if self.description is not None: - result["description"] = from_union([from_str, from_none], self.description) + result["title"] = from_str(self.title) return result @dataclass -class CompactionCompleteCompactionTokensUsed: - """Token usage breakdown for the compaction LLM call""" - - cached_input: float - """Cached input tokens reused in the compaction LLM call""" - - input: float - """Input tokens consumed by the compaction LLM call""" - - output: float - """Output tokens produced by the compaction LLM call""" +class SessionInfoData: + """Informational message for timeline display with categorization""" + info_type: str + message: str + url: str | None = None @staticmethod - def from_dict(obj: Any) -> 'CompactionCompleteCompactionTokensUsed': + def from_dict(obj: Any) -> "SessionInfoData": assert isinstance(obj, dict) - cached_input = from_float(obj.get("cachedInput")) - input = from_float(obj.get("input")) - output = from_float(obj.get("output")) - return CompactionCompleteCompactionTokensUsed(cached_input, input, output) + info_type = from_str(obj.get("infoType")) + message = from_str(obj.get("message")) + url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) + return SessionInfoData( + info_type=info_type, + message=message, + url=url, + ) def to_dict(self) -> dict: result: dict = {} - result["cachedInput"] = to_float(self.cached_input) - result["input"] = to_float(self.input) - result["output"] = to_float(self.output) + result["infoType"] = from_str(self.info_type) + result["message"] = from_str(self.message) + if self.url is not None: + result["url"] = from_union([from_none, lambda x: from_str(x)], self.url) return result -class ContextChangedHostType(Enum): - """Hosting platform type of the repository (github or ado)""" - - ADO = "ado" - GITHUB = "github" - - @dataclass -class Context: - """Working directory and git context at session start - - Updated working directory and git context at resume time - """ - cwd: str - """Current working directory path""" - - base_commit: str | None = None - """Base commit of current git branch at session start time""" - - branch: str | None = None - """Current git branch name""" - - git_root: str | None = None - """Root directory of the git repository, resolved via git rev-parse""" - - head_commit: str | None = None - """Head commit of current git branch at session start time""" - - host_type: ContextChangedHostType | None = None - """Hosting platform type of the repository (github or ado)""" - - repository: str | None = None - """Repository identifier derived from the git remote URL ("owner/name" for GitHub, - "org/project/repo" for Azure DevOps) - """ +class SessionWarningData: + """Warning message for timeline display with categorization""" + warning_type: str + message: str + url: str | None = None @staticmethod - def from_dict(obj: Any) -> 'Context': + def from_dict(obj: Any) -> "SessionWarningData": assert isinstance(obj, dict) - cwd = from_str(obj.get("cwd")) - base_commit = from_union([from_str, from_none], obj.get("baseCommit")) - branch = from_union([from_str, from_none], obj.get("branch")) - git_root = from_union([from_str, from_none], obj.get("gitRoot")) - head_commit = from_union([from_str, from_none], obj.get("headCommit")) - host_type = from_union([ContextChangedHostType, from_none], obj.get("hostType")) - repository = from_union([from_str, from_none], obj.get("repository")) - return Context(cwd, base_commit, branch, git_root, head_commit, host_type, repository) + warning_type = from_str(obj.get("warningType")) + message = from_str(obj.get("message")) + url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) + return SessionWarningData( + warning_type=warning_type, + message=message, + url=url, + ) def to_dict(self) -> dict: result: dict = {} - result["cwd"] = from_str(self.cwd) - if self.base_commit is not None: - result["baseCommit"] = from_union([from_str, from_none], self.base_commit) - if self.branch is not None: - result["branch"] = from_union([from_str, from_none], self.branch) - if self.git_root is not None: - result["gitRoot"] = from_union([from_str, from_none], self.git_root) - if self.head_commit is not None: - result["headCommit"] = from_union([from_str, from_none], self.head_commit) - if self.host_type is not None: - result["hostType"] = from_union([lambda x: to_enum(ContextChangedHostType, x), from_none], self.host_type) - if self.repository is not None: - result["repository"] = from_union([from_str, from_none], self.repository) + result["warningType"] = from_str(self.warning_type) + result["message"] = from_str(self.message) + if self.url is not None: + result["url"] = from_union([from_none, lambda x: from_str(x)], self.url) return result @dataclass -class AssistantUsageCopilotUsageTokenDetail: - """Token usage detail for a single billing category""" +class SessionModelChangeData: + """Model change details including previous and new model identifiers""" + new_model: str + previous_model: str | None = None + previous_reasoning_effort: str | None = None + reasoning_effort: str | None = None - batch_size: float - """Number of tokens in this billing batch""" + @staticmethod + def from_dict(obj: Any) -> "SessionModelChangeData": + assert isinstance(obj, dict) + new_model = from_str(obj.get("newModel")) + previous_model = from_union([from_none, lambda x: from_str(x)], obj.get("previousModel")) + previous_reasoning_effort = from_union([from_none, lambda x: from_str(x)], obj.get("previousReasoningEffort")) + reasoning_effort = from_union([from_none, lambda x: from_str(x)], obj.get("reasoningEffort")) + return SessionModelChangeData( + new_model=new_model, + previous_model=previous_model, + previous_reasoning_effort=previous_reasoning_effort, + reasoning_effort=reasoning_effort, + ) - cost_per_batch: float - """Cost per batch of tokens""" + def to_dict(self) -> dict: + result: dict = {} + result["newModel"] = from_str(self.new_model) + if self.previous_model is not None: + result["previousModel"] = from_union([from_none, lambda x: from_str(x)], self.previous_model) + if self.previous_reasoning_effort is not None: + result["previousReasoningEffort"] = from_union([from_none, lambda x: from_str(x)], self.previous_reasoning_effort) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_none, lambda x: from_str(x)], self.reasoning_effort) + return result - token_count: float - """Total token count for this entry""" - token_type: str - """Token category (e.g., "input", "output")""" +@dataclass +class SessionModeChangedData: + """Agent mode change details including previous and new modes""" + previous_mode: str + new_mode: str @staticmethod - def from_dict(obj: Any) -> 'AssistantUsageCopilotUsageTokenDetail': + def from_dict(obj: Any) -> "SessionModeChangedData": assert isinstance(obj, dict) - batch_size = from_float(obj.get("batchSize")) - cost_per_batch = from_float(obj.get("costPerBatch")) - token_count = from_float(obj.get("tokenCount")) - token_type = from_str(obj.get("tokenType")) - return AssistantUsageCopilotUsageTokenDetail(batch_size, cost_per_batch, token_count, token_type) + previous_mode = from_str(obj.get("previousMode")) + new_mode = from_str(obj.get("newMode")) + return SessionModeChangedData( + previous_mode=previous_mode, + new_mode=new_mode, + ) def to_dict(self) -> dict: result: dict = {} - result["batchSize"] = to_float(self.batch_size) - result["costPerBatch"] = to_float(self.cost_per_batch) - result["tokenCount"] = to_float(self.token_count) - result["tokenType"] = from_str(self.token_type) + result["previousMode"] = from_str(self.previous_mode) + result["newMode"] = from_str(self.new_mode) return result @dataclass -class AssistantUsageCopilotUsage: - """Per-request cost and usage data from the CAPI copilot_usage response field""" - - token_details: list[AssistantUsageCopilotUsageTokenDetail] - """Itemized token usage breakdown""" - - total_nano_aiu: float - """Total cost in nano-AIU (AI Units) for this request""" +class SessionPlanChangedData: + """Plan file operation details indicating what changed""" + operation: SessionPlanChangedDataOperation @staticmethod - def from_dict(obj: Any) -> 'AssistantUsageCopilotUsage': + def from_dict(obj: Any) -> "SessionPlanChangedData": assert isinstance(obj, dict) - token_details = from_list(AssistantUsageCopilotUsageTokenDetail.from_dict, obj.get("tokenDetails")) - total_nano_aiu = from_float(obj.get("totalNanoAiu")) - return AssistantUsageCopilotUsage(token_details, total_nano_aiu) + operation = parse_enum(SessionPlanChangedDataOperation, obj.get("operation")) + return SessionPlanChangedData( + operation=operation, + ) def to_dict(self) -> dict: result: dict = {} - result["tokenDetails"] = from_list(lambda x: to_class(AssistantUsageCopilotUsageTokenDetail, x), self.token_details) - result["totalNanoAiu"] = to_float(self.total_nano_aiu) + result["operation"] = to_enum(SessionPlanChangedDataOperation, self.operation) return result @dataclass -class Error: - """Error details when the tool execution failed - - Error details when the hook failed - """ - message: str - """Human-readable error message""" - - code: str | None = None - """Machine-readable error code""" - - stack: str | None = None - """Error stack trace, when available""" +class SessionWorkspaceFileChangedData: + """Workspace file change details including path and operation type""" + path: str + operation: SessionWorkspaceFileChangedDataOperation @staticmethod - def from_dict(obj: Any) -> 'Error': + def from_dict(obj: Any) -> "SessionWorkspaceFileChangedData": assert isinstance(obj, dict) - message = from_str(obj.get("message")) - code = from_union([from_str, from_none], obj.get("code")) - stack = from_union([from_str, from_none], obj.get("stack")) - return Error(message, code, stack) + path = from_str(obj.get("path")) + operation = parse_enum(SessionWorkspaceFileChangedDataOperation, obj.get("operation")) + return SessionWorkspaceFileChangedData( + path=path, + operation=operation, + ) def to_dict(self) -> dict: result: dict = {} - result["message"] = from_str(self.message) - if self.code is not None: - result["code"] = from_union([from_str, from_none], self.code) - if self.stack is not None: - result["stack"] = from_union([from_str, from_none], self.stack) + result["path"] = from_str(self.path) + result["operation"] = to_enum(SessionWorkspaceFileChangedDataOperation, self.operation) return result -class ExtensionsLoadedExtensionSource(Enum): - """Discovery source""" - - PROJECT = "project" - USER = "user" - +@dataclass +class SessionImportLegacyDataLegacySessionChatMessagesItemAudio: + id: str -class ExtensionsLoadedExtensionStatus(Enum): - """Current status: running, disabled, failed, or starting""" + @staticmethod + def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemAudio": + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + return SessionImportLegacyDataLegacySessionChatMessagesItemAudio( + id=id, + ) - DISABLED = "disabled" - FAILED = "failed" - RUNNING = "running" - STARTING = "starting" + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + return result @dataclass -class ExtensionsLoadedExtension: - id: str - """Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper')""" - +class SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall: name: str - """Extension name (directory name)""" - - source: ExtensionsLoadedExtensionSource - """Discovery source""" - - status: ExtensionsLoadedExtensionStatus - """Current status: running, disabled, failed, or starting""" + arguments: str @staticmethod - def from_dict(obj: Any) -> 'ExtensionsLoadedExtension': + def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall": assert isinstance(obj, dict) - id = from_str(obj.get("id")) name = from_str(obj.get("name")) - source = ExtensionsLoadedExtensionSource(obj.get("source")) - status = ExtensionsLoadedExtensionStatus(obj.get("status")) - return ExtensionsLoadedExtension(id, name, source, status) + arguments = from_str(obj.get("arguments")) + return SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall( + name=name, + arguments=arguments, + ) def to_dict(self) -> dict: result: dict = {} - result["id"] = from_str(self.id) result["name"] = from_str(self.name) - result["source"] = to_enum(ExtensionsLoadedExtensionSource, self.source) - result["status"] = to_enum(ExtensionsLoadedExtensionStatus, self.status) + result["arguments"] = from_str(self.arguments) return result -class SystemNotificationAgentCompletedStatus(Enum): - """Whether the agent completed successfully or failed""" - - COMPLETED = "completed" - FAILED = "failed" +@dataclass +class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction: + name: str + arguments: str + @staticmethod + def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction": + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + arguments = from_str(obj.get("arguments")) + return SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction( + name=name, + arguments=arguments, + ) -class SystemNotificationType(Enum): - AGENT_COMPLETED = "agent_completed" - AGENT_IDLE = "agent_idle" - SHELL_COMPLETED = "shell_completed" - SHELL_DETACHED_COMPLETED = "shell_detached_completed" + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["arguments"] = from_str(self.arguments) + return result @dataclass -class SystemNotification: - """Structured metadata identifying what triggered this notification""" - - type: SystemNotificationType - agent_id: str | None = None - """Unique identifier of the background agent""" - - agent_type: str | None = None - """Type of the agent (e.g., explore, task, general-purpose)""" +class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom: + name: str + input: str - description: str | None = None - """Human-readable description of the agent task - - Human-readable description of the command - """ - prompt: str | None = None - """The full prompt given to the background agent""" + @staticmethod + def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom": + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + input = from_str(obj.get("input")) + return SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom( + name=name, + input=input, + ) - status: SystemNotificationAgentCompletedStatus | None = None - """Whether the agent completed successfully or failed""" + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["input"] = from_str(self.input) + return result - exit_code: float | None = None - """Exit code of the shell command, if available""" - shell_id: str | None = None - """Unique identifier of the shell session - - Unique identifier of the detached shell session - """ +@dataclass +class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem: + type: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType + id: str + function: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction | None = None + custom: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom | None = None @staticmethod - def from_dict(obj: Any) -> 'SystemNotification': + def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem": assert isinstance(obj, dict) - type = SystemNotificationType(obj.get("type")) - agent_id = from_union([from_str, from_none], obj.get("agentId")) - agent_type = from_union([from_str, from_none], obj.get("agentType")) - description = from_union([from_str, from_none], obj.get("description")) - prompt = from_union([from_str, from_none], obj.get("prompt")) - status = from_union([SystemNotificationAgentCompletedStatus, from_none], obj.get("status")) - exit_code = from_union([from_float, from_none], obj.get("exitCode")) - shell_id = from_union([from_str, from_none], obj.get("shellId")) - return SystemNotification(type, agent_id, agent_type, description, prompt, status, exit_code, shell_id) + type = parse_enum(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType, obj.get("type")) + id = from_str(obj.get("id")) + function = from_union([from_none, lambda x: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction.from_dict(x)], obj.get("function")) + custom = from_union([from_none, lambda x: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom.from_dict(x)], obj.get("custom")) + return SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem( + type=type, + id=id, + function=function, + custom=custom, + ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(SystemNotificationType, self.type) - if self.agent_id is not None: - result["agentId"] = from_union([from_str, from_none], self.agent_id) - if self.agent_type is not None: - result["agentType"] = from_union([from_str, from_none], self.agent_type) - if self.description is not None: - result["description"] = from_union([from_str, from_none], self.description) - if self.prompt is not None: - result["prompt"] = from_union([from_str, from_none], self.prompt) - if self.status is not None: - result["status"] = from_union([lambda x: to_enum(SystemNotificationAgentCompletedStatus, x), from_none], self.status) - if self.exit_code is not None: - result["exitCode"] = from_union([to_float, from_none], self.exit_code) - if self.shell_id is not None: - result["shellId"] = from_union([from_str, from_none], self.shell_id) + result["type"] = to_enum(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType, self.type) + result["id"] = from_str(self.id) + if self.function is not None: + result["function"] = from_union([from_none, lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction, x)], self.function) + if self.custom is not None: + result["custom"] = from_union([from_none, lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom, x)], self.custom) return result @dataclass -class SystemMessageMetadata: - """Metadata about the prompt template and its construction""" - - prompt_version: str | None = None - """Version identifier of the prompt template used""" - - variables: dict[str, Any] | None = None - """Template variables used when constructing the prompt""" +class SessionImportLegacyDataLegacySessionChatMessagesItem: + role: SessionImportLegacyDataLegacySessionChatMessagesItemRole + content: Any = None + name: str | None = None + refusal: str | None = None + audio: SessionImportLegacyDataLegacySessionChatMessagesItemAudio | None = None + function_call: SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall | None = None + tool_calls: list[SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem] | None = None + tool_call_id: str | None = None @staticmethod - def from_dict(obj: Any) -> 'SystemMessageMetadata': + def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItem": assert isinstance(obj, dict) - prompt_version = from_union([from_str, from_none], obj.get("promptVersion")) - variables = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("variables")) - return SystemMessageMetadata(prompt_version, variables) + role = parse_enum(SessionImportLegacyDataLegacySessionChatMessagesItemRole, obj.get("role")) + content = obj.get("content") + name = from_union([from_none, lambda x: from_str(x)], obj.get("name")) + refusal = from_union([from_none, lambda x: from_str(x)], obj.get("refusal")) + audio = from_union([from_none, lambda x: SessionImportLegacyDataLegacySessionChatMessagesItemAudio.from_dict(x)], obj.get("audio")) + function_call = from_union([from_none, lambda x: SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall.from_dict(x)], obj.get("function_call")) + tool_calls = from_union([from_none, lambda x: from_list(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem.from_dict, x)], obj.get("tool_calls")) + tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("tool_call_id")) + return SessionImportLegacyDataLegacySessionChatMessagesItem( + role=role, + content=content, + name=name, + refusal=refusal, + audio=audio, + function_call=function_call, + tool_calls=tool_calls, + tool_call_id=tool_call_id, + ) def to_dict(self) -> dict: result: dict = {} - if self.prompt_version is not None: - result["promptVersion"] = from_union([from_str, from_none], self.prompt_version) - if self.variables is not None: - result["variables"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.variables) + result["role"] = to_enum(SessionImportLegacyDataLegacySessionChatMessagesItemRole, self.role) + if self.content is not None: + result["content"] = self.content + if self.name is not None: + result["name"] = from_union([from_none, lambda x: from_str(x)], self.name) + if self.refusal is not None: + result["refusal"] = from_union([from_none, lambda x: from_str(x)], self.refusal) + if self.audio is not None: + result["audio"] = from_union([from_none, lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemAudio, x)], self.audio) + if self.function_call is not None: + result["function_call"] = from_union([from_none, lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall, x)], self.function_call) + if self.tool_calls is not None: + result["tool_calls"] = from_union([from_none, lambda x: from_list(lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem, x), x)], self.tool_calls) + if self.tool_call_id is not None: + result["tool_call_id"] = from_union([from_none, lambda x: from_str(x)], self.tool_call_id) return result -class ElicitationRequestedMode(Enum): - """Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to - "form" when absent. - """ - FORM = "form" - URL = "url" - - @dataclass -class ShutdownModelMetricRequests: - """Request count and cost metrics""" - - cost: float - """Cumulative cost multiplier for requests to this model""" - - count: float - """Total number of API requests made to this model""" +class SessionImportLegacyDataLegacySession: + session_id: str + start_time: datetime + chat_messages: list[SessionImportLegacyDataLegacySessionChatMessagesItem] + timeline: list[Any] + selected_model: SessionImportLegacyDataLegacySessionSelectedModel | None = None @staticmethod - def from_dict(obj: Any) -> 'ShutdownModelMetricRequests': + def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySession": assert isinstance(obj, dict) - cost = from_float(obj.get("cost")) - count = from_float(obj.get("count")) - return ShutdownModelMetricRequests(cost, count) + session_id = from_str(obj.get("sessionId")) + start_time = from_datetime(obj.get("startTime")) + chat_messages = from_list(SessionImportLegacyDataLegacySessionChatMessagesItem.from_dict, obj.get("chatMessages")) + timeline = from_list(lambda x: x, obj.get("timeline")) + selected_model = from_union([from_none, lambda x: parse_enum(SessionImportLegacyDataLegacySessionSelectedModel, x)], obj.get("selectedModel")) + return SessionImportLegacyDataLegacySession( + session_id=session_id, + start_time=start_time, + chat_messages=chat_messages, + timeline=timeline, + selected_model=selected_model, + ) def to_dict(self) -> dict: result: dict = {} - result["cost"] = to_float(self.cost) - result["count"] = to_float(self.count) + result["sessionId"] = from_str(self.session_id) + result["startTime"] = to_datetime(self.start_time) + result["chatMessages"] = from_list(lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItem, x), self.chat_messages) + result["timeline"] = from_list(lambda x: x, self.timeline) + if self.selected_model is not None: + result["selectedModel"] = from_union([from_none, lambda x: to_enum(SessionImportLegacyDataLegacySessionSelectedModel, x)], self.selected_model) return result @dataclass -class ShutdownModelMetricUsage: - """Token usage breakdown""" - - cache_read_tokens: float - """Total tokens read from prompt cache across all requests""" +class SessionImportLegacyData: + """Legacy session import data including the complete session JSON, import timestamp, and source file path""" + legacy_session: SessionImportLegacyDataLegacySession + import_time: datetime + source_file: str - cache_write_tokens: float - """Total tokens written to prompt cache across all requests""" + @staticmethod + def from_dict(obj: Any) -> "SessionImportLegacyData": + assert isinstance(obj, dict) + legacy_session = SessionImportLegacyDataLegacySession.from_dict(obj.get("legacySession")) + import_time = from_datetime(obj.get("importTime")) + source_file = from_str(obj.get("sourceFile")) + return SessionImportLegacyData( + legacy_session=legacy_session, + import_time=import_time, + source_file=source_file, + ) - input_tokens: float - """Total input tokens consumed across all requests to this model""" + def to_dict(self) -> dict: + result: dict = {} + result["legacySession"] = to_class(SessionImportLegacyDataLegacySession, self.legacy_session) + result["importTime"] = to_datetime(self.import_time) + result["sourceFile"] = from_str(self.source_file) + return result - output_tokens: float - """Total output tokens produced across all requests to this model""" - reasoning_tokens: float | None = None - """Total reasoning tokens produced across all requests to this model""" +@dataclass +class SessionHandoffDataRepository: + """Repository context for the handed-off session""" + owner: str + name: str + branch: str | None = None @staticmethod - def from_dict(obj: Any) -> 'ShutdownModelMetricUsage': + def from_dict(obj: Any) -> "SessionHandoffDataRepository": assert isinstance(obj, dict) - cache_read_tokens = from_float(obj.get("cacheReadTokens")) - cache_write_tokens = from_float(obj.get("cacheWriteTokens")) - input_tokens = from_float(obj.get("inputTokens")) - output_tokens = from_float(obj.get("outputTokens")) - reasoning_tokens = from_union([from_float, from_none], obj.get("reasoningTokens")) - return ShutdownModelMetricUsage(cache_read_tokens, cache_write_tokens, input_tokens, output_tokens, reasoning_tokens) + owner = from_str(obj.get("owner")) + name = from_str(obj.get("name")) + branch = from_union([from_none, lambda x: from_str(x)], obj.get("branch")) + return SessionHandoffDataRepository( + owner=owner, + name=name, + branch=branch, + ) def to_dict(self) -> dict: result: dict = {} - result["cacheReadTokens"] = to_float(self.cache_read_tokens) - result["cacheWriteTokens"] = to_float(self.cache_write_tokens) - result["inputTokens"] = to_float(self.input_tokens) - result["outputTokens"] = to_float(self.output_tokens) - if self.reasoning_tokens is not None: - result["reasoningTokens"] = from_union([to_float, from_none], self.reasoning_tokens) + result["owner"] = from_str(self.owner) + result["name"] = from_str(self.name) + if self.branch is not None: + result["branch"] = from_union([from_none, lambda x: from_str(x)], self.branch) return result @dataclass -class ShutdownModelMetric: - requests: ShutdownModelMetricRequests - """Request count and cost metrics""" - - usage: ShutdownModelMetricUsage - """Token usage breakdown""" +class SessionHandoffData: + """Session handoff metadata including source, context, and repository information""" + handoff_time: datetime + source_type: SessionHandoffDataSourceType + repository: SessionHandoffDataRepository | None = None + context: str | None = None + summary: str | None = None + remote_session_id: str | None = None + host: str | None = None @staticmethod - def from_dict(obj: Any) -> 'ShutdownModelMetric': + def from_dict(obj: Any) -> "SessionHandoffData": assert isinstance(obj, dict) - requests = ShutdownModelMetricRequests.from_dict(obj.get("requests")) - usage = ShutdownModelMetricUsage.from_dict(obj.get("usage")) - return ShutdownModelMetric(requests, usage) + handoff_time = from_datetime(obj.get("handoffTime")) + source_type = parse_enum(SessionHandoffDataSourceType, obj.get("sourceType")) + repository = from_union([from_none, lambda x: SessionHandoffDataRepository.from_dict(x)], obj.get("repository")) + context = from_union([from_none, lambda x: from_str(x)], obj.get("context")) + summary = from_union([from_none, lambda x: from_str(x)], obj.get("summary")) + remote_session_id = from_union([from_none, lambda x: from_str(x)], obj.get("remoteSessionId")) + host = from_union([from_none, lambda x: from_str(x)], obj.get("host")) + return SessionHandoffData( + handoff_time=handoff_time, + source_type=source_type, + repository=repository, + context=context, + summary=summary, + remote_session_id=remote_session_id, + host=host, + ) def to_dict(self) -> dict: result: dict = {} - result["requests"] = to_class(ShutdownModelMetricRequests, self.requests) - result["usage"] = to_class(ShutdownModelMetricUsage, self.usage) + result["handoffTime"] = to_datetime(self.handoff_time) + result["sourceType"] = to_enum(SessionHandoffDataSourceType, self.source_type) + if self.repository is not None: + result["repository"] = from_union([from_none, lambda x: to_class(SessionHandoffDataRepository, x)], self.repository) + if self.context is not None: + result["context"] = from_union([from_none, lambda x: from_str(x)], self.context) + if self.summary is not None: + result["summary"] = from_union([from_none, lambda x: from_str(x)], self.summary) + if self.remote_session_id is not None: + result["remoteSessionId"] = from_union([from_none, lambda x: from_str(x)], self.remote_session_id) + if self.host is not None: + result["host"] = from_union([from_none, lambda x: from_str(x)], self.host) return result -class ChangedOperation(Enum): - """The type of operation performed on the plan file - - Whether the file was newly created or updated - """ - CREATE = "create" - DELETE = "delete" - UPDATE = "update" - +@dataclass +class SessionTruncationData: + """Conversation truncation statistics including token counts and removed content metrics""" + token_limit: float + pre_truncation_tokens_in_messages: float + pre_truncation_messages_length: float + post_truncation_tokens_in_messages: float + post_truncation_messages_length: float + tokens_removed_during_truncation: float + messages_removed_during_truncation: float + performed_by: str -class PermissionRequestMemoryAction(Enum): - """Whether this is a store or vote memory operation""" + @staticmethod + def from_dict(obj: Any) -> "SessionTruncationData": + assert isinstance(obj, dict) + token_limit = from_float(obj.get("tokenLimit")) + pre_truncation_tokens_in_messages = from_float(obj.get("preTruncationTokensInMessages")) + pre_truncation_messages_length = from_float(obj.get("preTruncationMessagesLength")) + post_truncation_tokens_in_messages = from_float(obj.get("postTruncationTokensInMessages")) + post_truncation_messages_length = from_float(obj.get("postTruncationMessagesLength")) + tokens_removed_during_truncation = from_float(obj.get("tokensRemovedDuringTruncation")) + messages_removed_during_truncation = from_float(obj.get("messagesRemovedDuringTruncation")) + performed_by = from_str(obj.get("performedBy")) + return SessionTruncationData( + token_limit=token_limit, + pre_truncation_tokens_in_messages=pre_truncation_tokens_in_messages, + pre_truncation_messages_length=pre_truncation_messages_length, + post_truncation_tokens_in_messages=post_truncation_tokens_in_messages, + post_truncation_messages_length=post_truncation_messages_length, + tokens_removed_during_truncation=tokens_removed_during_truncation, + messages_removed_during_truncation=messages_removed_during_truncation, + performed_by=performed_by, + ) - STORE = "store" - VOTE = "vote" + def to_dict(self) -> dict: + result: dict = {} + result["tokenLimit"] = to_float(self.token_limit) + result["preTruncationTokensInMessages"] = to_float(self.pre_truncation_tokens_in_messages) + result["preTruncationMessagesLength"] = to_float(self.pre_truncation_messages_length) + result["postTruncationTokensInMessages"] = to_float(self.post_truncation_tokens_in_messages) + result["postTruncationMessagesLength"] = to_float(self.post_truncation_messages_length) + result["tokensRemovedDuringTruncation"] = to_float(self.tokens_removed_during_truncation) + result["messagesRemovedDuringTruncation"] = to_float(self.messages_removed_during_truncation) + result["performedBy"] = from_str(self.performed_by) + return result @dataclass -class PermissionRequestShellCommand: - identifier: str - """Command identifier (e.g., executable name)""" - - read_only: bool - """Whether this command is read-only (no side effects)""" +class SessionSnapshotRewindData: + """Session rewind details including target event and count of removed events""" + up_to_event_id: str + events_removed: float @staticmethod - def from_dict(obj: Any) -> 'PermissionRequestShellCommand': + def from_dict(obj: Any) -> "SessionSnapshotRewindData": assert isinstance(obj, dict) - identifier = from_str(obj.get("identifier")) - read_only = from_bool(obj.get("readOnly")) - return PermissionRequestShellCommand(identifier, read_only) + up_to_event_id = from_str(obj.get("upToEventId")) + events_removed = from_float(obj.get("eventsRemoved")) + return SessionSnapshotRewindData( + up_to_event_id=up_to_event_id, + events_removed=events_removed, + ) def to_dict(self) -> dict: result: dict = {} - result["identifier"] = from_str(self.identifier) - result["readOnly"] = from_bool(self.read_only) + result["upToEventId"] = from_str(self.up_to_event_id) + result["eventsRemoved"] = to_float(self.events_removed) return result -class PermissionRequestMemoryDirection(Enum): - """Vote direction (vote only)""" - - DOWNVOTE = "downvote" - UPVOTE = "upvote" +@dataclass +class SessionShutdownDataCodeChanges: + """Aggregate code change metrics for the session""" + lines_added: float + lines_removed: float + files_modified: list[str] + @staticmethod + def from_dict(obj: Any) -> "SessionShutdownDataCodeChanges": + assert isinstance(obj, dict) + lines_added = from_float(obj.get("linesAdded")) + lines_removed = from_float(obj.get("linesRemoved")) + files_modified = from_list(lambda x: from_str(x), obj.get("filesModified")) + return SessionShutdownDataCodeChanges( + lines_added=lines_added, + lines_removed=lines_removed, + files_modified=files_modified, + ) -class Kind(Enum): - CUSTOM_TOOL = "custom-tool" - HOOK = "hook" - MCP = "mcp" - MEMORY = "memory" - READ = "read" - SHELL = "shell" - URL = "url" - WRITE = "write" + def to_dict(self) -> dict: + result: dict = {} + result["linesAdded"] = to_float(self.lines_added) + result["linesRemoved"] = to_float(self.lines_removed) + result["filesModified"] = from_list(lambda x: from_str(x), self.files_modified) + return result @dataclass -class PermissionRequestShellPossibleURL: - url: str - """URL that may be accessed by the command""" +class SessionShutdownDataModelMetricsValueRequests: + """Request count and cost metrics""" + count: float + cost: float @staticmethod - def from_dict(obj: Any) -> 'PermissionRequestShellPossibleURL': + def from_dict(obj: Any) -> "SessionShutdownDataModelMetricsValueRequests": assert isinstance(obj, dict) - url = from_str(obj.get("url")) - return PermissionRequestShellPossibleURL(url) + count = from_float(obj.get("count")) + cost = from_float(obj.get("cost")) + return SessionShutdownDataModelMetricsValueRequests( + count=count, + cost=cost, + ) def to_dict(self) -> dict: result: dict = {} - result["url"] = from_str(self.url) + result["count"] = to_float(self.count) + result["cost"] = to_float(self.cost) return result @dataclass -class PermissionRequest: - """Details of the permission being requested - - Shell command permission request - - File write permission request - - File or directory read permission request - - MCP tool invocation permission request - - URL access permission request - - Memory operation permission request - - Custom tool invocation permission request - - Hook confirmation permission request - """ - kind: Kind - """Permission kind discriminator""" +class SessionShutdownDataModelMetricsValueUsage: + """Token usage breakdown""" + input_tokens: float + output_tokens: float + cache_read_tokens: float + cache_write_tokens: float - can_offer_session_approval: bool | None = None - """Whether the UI can offer session-wide approval for this command pattern""" + @staticmethod + def from_dict(obj: Any) -> "SessionShutdownDataModelMetricsValueUsage": + assert isinstance(obj, dict) + input_tokens = from_float(obj.get("inputTokens")) + output_tokens = from_float(obj.get("outputTokens")) + cache_read_tokens = from_float(obj.get("cacheReadTokens")) + cache_write_tokens = from_float(obj.get("cacheWriteTokens")) + return SessionShutdownDataModelMetricsValueUsage( + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_tokens=cache_read_tokens, + cache_write_tokens=cache_write_tokens, + ) - commands: list[PermissionRequestShellCommand] | None = None - """Parsed command identifiers found in the command text""" + def to_dict(self) -> dict: + result: dict = {} + result["inputTokens"] = to_float(self.input_tokens) + result["outputTokens"] = to_float(self.output_tokens) + result["cacheReadTokens"] = to_float(self.cache_read_tokens) + result["cacheWriteTokens"] = to_float(self.cache_write_tokens) + return result - full_command_text: str | None = None - """The complete shell command text to be executed""" - has_write_file_redirection: bool | None = None - """Whether the command includes a file write redirection (e.g., > or >>)""" +@dataclass +class SessionShutdownDataModelMetricsValue: + requests: SessionShutdownDataModelMetricsValueRequests + usage: SessionShutdownDataModelMetricsValueUsage - intention: str | None = None - """Human-readable description of what the command intends to do - - Human-readable description of the intended file change - - Human-readable description of why the file is being read - - Human-readable description of why the URL is being accessed - """ - possible_paths: list[str] | None = None - """File paths that may be read or written by the command""" + @staticmethod + def from_dict(obj: Any) -> "SessionShutdownDataModelMetricsValue": + assert isinstance(obj, dict) + requests = SessionShutdownDataModelMetricsValueRequests.from_dict(obj.get("requests")) + usage = SessionShutdownDataModelMetricsValueUsage.from_dict(obj.get("usage")) + return SessionShutdownDataModelMetricsValue( + requests=requests, + usage=usage, + ) - possible_urls: list[PermissionRequestShellPossibleURL] | None = None - """URLs that may be accessed by the command""" + def to_dict(self) -> dict: + result: dict = {} + result["requests"] = to_class(SessionShutdownDataModelMetricsValueRequests, self.requests) + result["usage"] = to_class(SessionShutdownDataModelMetricsValueUsage, self.usage) + return result - tool_call_id: str | None = None - """Tool call ID that triggered this permission request""" - warning: str | None = None - """Optional warning message about risks of running this command""" +@dataclass +class SessionShutdownData: + """Session termination metrics including usage statistics, code changes, and shutdown reason""" + shutdown_type: SessionShutdownDataShutdownType + total_premium_requests: float + total_api_duration_ms: float + session_start_time: float + code_changes: SessionShutdownDataCodeChanges + model_metrics: dict[str, SessionShutdownDataModelMetricsValue] + error_reason: str | None = None + current_model: str | None = None + current_tokens: float | None = None + system_tokens: float | None = None + conversation_tokens: float | None = None + tool_definitions_tokens: float | None = None - diff: str | None = None - """Unified diff showing the proposed changes""" + @staticmethod + def from_dict(obj: Any) -> "SessionShutdownData": + assert isinstance(obj, dict) + shutdown_type = parse_enum(SessionShutdownDataShutdownType, obj.get("shutdownType")) + total_premium_requests = from_float(obj.get("totalPremiumRequests")) + total_api_duration_ms = from_float(obj.get("totalApiDurationMs")) + session_start_time = from_float(obj.get("sessionStartTime")) + code_changes = SessionShutdownDataCodeChanges.from_dict(obj.get("codeChanges")) + model_metrics = from_dict(lambda x: SessionShutdownDataModelMetricsValue.from_dict(x), obj.get("modelMetrics")) + error_reason = from_union([from_none, lambda x: from_str(x)], obj.get("errorReason")) + current_model = from_union([from_none, lambda x: from_str(x)], obj.get("currentModel")) + current_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("currentTokens")) + system_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("systemTokens")) + conversation_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("conversationTokens")) + tool_definitions_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("toolDefinitionsTokens")) + return SessionShutdownData( + shutdown_type=shutdown_type, + total_premium_requests=total_premium_requests, + total_api_duration_ms=total_api_duration_ms, + session_start_time=session_start_time, + code_changes=code_changes, + model_metrics=model_metrics, + error_reason=error_reason, + current_model=current_model, + current_tokens=current_tokens, + system_tokens=system_tokens, + conversation_tokens=conversation_tokens, + tool_definitions_tokens=tool_definitions_tokens, + ) - file_name: str | None = None - """Path of the file being written to""" + def to_dict(self) -> dict: + result: dict = {} + result["shutdownType"] = to_enum(SessionShutdownDataShutdownType, self.shutdown_type) + result["totalPremiumRequests"] = to_float(self.total_premium_requests) + result["totalApiDurationMs"] = to_float(self.total_api_duration_ms) + result["sessionStartTime"] = to_float(self.session_start_time) + result["codeChanges"] = to_class(SessionShutdownDataCodeChanges, self.code_changes) + result["modelMetrics"] = from_dict(lambda x: to_class(SessionShutdownDataModelMetricsValue, x), self.model_metrics) + if self.error_reason is not None: + result["errorReason"] = from_union([from_none, lambda x: from_str(x)], self.error_reason) + if self.current_model is not None: + result["currentModel"] = from_union([from_none, lambda x: from_str(x)], self.current_model) + if self.current_tokens is not None: + result["currentTokens"] = from_union([from_none, lambda x: to_float(x)], self.current_tokens) + if self.system_tokens is not None: + result["systemTokens"] = from_union([from_none, lambda x: to_float(x)], self.system_tokens) + if self.conversation_tokens is not None: + result["conversationTokens"] = from_union([from_none, lambda x: to_float(x)], self.conversation_tokens) + if self.tool_definitions_tokens is not None: + result["toolDefinitionsTokens"] = from_union([from_none, lambda x: to_float(x)], self.tool_definitions_tokens) + return result - new_file_contents: str | None = None - """Complete new file contents for newly created files""" - path: str | None = None - """Path of the file or directory being read""" +@dataclass +class SessionContextChangedData: + """Updated working directory and git context after the change""" + cwd: str + git_root: str | None = None + repository: str | None = None + host_type: SessionStartDataContextHostType | None = None + branch: str | None = None + head_commit: str | None = None + base_commit: str | None = None - args: Any = None - """Arguments to pass to the MCP tool - - Arguments to pass to the custom tool - """ - read_only: bool | None = None - """Whether this MCP tool is read-only (no side effects)""" + @staticmethod + def from_dict(obj: Any) -> "SessionContextChangedData": + assert isinstance(obj, dict) + cwd = from_str(obj.get("cwd")) + git_root = from_union([from_none, lambda x: from_str(x)], obj.get("gitRoot")) + repository = from_union([from_none, lambda x: from_str(x)], obj.get("repository")) + host_type = from_union([from_none, lambda x: parse_enum(SessionStartDataContextHostType, x)], obj.get("hostType")) + branch = from_union([from_none, lambda x: from_str(x)], obj.get("branch")) + head_commit = from_union([from_none, lambda x: from_str(x)], obj.get("headCommit")) + base_commit = from_union([from_none, lambda x: from_str(x)], obj.get("baseCommit")) + return SessionContextChangedData( + cwd=cwd, + git_root=git_root, + repository=repository, + host_type=host_type, + branch=branch, + head_commit=head_commit, + base_commit=base_commit, + ) - server_name: str | None = None - """Name of the MCP server providing the tool""" + def to_dict(self) -> dict: + result: dict = {} + result["cwd"] = from_str(self.cwd) + if self.git_root is not None: + result["gitRoot"] = from_union([from_none, lambda x: from_str(x)], self.git_root) + if self.repository is not None: + result["repository"] = from_union([from_none, lambda x: from_str(x)], self.repository) + if self.host_type is not None: + result["hostType"] = from_union([from_none, lambda x: to_enum(SessionStartDataContextHostType, x)], self.host_type) + if self.branch is not None: + result["branch"] = from_union([from_none, lambda x: from_str(x)], self.branch) + if self.head_commit is not None: + result["headCommit"] = from_union([from_none, lambda x: from_str(x)], self.head_commit) + if self.base_commit is not None: + result["baseCommit"] = from_union([from_none, lambda x: from_str(x)], self.base_commit) + return result - tool_name: str | None = None - """Internal name of the MCP tool - - Name of the custom tool - - Name of the tool the hook is gating - """ - tool_title: str | None = None - """Human-readable title of the MCP tool""" - url: str | None = None - """URL to be fetched""" +@dataclass +class SessionUsageInfoData: + """Current context window usage statistics including token and message counts""" + token_limit: float + current_tokens: float + messages_length: float + system_tokens: float | None = None + conversation_tokens: float | None = None + tool_definitions_tokens: float | None = None + is_initial: bool | None = None - action: PermissionRequestMemoryAction | None = None - """Whether this is a store or vote memory operation""" + @staticmethod + def from_dict(obj: Any) -> "SessionUsageInfoData": + assert isinstance(obj, dict) + token_limit = from_float(obj.get("tokenLimit")) + current_tokens = from_float(obj.get("currentTokens")) + messages_length = from_float(obj.get("messagesLength")) + system_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("systemTokens")) + conversation_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("conversationTokens")) + tool_definitions_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("toolDefinitionsTokens")) + is_initial = from_union([from_none, lambda x: from_bool(x)], obj.get("isInitial")) + return SessionUsageInfoData( + token_limit=token_limit, + current_tokens=current_tokens, + messages_length=messages_length, + system_tokens=system_tokens, + conversation_tokens=conversation_tokens, + tool_definitions_tokens=tool_definitions_tokens, + is_initial=is_initial, + ) - citations: str | None = None - """Source references for the stored fact (store only)""" + def to_dict(self) -> dict: + result: dict = {} + result["tokenLimit"] = to_float(self.token_limit) + result["currentTokens"] = to_float(self.current_tokens) + result["messagesLength"] = to_float(self.messages_length) + if self.system_tokens is not None: + result["systemTokens"] = from_union([from_none, lambda x: to_float(x)], self.system_tokens) + if self.conversation_tokens is not None: + result["conversationTokens"] = from_union([from_none, lambda x: to_float(x)], self.conversation_tokens) + if self.tool_definitions_tokens is not None: + result["toolDefinitionsTokens"] = from_union([from_none, lambda x: to_float(x)], self.tool_definitions_tokens) + if self.is_initial is not None: + result["isInitial"] = from_union([from_none, lambda x: from_bool(x)], self.is_initial) + return result - direction: PermissionRequestMemoryDirection | None = None - """Vote direction (vote only)""" - fact: str | None = None - """The fact being stored or voted on""" +@dataclass +class SessionCompactionStartData: + """Context window breakdown at the start of LLM-powered conversation compaction""" + system_tokens: float | None = None + conversation_tokens: float | None = None + tool_definitions_tokens: float | None = None - reason: str | None = None - """Reason for the vote (vote only)""" + @staticmethod + def from_dict(obj: Any) -> "SessionCompactionStartData": + assert isinstance(obj, dict) + system_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("systemTokens")) + conversation_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("conversationTokens")) + tool_definitions_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("toolDefinitionsTokens")) + return SessionCompactionStartData( + system_tokens=system_tokens, + conversation_tokens=conversation_tokens, + tool_definitions_tokens=tool_definitions_tokens, + ) - subject: str | None = None - """Topic or subject of the memory (store only)""" + def to_dict(self) -> dict: + result: dict = {} + if self.system_tokens is not None: + result["systemTokens"] = from_union([from_none, lambda x: to_float(x)], self.system_tokens) + if self.conversation_tokens is not None: + result["conversationTokens"] = from_union([from_none, lambda x: to_float(x)], self.conversation_tokens) + if self.tool_definitions_tokens is not None: + result["toolDefinitionsTokens"] = from_union([from_none, lambda x: to_float(x)], self.tool_definitions_tokens) + return result - tool_description: str | None = None - """Description of what the custom tool does""" - hook_message: str | None = None - """Optional message from the hook explaining why confirmation is needed""" +@dataclass +class SessionCompactionCompleteDataCompactionTokensUsed: + """Token usage breakdown for the compaction LLM call""" + input: float + output: float + cached_input: float - tool_args: Any = None - """Arguments of the tool call being gated""" - - @staticmethod - def from_dict(obj: Any) -> 'PermissionRequest': - assert isinstance(obj, dict) - kind = Kind(obj.get("kind")) - can_offer_session_approval = from_union([from_bool, from_none], obj.get("canOfferSessionApproval")) - commands = from_union([lambda x: from_list(PermissionRequestShellCommand.from_dict, x), from_none], obj.get("commands")) - full_command_text = from_union([from_str, from_none], obj.get("fullCommandText")) - has_write_file_redirection = from_union([from_bool, from_none], obj.get("hasWriteFileRedirection")) - intention = from_union([from_str, from_none], obj.get("intention")) - possible_paths = from_union([lambda x: from_list(from_str, x), from_none], obj.get("possiblePaths")) - possible_urls = from_union([lambda x: from_list(PermissionRequestShellPossibleURL.from_dict, x), from_none], obj.get("possibleUrls")) - tool_call_id = from_union([from_str, from_none], obj.get("toolCallId")) - warning = from_union([from_str, from_none], obj.get("warning")) - diff = from_union([from_str, from_none], obj.get("diff")) - file_name = from_union([from_str, from_none], obj.get("fileName")) - new_file_contents = from_union([from_str, from_none], obj.get("newFileContents")) - path = from_union([from_str, from_none], obj.get("path")) - args = obj.get("args") - read_only = from_union([from_bool, from_none], obj.get("readOnly")) - server_name = from_union([from_str, from_none], obj.get("serverName")) - tool_name = from_union([from_str, from_none], obj.get("toolName")) - tool_title = from_union([from_str, from_none], obj.get("toolTitle")) - url = from_union([from_str, from_none], obj.get("url")) - action = from_union([PermissionRequestMemoryAction, from_none], obj.get("action")) - citations = from_union([from_str, from_none], obj.get("citations")) - direction = from_union([PermissionRequestMemoryDirection, from_none], obj.get("direction")) - fact = from_union([from_str, from_none], obj.get("fact")) - reason = from_union([from_str, from_none], obj.get("reason")) - subject = from_union([from_str, from_none], obj.get("subject")) - tool_description = from_union([from_str, from_none], obj.get("toolDescription")) - hook_message = from_union([from_str, from_none], obj.get("hookMessage")) - tool_args = obj.get("toolArgs") - return PermissionRequest(kind, can_offer_session_approval, commands, full_command_text, has_write_file_redirection, intention, possible_paths, possible_urls, tool_call_id, warning, diff, file_name, new_file_contents, path, args, read_only, server_name, tool_name, tool_title, url, action, citations, direction, fact, reason, subject, tool_description, hook_message, tool_args) + @staticmethod + def from_dict(obj: Any) -> "SessionCompactionCompleteDataCompactionTokensUsed": + assert isinstance(obj, dict) + input = from_float(obj.get("input")) + output = from_float(obj.get("output")) + cached_input = from_float(obj.get("cachedInput")) + return SessionCompactionCompleteDataCompactionTokensUsed( + input=input, + output=output, + cached_input=cached_input, + ) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(Kind, self.kind) - if self.can_offer_session_approval is not None: - result["canOfferSessionApproval"] = from_union([from_bool, from_none], self.can_offer_session_approval) - if self.commands is not None: - result["commands"] = from_union([lambda x: from_list(lambda x: to_class(PermissionRequestShellCommand, x), x), from_none], self.commands) - if self.full_command_text is not None: - result["fullCommandText"] = from_union([from_str, from_none], self.full_command_text) - if self.has_write_file_redirection is not None: - result["hasWriteFileRedirection"] = from_union([from_bool, from_none], self.has_write_file_redirection) - if self.intention is not None: - result["intention"] = from_union([from_str, from_none], self.intention) - if self.possible_paths is not None: - result["possiblePaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.possible_paths) - if self.possible_urls is not None: - result["possibleUrls"] = from_union([lambda x: from_list(lambda x: to_class(PermissionRequestShellPossibleURL, x), x), from_none], self.possible_urls) - if self.tool_call_id is not None: - result["toolCallId"] = from_union([from_str, from_none], self.tool_call_id) - if self.warning is not None: - result["warning"] = from_union([from_str, from_none], self.warning) - if self.diff is not None: - result["diff"] = from_union([from_str, from_none], self.diff) - if self.file_name is not None: - result["fileName"] = from_union([from_str, from_none], self.file_name) - if self.new_file_contents is not None: - result["newFileContents"] = from_union([from_str, from_none], self.new_file_contents) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.args is not None: - result["args"] = self.args - if self.read_only is not None: - result["readOnly"] = from_union([from_bool, from_none], self.read_only) - if self.server_name is not None: - result["serverName"] = from_union([from_str, from_none], self.server_name) - if self.tool_name is not None: - result["toolName"] = from_union([from_str, from_none], self.tool_name) - if self.tool_title is not None: - result["toolTitle"] = from_union([from_str, from_none], self.tool_title) - if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) - if self.action is not None: - result["action"] = from_union([lambda x: to_enum(PermissionRequestMemoryAction, x), from_none], self.action) - if self.citations is not None: - result["citations"] = from_union([from_str, from_none], self.citations) - if self.direction is not None: - result["direction"] = from_union([lambda x: to_enum(PermissionRequestMemoryDirection, x), from_none], self.direction) - if self.fact is not None: - result["fact"] = from_union([from_str, from_none], self.fact) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.subject is not None: - result["subject"] = from_union([from_str, from_none], self.subject) - if self.tool_description is not None: - result["toolDescription"] = from_union([from_str, from_none], self.tool_description) - if self.hook_message is not None: - result["hookMessage"] = from_union([from_str, from_none], self.hook_message) - if self.tool_args is not None: - result["toolArgs"] = self.tool_args + result["input"] = to_float(self.input) + result["output"] = to_float(self.output) + result["cachedInput"] = to_float(self.cached_input) return result @dataclass -class AssistantUsageQuotaSnapshot: - entitlement_requests: float - """Total requests allowed by the entitlement""" - - is_unlimited_entitlement: bool - """Whether the user has an unlimited usage entitlement""" - - overage: float - """Number of requests over the entitlement limit""" - - overage_allowed_with_exhausted_quota: bool - """Whether overage is allowed when quota is exhausted""" - - remaining_percentage: float - """Percentage of quota remaining (0.0 to 1.0)""" - - usage_allowed_with_exhausted_quota: bool - """Whether usage is still permitted after quota exhaustion""" - - used_requests: float - """Number of requests already consumed""" - - reset_date: datetime | None = None - """Date when the quota resets""" +class SessionCompactionCompleteData: + """Conversation compaction results including success status, metrics, and optional error details""" + success: bool + error: str | None = None + pre_compaction_tokens: float | None = None + post_compaction_tokens: float | None = None + pre_compaction_messages_length: float | None = None + messages_removed: float | None = None + tokens_removed: float | None = None + summary_content: str | None = None + checkpoint_number: float | None = None + checkpoint_path: str | None = None + compaction_tokens_used: SessionCompactionCompleteDataCompactionTokensUsed | None = None + request_id: str | None = None + system_tokens: float | None = None + conversation_tokens: float | None = None + tool_definitions_tokens: float | None = None @staticmethod - def from_dict(obj: Any) -> 'AssistantUsageQuotaSnapshot': + def from_dict(obj: Any) -> "SessionCompactionCompleteData": assert isinstance(obj, dict) - entitlement_requests = from_float(obj.get("entitlementRequests")) - is_unlimited_entitlement = from_bool(obj.get("isUnlimitedEntitlement")) - overage = from_float(obj.get("overage")) - overage_allowed_with_exhausted_quota = from_bool(obj.get("overageAllowedWithExhaustedQuota")) - remaining_percentage = from_float(obj.get("remainingPercentage")) - usage_allowed_with_exhausted_quota = from_bool(obj.get("usageAllowedWithExhaustedQuota")) - used_requests = from_float(obj.get("usedRequests")) - reset_date = from_union([from_datetime, from_none], obj.get("resetDate")) - return AssistantUsageQuotaSnapshot(entitlement_requests, is_unlimited_entitlement, overage, overage_allowed_with_exhausted_quota, remaining_percentage, usage_allowed_with_exhausted_quota, used_requests, reset_date) + success = from_bool(obj.get("success")) + error = from_union([from_none, lambda x: from_str(x)], obj.get("error")) + pre_compaction_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("preCompactionTokens")) + post_compaction_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("postCompactionTokens")) + pre_compaction_messages_length = from_union([from_none, lambda x: from_float(x)], obj.get("preCompactionMessagesLength")) + messages_removed = from_union([from_none, lambda x: from_float(x)], obj.get("messagesRemoved")) + tokens_removed = from_union([from_none, lambda x: from_float(x)], obj.get("tokensRemoved")) + summary_content = from_union([from_none, lambda x: from_str(x)], obj.get("summaryContent")) + checkpoint_number = from_union([from_none, lambda x: from_float(x)], obj.get("checkpointNumber")) + checkpoint_path = from_union([from_none, lambda x: from_str(x)], obj.get("checkpointPath")) + compaction_tokens_used = from_union([from_none, lambda x: SessionCompactionCompleteDataCompactionTokensUsed.from_dict(x)], obj.get("compactionTokensUsed")) + request_id = from_union([from_none, lambda x: from_str(x)], obj.get("requestId")) + system_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("systemTokens")) + conversation_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("conversationTokens")) + tool_definitions_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("toolDefinitionsTokens")) + return SessionCompactionCompleteData( + success=success, + error=error, + pre_compaction_tokens=pre_compaction_tokens, + post_compaction_tokens=post_compaction_tokens, + pre_compaction_messages_length=pre_compaction_messages_length, + messages_removed=messages_removed, + tokens_removed=tokens_removed, + summary_content=summary_content, + checkpoint_number=checkpoint_number, + checkpoint_path=checkpoint_path, + compaction_tokens_used=compaction_tokens_used, + request_id=request_id, + system_tokens=system_tokens, + conversation_tokens=conversation_tokens, + tool_definitions_tokens=tool_definitions_tokens, + ) def to_dict(self) -> dict: result: dict = {} - result["entitlementRequests"] = to_float(self.entitlement_requests) - result["isUnlimitedEntitlement"] = from_bool(self.is_unlimited_entitlement) - result["overage"] = to_float(self.overage) - result["overageAllowedWithExhaustedQuota"] = from_bool(self.overage_allowed_with_exhausted_quota) - result["remainingPercentage"] = to_float(self.remaining_percentage) - result["usageAllowedWithExhaustedQuota"] = from_bool(self.usage_allowed_with_exhausted_quota) - result["usedRequests"] = to_float(self.used_requests) - if self.reset_date is not None: - result["resetDate"] = from_union([lambda x: x.isoformat(), from_none], self.reset_date) + result["success"] = from_bool(self.success) + if self.error is not None: + result["error"] = from_union([from_none, lambda x: from_str(x)], self.error) + if self.pre_compaction_tokens is not None: + result["preCompactionTokens"] = from_union([from_none, lambda x: to_float(x)], self.pre_compaction_tokens) + if self.post_compaction_tokens is not None: + result["postCompactionTokens"] = from_union([from_none, lambda x: to_float(x)], self.post_compaction_tokens) + if self.pre_compaction_messages_length is not None: + result["preCompactionMessagesLength"] = from_union([from_none, lambda x: to_float(x)], self.pre_compaction_messages_length) + if self.messages_removed is not None: + result["messagesRemoved"] = from_union([from_none, lambda x: to_float(x)], self.messages_removed) + if self.tokens_removed is not None: + result["tokensRemoved"] = from_union([from_none, lambda x: to_float(x)], self.tokens_removed) + if self.summary_content is not None: + result["summaryContent"] = from_union([from_none, lambda x: from_str(x)], self.summary_content) + if self.checkpoint_number is not None: + result["checkpointNumber"] = from_union([from_none, lambda x: to_float(x)], self.checkpoint_number) + if self.checkpoint_path is not None: + result["checkpointPath"] = from_union([from_none, lambda x: from_str(x)], self.checkpoint_path) + if self.compaction_tokens_used is not None: + result["compactionTokensUsed"] = from_union([from_none, lambda x: to_class(SessionCompactionCompleteDataCompactionTokensUsed, x)], self.compaction_tokens_used) + if self.request_id is not None: + result["requestId"] = from_union([from_none, lambda x: from_str(x)], self.request_id) + if self.system_tokens is not None: + result["systemTokens"] = from_union([from_none, lambda x: to_float(x)], self.system_tokens) + if self.conversation_tokens is not None: + result["conversationTokens"] = from_union([from_none, lambda x: to_float(x)], self.conversation_tokens) + if self.tool_definitions_tokens is not None: + result["toolDefinitionsTokens"] = from_union([from_none, lambda x: to_float(x)], self.tool_definitions_tokens) return result @dataclass -class HandoffRepository: - """Repository context for the handed-off session""" - - name: str - """Repository name""" - - owner: str - """Repository owner (user or organization)""" - - branch: str | None = None - """Git branch name, if applicable""" +class SessionTaskCompleteData: + """Task completion notification with summary from the agent""" + summary: str | None = None + success: bool | None = None @staticmethod - def from_dict(obj: Any) -> 'HandoffRepository': + def from_dict(obj: Any) -> "SessionTaskCompleteData": assert isinstance(obj, dict) - name = from_str(obj.get("name")) - owner = from_str(obj.get("owner")) - branch = from_union([from_str, from_none], obj.get("branch")) - return HandoffRepository(name, owner, branch) + summary = from_union([from_none, lambda x: from_str(x)], obj.get("summary")) + success = from_union([from_none, lambda x: from_bool(x)], obj.get("success")) + return SessionTaskCompleteData( + summary=summary, + success=success, + ) def to_dict(self) -> dict: result: dict = {} - result["name"] = from_str(self.name) - result["owner"] = from_str(self.owner) - if self.branch is not None: - result["branch"] = from_union([from_str, from_none], self.branch) + if self.summary is not None: + result["summary"] = from_union([from_none, lambda x: from_str(x)], self.summary) + if self.success is not None: + result["success"] = from_union([from_none, lambda x: from_bool(x)], self.success) return result -class RequestedSchemaType(Enum): - OBJECT = "object" - - @dataclass -class ElicitationRequestedSchema: - """JSON Schema describing the form fields to present to the user (form mode only)""" - - properties: dict[str, Any] - """Form field definitions, keyed by field name""" - - type: RequestedSchemaType - """Schema type indicator (always 'object')""" - - required: list[str] | None = None - """List of required field names""" +class UserMessageDataAttachmentsItemLineRange: + """Optional line range to scope the attachment to a specific section of the file""" + start: float + end: float @staticmethod - def from_dict(obj: Any) -> 'ElicitationRequestedSchema': + def from_dict(obj: Any) -> "UserMessageDataAttachmentsItemLineRange": assert isinstance(obj, dict) - properties = from_dict(lambda x: x, obj.get("properties")) - type = RequestedSchemaType(obj.get("type")) - required = from_union([lambda x: from_list(from_str, x), from_none], obj.get("required")) - return ElicitationRequestedSchema(properties, type, required) + start = from_float(obj.get("start")) + end = from_float(obj.get("end")) + return UserMessageDataAttachmentsItemLineRange( + start=start, + end=end, + ) def to_dict(self) -> dict: result: dict = {} - result["properties"] = from_dict(lambda x: x, self.properties) - result["type"] = to_enum(RequestedSchemaType, self.type) - if self.required is not None: - result["required"] = from_union([lambda x: from_list(from_str, x), from_none], self.required) + result["start"] = to_float(self.start) + result["end"] = to_float(self.end) return result -class ToolExecutionCompleteContentResourceLinkIconTheme(Enum): - """Theme variant this icon is intended for""" - - DARK = "dark" - LIGHT = "light" - - @dataclass -class ToolExecutionCompleteContentResourceLinkIcon: - """Icon image for a resource""" +class UserMessageDataAttachmentsItemSelectionStart: + """Start position of the selection""" + line: float + character: float - src: str - """URL or path to the icon image""" + @staticmethod + def from_dict(obj: Any) -> "UserMessageDataAttachmentsItemSelectionStart": + assert isinstance(obj, dict) + line = from_float(obj.get("line")) + character = from_float(obj.get("character")) + return UserMessageDataAttachmentsItemSelectionStart( + line=line, + character=character, + ) - mime_type: str | None = None - """MIME type of the icon image""" + def to_dict(self) -> dict: + result: dict = {} + result["line"] = to_float(self.line) + result["character"] = to_float(self.character) + return result - sizes: list[str] | None = None - """Available icon sizes (e.g., ['16x16', '32x32'])""" - theme: ToolExecutionCompleteContentResourceLinkIconTheme | None = None - """Theme variant this icon is intended for""" +@dataclass +class UserMessageDataAttachmentsItemSelectionEnd: + """End position of the selection""" + line: float + character: float @staticmethod - def from_dict(obj: Any) -> 'ToolExecutionCompleteContentResourceLinkIcon': + def from_dict(obj: Any) -> "UserMessageDataAttachmentsItemSelectionEnd": assert isinstance(obj, dict) - src = from_str(obj.get("src")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - sizes = from_union([lambda x: from_list(from_str, x), from_none], obj.get("sizes")) - theme = from_union([ToolExecutionCompleteContentResourceLinkIconTheme, from_none], obj.get("theme")) - return ToolExecutionCompleteContentResourceLinkIcon(src, mime_type, sizes, theme) + line = from_float(obj.get("line")) + character = from_float(obj.get("character")) + return UserMessageDataAttachmentsItemSelectionEnd( + line=line, + character=character, + ) def to_dict(self) -> dict: result: dict = {} - result["src"] = from_str(self.src) - if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) - if self.sizes is not None: - result["sizes"] = from_union([lambda x: from_list(from_str, x), from_none], self.sizes) - if self.theme is not None: - result["theme"] = from_union([lambda x: to_enum(ToolExecutionCompleteContentResourceLinkIconTheme, x), from_none], self.theme) + result["line"] = to_float(self.line) + result["character"] = to_float(self.character) return result @dataclass -class ToolExecutionCompleteContentResourceDetails: - """The embedded resource contents, either text or base64-encoded binary""" - - uri: str - """URI identifying the resource""" - - mime_type: str | None = None - """MIME type of the text content - - MIME type of the blob content - """ - text: str | None = None - """Text content of the resource""" - - blob: str | None = None - """Base64-encoded binary content of the resource""" +class UserMessageDataAttachmentsItemSelection: + """Position range of the selection within the file""" + start: UserMessageDataAttachmentsItemSelectionStart + end: UserMessageDataAttachmentsItemSelectionEnd @staticmethod - def from_dict(obj: Any) -> 'ToolExecutionCompleteContentResourceDetails': + def from_dict(obj: Any) -> "UserMessageDataAttachmentsItemSelection": assert isinstance(obj, dict) - uri = from_str(obj.get("uri")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - text = from_union([from_str, from_none], obj.get("text")) - blob = from_union([from_str, from_none], obj.get("blob")) - return ToolExecutionCompleteContentResourceDetails(uri, mime_type, text, blob) + start = UserMessageDataAttachmentsItemSelectionStart.from_dict(obj.get("start")) + end = UserMessageDataAttachmentsItemSelectionEnd.from_dict(obj.get("end")) + return UserMessageDataAttachmentsItemSelection( + start=start, + end=end, + ) def to_dict(self) -> dict: result: dict = {} - result["uri"] = from_str(self.uri) - if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) - if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - if self.blob is not None: - result["blob"] = from_union([from_str, from_none], self.blob) + result["start"] = to_class(UserMessageDataAttachmentsItemSelectionStart, self.start) + result["end"] = to_class(UserMessageDataAttachmentsItemSelectionEnd, self.end) return result -class ToolExecutionCompleteContentType(Enum): - AUDIO = "audio" - IMAGE = "image" - RESOURCE = "resource" - RESOURCE_LINK = "resource_link" - TERMINAL = "terminal" - TEXT = "text" - - @dataclass -class ToolExecutionCompleteContent: - """A content block within a tool result, which may be text, terminal output, image, audio, - or a resource - - Plain text content block - - Terminal/shell output content block with optional exit code and working directory - - Image content block with base64-encoded data - - Audio content block with base64-encoded data - - Resource link content block referencing an external resource - - Embedded resource content block with inline text or binary data - """ - type: ToolExecutionCompleteContentType - """Content block type discriminator""" - +class UserMessageDataAttachmentsItem: + """A user message attachment — a file, directory, code selection, blob, or GitHub reference""" + type: UserMessageDataAttachmentsItemType + path: str | None = None + display_name: str | None = None + line_range: UserMessageDataAttachmentsItemLineRange | None = None + file_path: str | None = None text: str | None = None - """The text content - - Terminal/shell output text - """ - cwd: str | None = None - """Working directory where the command was executed""" - - exit_code: float | None = None - """Process exit code, if the command has completed""" - + selection: UserMessageDataAttachmentsItemSelection | None = None + number: float | None = None + title: str | None = None + reference_type: UserMessageDataAttachmentsItemReferenceType | None = None + state: str | None = None + url: str | None = None data: str | None = None - """Base64-encoded image data - - Base64-encoded audio data - """ mime_type: str | None = None - """MIME type of the image (e.g., image/png, image/jpeg) - - MIME type of the audio (e.g., audio/wav, audio/mpeg) - - MIME type of the resource content - """ - description: str | None = None - """Human-readable description of the resource""" - - icons: list[ToolExecutionCompleteContentResourceLinkIcon] | None = None - """Icons associated with this resource""" - - name: str | None = None - """Resource name identifier""" - - size: float | None = None - """Size of the resource in bytes""" - - title: str | None = None - """Human-readable display title for the resource""" - - uri: str | None = None - """URI identifying the resource""" - - resource: ToolExecutionCompleteContentResourceDetails | None = None - """The embedded resource contents, either text or base64-encoded binary""" @staticmethod - def from_dict(obj: Any) -> 'ToolExecutionCompleteContent': + def from_dict(obj: Any) -> "UserMessageDataAttachmentsItem": assert isinstance(obj, dict) - type = ToolExecutionCompleteContentType(obj.get("type")) - text = from_union([from_str, from_none], obj.get("text")) - cwd = from_union([from_str, from_none], obj.get("cwd")) - exit_code = from_union([from_float, from_none], obj.get("exitCode")) - data = from_union([from_str, from_none], obj.get("data")) - mime_type = from_union([from_str, from_none], obj.get("mimeType")) - description = from_union([from_str, from_none], obj.get("description")) - icons = from_union([lambda x: from_list(ToolExecutionCompleteContentResourceLinkIcon.from_dict, x), from_none], obj.get("icons")) - name = from_union([from_str, from_none], obj.get("name")) - size = from_union([from_float, from_none], obj.get("size")) - title = from_union([from_str, from_none], obj.get("title")) - uri = from_union([from_str, from_none], obj.get("uri")) - resource = from_union([ToolExecutionCompleteContentResourceDetails.from_dict, from_none], obj.get("resource")) - return ToolExecutionCompleteContent(type, text, cwd, exit_code, data, mime_type, description, icons, name, size, title, uri, resource) + type = parse_enum(UserMessageDataAttachmentsItemType, obj.get("type")) + path = from_union([from_none, lambda x: from_str(x)], obj.get("path")) + display_name = from_union([from_none, lambda x: from_str(x)], obj.get("displayName")) + line_range = from_union([from_none, lambda x: UserMessageDataAttachmentsItemLineRange.from_dict(x)], obj.get("lineRange")) + file_path = from_union([from_none, lambda x: from_str(x)], obj.get("filePath")) + text = from_union([from_none, lambda x: from_str(x)], obj.get("text")) + selection = from_union([from_none, lambda x: UserMessageDataAttachmentsItemSelection.from_dict(x)], obj.get("selection")) + number = from_union([from_none, lambda x: from_float(x)], obj.get("number")) + title = from_union([from_none, lambda x: from_str(x)], obj.get("title")) + reference_type = from_union([from_none, lambda x: parse_enum(UserMessageDataAttachmentsItemReferenceType, x)], obj.get("referenceType")) + state = from_union([from_none, lambda x: from_str(x)], obj.get("state")) + url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) + data = from_union([from_none, lambda x: from_str(x)], obj.get("data")) + mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType")) + return UserMessageDataAttachmentsItem( + type=type, + path=path, + display_name=display_name, + line_range=line_range, + file_path=file_path, + text=text, + selection=selection, + number=number, + title=title, + reference_type=reference_type, + state=state, + url=url, + data=data, + mime_type=mime_type, + ) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(ToolExecutionCompleteContentType, self.type) + result["type"] = to_enum(UserMessageDataAttachmentsItemType, self.type) + if self.path is not None: + result["path"] = from_union([from_none, lambda x: from_str(x)], self.path) + if self.display_name is not None: + result["displayName"] = from_union([from_none, lambda x: from_str(x)], self.display_name) + if self.line_range is not None: + result["lineRange"] = from_union([from_none, lambda x: to_class(UserMessageDataAttachmentsItemLineRange, x)], self.line_range) + if self.file_path is not None: + result["filePath"] = from_union([from_none, lambda x: from_str(x)], self.file_path) if self.text is not None: - result["text"] = from_union([from_str, from_none], self.text) - if self.cwd is not None: - result["cwd"] = from_union([from_str, from_none], self.cwd) - if self.exit_code is not None: - result["exitCode"] = from_union([to_float, from_none], self.exit_code) + result["text"] = from_union([from_none, lambda x: from_str(x)], self.text) + if self.selection is not None: + result["selection"] = from_union([from_none, lambda x: to_class(UserMessageDataAttachmentsItemSelection, x)], self.selection) + if self.number is not None: + result["number"] = from_union([from_none, lambda x: to_float(x)], self.number) + if self.title is not None: + result["title"] = from_union([from_none, lambda x: from_str(x)], self.title) + if self.reference_type is not None: + result["referenceType"] = from_union([from_none, lambda x: to_enum(UserMessageDataAttachmentsItemReferenceType, x)], self.reference_type) + if self.state is not None: + result["state"] = from_union([from_none, lambda x: from_str(x)], self.state) + if self.url is not None: + result["url"] = from_union([from_none, lambda x: from_str(x)], self.url) if self.data is not None: - result["data"] = from_union([from_str, from_none], self.data) + result["data"] = from_union([from_none, lambda x: from_str(x)], self.data) if self.mime_type is not None: - result["mimeType"] = from_union([from_str, from_none], self.mime_type) - if self.description is not None: - result["description"] = from_union([from_str, from_none], self.description) - if self.icons is not None: - result["icons"] = from_union([lambda x: from_list(lambda x: to_class(ToolExecutionCompleteContentResourceLinkIcon, x), x), from_none], self.icons) - if self.name is not None: - result["name"] = from_union([from_str, from_none], self.name) - if self.size is not None: - result["size"] = from_union([to_float, from_none], self.size) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) - if self.uri is not None: - result["uri"] = from_union([from_str, from_none], self.uri) - if self.resource is not None: - result["resource"] = from_union([lambda x: to_class(ToolExecutionCompleteContentResourceDetails, x), from_none], self.resource) + result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type) return result -class PermissionCompletedKind(Enum): - """The outcome of the permission request""" - - APPROVED = "approved" - DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy" - DENIED_BY_PERMISSION_REQUEST_HOOK = "denied-by-permission-request-hook" - DENIED_BY_RULES = "denied-by-rules" - DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user" - DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user" - - @dataclass -class Result: - """Tool execution result on success - - The result of the permission request - """ - content: str | None = None - """Concise tool result text sent to the LLM for chat completion, potentially truncated for - token efficiency - """ - contents: list[ToolExecutionCompleteContent] | None = None - """Structured content blocks (text, images, audio, resources) returned by the tool in their - native format - """ - detailed_content: str | None = None - """Full detailed tool result for UI/timeline display, preserving complete content such as - diffs. Falls back to content when absent. - """ - kind: PermissionCompletedKind | None = None - """The outcome of the permission request""" +class UserMessageData: + content: str + transformed_content: str | None = None + attachments: list[UserMessageDataAttachmentsItem] | None = None + source: str | None = None + agent_mode: UserMessageDataAgentMode | None = None + interaction_id: str | None = None @staticmethod - def from_dict(obj: Any) -> 'Result': + def from_dict(obj: Any) -> "UserMessageData": assert isinstance(obj, dict) - content = from_union([from_str, from_none], obj.get("content")) - contents = from_union([lambda x: from_list(ToolExecutionCompleteContent.from_dict, x), from_none], obj.get("contents")) - detailed_content = from_union([from_str, from_none], obj.get("detailedContent")) - kind = from_union([PermissionCompletedKind, from_none], obj.get("kind")) - return Result(content, contents, detailed_content, kind) + content = from_str(obj.get("content")) + transformed_content = from_union([from_none, lambda x: from_str(x)], obj.get("transformedContent")) + attachments = from_union([from_none, lambda x: from_list(UserMessageDataAttachmentsItem.from_dict, x)], obj.get("attachments")) + source = from_union([from_none, lambda x: from_str(x)], obj.get("source")) + agent_mode = from_union([from_none, lambda x: parse_enum(UserMessageDataAgentMode, x)], obj.get("agentMode")) + interaction_id = from_union([from_none, lambda x: from_str(x)], obj.get("interactionId")) + return UserMessageData( + content=content, + transformed_content=transformed_content, + attachments=attachments, + source=source, + agent_mode=agent_mode, + interaction_id=interaction_id, + ) def to_dict(self) -> dict: result: dict = {} - if self.content is not None: - result["content"] = from_union([from_str, from_none], self.content) - if self.contents is not None: - result["contents"] = from_union([lambda x: from_list(lambda x: to_class(ToolExecutionCompleteContent, x), x), from_none], self.contents) - if self.detailed_content is not None: - result["detailedContent"] = from_union([from_str, from_none], self.detailed_content) - if self.kind is not None: - result["kind"] = from_union([lambda x: to_enum(PermissionCompletedKind, x), from_none], self.kind) + result["content"] = from_str(self.content) + if self.transformed_content is not None: + result["transformedContent"] = from_union([from_none, lambda x: from_str(x)], self.transformed_content) + if self.attachments is not None: + result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: to_class(UserMessageDataAttachmentsItem, x), x)], self.attachments) + if self.source is not None: + result["source"] = from_union([from_none, lambda x: from_str(x)], self.source) + if self.agent_mode is not None: + result["agentMode"] = from_union([from_none, lambda x: to_enum(UserMessageDataAgentMode, x)], self.agent_mode) + if self.interaction_id is not None: + result["interactionId"] = from_union([from_none, lambda x: from_str(x)], self.interaction_id) return result -class SystemMessageRole(Enum): - """Message role: "system" for system prompts, "developer" for developer-injected instructions""" - - DEVELOPER = "developer" - SYSTEM = "system" - +@dataclass +class PendingMessagesModifiedData: + """Empty payload; the event signals that the pending message queue has changed""" + @staticmethod + def from_dict(obj: Any) -> "PendingMessagesModifiedData": + assert isinstance(obj, dict) + return PendingMessagesModifiedData() -class MCPServerStatus(Enum): - """Connection status: connected, failed, needs-auth, pending, disabled, or not_configured - - New connection status: connected, failed, needs-auth, pending, disabled, or not_configured - """ - CONNECTED = "connected" - DISABLED = "disabled" - FAILED = "failed" - NEEDS_AUTH = "needs-auth" - NOT_CONFIGURED = "not_configured" - PENDING = "pending" + def to_dict(self) -> dict: + return {} @dataclass -class MCPServersLoadedServer: - name: str - """Server name (config key)""" +class AssistantTurnStartData: + """Turn initialization metadata including identifier and interaction tracking""" + turn_id: str + interaction_id: str | None = None - status: MCPServerStatus - """Connection status: connected, failed, needs-auth, pending, disabled, or not_configured""" + @staticmethod + def from_dict(obj: Any) -> "AssistantTurnStartData": + assert isinstance(obj, dict) + turn_id = from_str(obj.get("turnId")) + interaction_id = from_union([from_none, lambda x: from_str(x)], obj.get("interactionId")) + return AssistantTurnStartData( + turn_id=turn_id, + interaction_id=interaction_id, + ) - error: str | None = None - """Error message if the server failed to connect""" + def to_dict(self) -> dict: + result: dict = {} + result["turnId"] = from_str(self.turn_id) + if self.interaction_id is not None: + result["interactionId"] = from_union([from_none, lambda x: from_str(x)], self.interaction_id) + return result - source: str | None = None - """Configuration source: user, workspace, plugin, or builtin""" + +@dataclass +class AssistantIntentData: + """Agent intent description for current activity or plan""" + intent: str @staticmethod - def from_dict(obj: Any) -> 'MCPServersLoadedServer': + def from_dict(obj: Any) -> "AssistantIntentData": assert isinstance(obj, dict) - name = from_str(obj.get("name")) - status = MCPServerStatus(obj.get("status")) - error = from_union([from_str, from_none], obj.get("error")) - source = from_union([from_str, from_none], obj.get("source")) - return MCPServersLoadedServer(name, status, error, source) + intent = from_str(obj.get("intent")) + return AssistantIntentData( + intent=intent, + ) def to_dict(self) -> dict: result: dict = {} - result["name"] = from_str(self.name) - result["status"] = to_enum(MCPServerStatus, self.status) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.source is not None: - result["source"] = from_union([from_str, from_none], self.source) + result["intent"] = from_str(self.intent) return result -class ShutdownType(Enum): - """Whether the session ended normally ("routine") or due to a crash/fatal error ("error")""" +@dataclass +class AssistantReasoningData: + """Assistant reasoning content for timeline display with complete thinking text""" + reasoning_id: str + content: str + + @staticmethod + def from_dict(obj: Any) -> "AssistantReasoningData": + assert isinstance(obj, dict) + reasoning_id = from_str(obj.get("reasoningId")) + content = from_str(obj.get("content")) + return AssistantReasoningData( + reasoning_id=reasoning_id, + content=content, + ) - ERROR = "error" - ROUTINE = "routine" + def to_dict(self) -> dict: + result: dict = {} + result["reasoningId"] = from_str(self.reasoning_id) + result["content"] = from_str(self.content) + return result @dataclass -class SkillsLoadedSkill: - description: str - """Description of what the skill does""" - - enabled: bool - """Whether the skill is currently enabled""" - - name: str - """Unique identifier for the skill""" - - source: str - """Source location type of the skill (e.g., project, personal, plugin)""" - - user_invocable: bool - """Whether the skill can be invoked by the user as a slash command""" - - path: str | None = None - """Absolute path to the skill file, if available""" +class AssistantReasoningDeltaData: + """Streaming reasoning delta for incremental extended thinking updates""" + reasoning_id: str + delta_content: str @staticmethod - def from_dict(obj: Any) -> 'SkillsLoadedSkill': + def from_dict(obj: Any) -> "AssistantReasoningDeltaData": assert isinstance(obj, dict) - description = from_str(obj.get("description")) - enabled = from_bool(obj.get("enabled")) - name = from_str(obj.get("name")) - source = from_str(obj.get("source")) - user_invocable = from_bool(obj.get("userInvocable")) - path = from_union([from_str, from_none], obj.get("path")) - return SkillsLoadedSkill(description, enabled, name, source, user_invocable, path) + reasoning_id = from_str(obj.get("reasoningId")) + delta_content = from_str(obj.get("deltaContent")) + return AssistantReasoningDeltaData( + reasoning_id=reasoning_id, + delta_content=delta_content, + ) def to_dict(self) -> dict: result: dict = {} - result["description"] = from_str(self.description) - result["enabled"] = from_bool(self.enabled) - result["name"] = from_str(self.name) - result["source"] = from_str(self.source) - result["userInvocable"] = from_bool(self.user_invocable) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) + result["reasoningId"] = from_str(self.reasoning_id) + result["deltaContent"] = from_str(self.delta_content) return result -class HandoffSourceType(Enum): - """Origin type of the session being handed off""" - - LOCAL = "local" - REMOTE = "remote" - - @dataclass -class MCPOauthRequiredStaticClientConfig: - """Static OAuth client configuration, if the server specifies one""" - - client_id: str - """OAuth client ID for the server""" - - public_client: bool | None = None - """Whether this is a public OAuth client""" +class AssistantStreamingDeltaData: + """Streaming response progress with cumulative byte count""" + total_response_size_bytes: float @staticmethod - def from_dict(obj: Any) -> 'MCPOauthRequiredStaticClientConfig': + def from_dict(obj: Any) -> "AssistantStreamingDeltaData": assert isinstance(obj, dict) - client_id = from_str(obj.get("clientId")) - public_client = from_union([from_bool, from_none], obj.get("publicClient")) - return MCPOauthRequiredStaticClientConfig(client_id, public_client) + total_response_size_bytes = from_float(obj.get("totalResponseSizeBytes")) + return AssistantStreamingDeltaData( + total_response_size_bytes=total_response_size_bytes, + ) def to_dict(self) -> dict: result: dict = {} - result["clientId"] = from_str(self.client_id) - if self.public_client is not None: - result["publicClient"] = from_union([from_bool, from_none], self.public_client) + result["totalResponseSizeBytes"] = to_float(self.total_response_size_bytes) return result -class AssistantMessageToolRequestType(Enum): - """Tool call type: "function" for standard tool calls, "custom" for grammar-based tool - calls. Defaults to "function" when absent. - """ - CUSTOM = "custom" - FUNCTION = "function" - - @dataclass -class AssistantMessageToolRequest: +class AssistantMessageDataToolRequestsItem: """A tool invocation request from the assistant""" - - name: str - """Name of the tool being invoked""" - tool_call_id: str - """Unique identifier for this tool call""" - + name: str arguments: Any = None - """Arguments to pass to the tool, format depends on the tool""" - - intention_summary: str | None = None - """Resolved intention summary describing what this specific call does""" - - mcp_server_name: str | None = None - """Name of the MCP server hosting this tool, when the tool is an MCP tool""" - + type: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType | None = None tool_title: str | None = None - """Human-readable display title for the tool""" - - type: AssistantMessageToolRequestType | None = None - """Tool call type: "function" for standard tool calls, "custom" for grammar-based tool - calls. Defaults to "function" when absent. - """ + mcp_server_name: str | None = None + intention_summary: str | None = None @staticmethod - def from_dict(obj: Any) -> 'AssistantMessageToolRequest': + def from_dict(obj: Any) -> "AssistantMessageDataToolRequestsItem": assert isinstance(obj, dict) - name = from_str(obj.get("name")) tool_call_id = from_str(obj.get("toolCallId")) + name = from_str(obj.get("name")) arguments = obj.get("arguments") - intention_summary = from_union([from_none, from_str], obj.get("intentionSummary")) - mcp_server_name = from_union([from_str, from_none], obj.get("mcpServerName")) - tool_title = from_union([from_str, from_none], obj.get("toolTitle")) - type = from_union([AssistantMessageToolRequestType, from_none], obj.get("type")) - return AssistantMessageToolRequest(name, tool_call_id, arguments, intention_summary, mcp_server_name, tool_title, type) + type = from_union([from_none, lambda x: parse_enum(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType, x)], obj.get("type")) + tool_title = from_union([from_none, lambda x: from_str(x)], obj.get("toolTitle")) + mcp_server_name = from_union([from_none, lambda x: from_str(x)], obj.get("mcpServerName")) + intention_summary = from_union([from_none, lambda x: from_str(x)], obj.get("intentionSummary")) + return AssistantMessageDataToolRequestsItem( + tool_call_id=tool_call_id, + name=name, + arguments=arguments, + type=type, + tool_title=tool_title, + mcp_server_name=mcp_server_name, + intention_summary=intention_summary, + ) def to_dict(self) -> dict: result: dict = {} - result["name"] = from_str(self.name) result["toolCallId"] = from_str(self.tool_call_id) + result["name"] = from_str(self.name) if self.arguments is not None: result["arguments"] = self.arguments - if self.intention_summary is not None: - result["intentionSummary"] = from_union([from_none, from_str], self.intention_summary) - if self.mcp_server_name is not None: - result["mcpServerName"] = from_union([from_str, from_none], self.mcp_server_name) - if self.tool_title is not None: - result["toolTitle"] = from_union([from_str, from_none], self.tool_title) if self.type is not None: - result["type"] = from_union([lambda x: to_enum(AssistantMessageToolRequestType, x), from_none], self.type) + result["type"] = from_union([from_none, lambda x: to_enum(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType, x)], self.type) + if self.tool_title is not None: + result["toolTitle"] = from_union([from_none, lambda x: from_str(x)], self.tool_title) + if self.mcp_server_name is not None: + result["mcpServerName"] = from_union([from_none, lambda x: from_str(x)], self.mcp_server_name) + if self.intention_summary is not None: + result["intentionSummary"] = from_union([from_none, lambda x: from_str(x)], self.intention_summary) return result @dataclass -class CapabilitiesChangedUI: - """UI capability changes""" - - elicitation: bool | None = None - """Whether elicitation is now supported""" +class AssistantMessageData: + """Assistant response containing text content, optional tool requests, and interaction metadata""" + message_id: str + content: str + tool_requests: list[AssistantMessageDataToolRequestsItem] | None = None + reasoning_opaque: str | None = None + reasoning_text: str | None = None + encrypted_content: str | None = None + phase: str | None = None + output_tokens: float | None = None + interaction_id: str | None = None + request_id: str | None = None + parent_tool_call_id: str | None = None @staticmethod - def from_dict(obj: Any) -> 'CapabilitiesChangedUI': + def from_dict(obj: Any) -> "AssistantMessageData": assert isinstance(obj, dict) - elicitation = from_union([from_bool, from_none], obj.get("elicitation")) - return CapabilitiesChangedUI(elicitation) + message_id = from_str(obj.get("messageId")) + content = from_str(obj.get("content")) + tool_requests = from_union([from_none, lambda x: from_list(lambda x: AssistantMessageDataToolRequestsItem.from_dict(x), x)], obj.get("toolRequests")) + reasoning_opaque = from_union([from_none, lambda x: from_str(x)], obj.get("reasoningOpaque")) + reasoning_text = from_union([from_none, lambda x: from_str(x)], obj.get("reasoningText")) + encrypted_content = from_union([from_none, lambda x: from_str(x)], obj.get("encryptedContent")) + phase = from_union([from_none, lambda x: from_str(x)], obj.get("phase")) + output_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("outputTokens")) + interaction_id = from_union([from_none, lambda x: from_str(x)], obj.get("interactionId")) + request_id = from_union([from_none, lambda x: from_str(x)], obj.get("requestId")) + parent_tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("parentToolCallId")) + return AssistantMessageData( + message_id=message_id, + content=content, + tool_requests=tool_requests, + reasoning_opaque=reasoning_opaque, + reasoning_text=reasoning_text, + encrypted_content=encrypted_content, + phase=phase, + output_tokens=output_tokens, + interaction_id=interaction_id, + request_id=request_id, + parent_tool_call_id=parent_tool_call_id, + ) def to_dict(self) -> dict: result: dict = {} - if self.elicitation is not None: - result["elicitation"] = from_union([from_bool, from_none], self.elicitation) + result["messageId"] = from_str(self.message_id) + result["content"] = from_str(self.content) + if self.tool_requests is not None: + result["toolRequests"] = from_union([from_none, lambda x: from_list(lambda x: to_class(AssistantMessageDataToolRequestsItem, x), x)], self.tool_requests) + if self.reasoning_opaque is not None: + result["reasoningOpaque"] = from_union([from_none, lambda x: from_str(x)], self.reasoning_opaque) + if self.reasoning_text is not None: + result["reasoningText"] = from_union([from_none, lambda x: from_str(x)], self.reasoning_text) + if self.encrypted_content is not None: + result["encryptedContent"] = from_union([from_none, lambda x: from_str(x)], self.encrypted_content) + if self.phase is not None: + result["phase"] = from_union([from_none, lambda x: from_str(x)], self.phase) + if self.output_tokens is not None: + result["outputTokens"] = from_union([from_none, lambda x: to_float(x)], self.output_tokens) + if self.interaction_id is not None: + result["interactionId"] = from_union([from_none, lambda x: from_str(x)], self.interaction_id) + if self.request_id is not None: + result["requestId"] = from_union([from_none, lambda x: from_str(x)], self.request_id) + if self.parent_tool_call_id is not None: + result["parentToolCallId"] = from_union([from_none, lambda x: from_str(x)], self.parent_tool_call_id) return result @dataclass -class Data: - """Session initialization metadata including context and configuration - - Session resume metadata including current context and event count - - Notifies Mission Control that the session's remote steering capability has changed - - Error details for timeline display including message and optional diagnostic information - - Payload indicating the session is idle with no background agents in flight - - Session title change payload containing the new display title - - Informational message for timeline display with categorization - - Warning message for timeline display with categorization - - Model change details including previous and new model identifiers - - Agent mode change details including previous and new modes - - Plan file operation details indicating what changed - - Workspace file change details including path and operation type - - Session handoff metadata including source, context, and repository information - - Conversation truncation statistics including token counts and removed content metrics - - Session rewind details including target event and count of removed events - - Session termination metrics including usage statistics, code changes, and shutdown - reason - - Updated working directory and git context after the change - - Current context window usage statistics including token and message counts - - Context window breakdown at the start of LLM-powered conversation compaction - - Conversation compaction results including success status, metrics, and optional error - details - - Task completion notification with summary from the agent - - Empty payload; the event signals that the pending message queue has changed - - Turn initialization metadata including identifier and interaction tracking - - Agent intent description for current activity or plan - - Assistant reasoning content for timeline display with complete thinking text - - Streaming reasoning delta for incremental extended thinking updates - - Streaming response progress with cumulative byte count - - Assistant response containing text content, optional tool requests, and interaction - metadata - - Streaming assistant message delta for incremental response updates - - Turn completion metadata including the turn identifier - - LLM API call usage metrics including tokens, costs, quotas, and billing information - - Turn abort information including the reason for termination - - User-initiated tool invocation request with tool name and arguments - - Tool execution startup details including MCP server information when applicable - - Streaming tool execution output for incremental result display - - Tool execution progress notification with status message - - Tool execution completion results including success status, detailed output, and error - information - - Skill invocation details including content, allowed tools, and plugin metadata - - Sub-agent startup details including parent tool call and agent information - - Sub-agent completion details for successful execution - - Sub-agent failure details including error message and agent information - - Custom agent selection details including name and available tools - - Empty payload; the event signals that the custom agent was deselected, returning to the - default agent - - Hook invocation start details including type and input data - - Hook invocation completion details including output, success status, and error - information - - System or developer message content with role and optional template metadata - - System-generated notification for runtime events like background task completion - - Permission request notification requiring client approval with request details - - Permission request completion notification signaling UI dismissal - - User input request notification with question and optional predefined choices - - User input request completion with the user's response - - Elicitation request; may be form-based (structured input) or URL-based (browser - redirect) - - Elicitation request completion with the user's response - - Sampling request from an MCP server; contains the server name and a requestId for - correlation - - Sampling request completion notification signaling UI dismissal - - OAuth authentication request for an MCP server - - MCP OAuth request completion notification - - External tool invocation request for client-side tool execution - - External tool completion notification signaling UI dismissal - - Queued slash command dispatch request for client execution - - Registered command dispatch request routed to the owning client - - Queued command completion notification signaling UI dismissal - - SDK command registration change notification - - Session capability change notification - - Plan approval request with plan content and available user actions - - Plan mode exit completion with the user's approval decision and optional feedback - """ - already_in_use: bool | None = None - """Whether the session was already in use by another client at start time - - Whether the session was already in use by another client at resume time - """ - context: Context | str | None = None - """Working directory and git context at session start - - Updated working directory and git context at resume time - - Additional context information for the handoff - """ - copilot_version: str | None = None - """Version string of the Copilot application""" - - producer: str | None = None - """Identifier of the software producing the events (e.g., "copilot-agent")""" - - reasoning_effort: str | None = None - """Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", - "xhigh") - - Reasoning effort level after the model change, if applicable - """ - remote_steerable: bool | None = None - """Whether this session supports remote steering via Mission Control - - Whether this session now supports remote steering via Mission Control - """ - selected_model: str | None = None - """Model selected at session creation time, if any - - Model currently selected at resume time - """ - session_id: str | None = None - """Unique identifier for the session - - Session ID that this external tool request belongs to - """ - start_time: datetime | None = None - """ISO 8601 timestamp when the session was created""" - - version: float | None = None - """Schema version number for the session event format""" - - event_count: float | None = None - """Total number of persisted events in the session at the time of resume""" - - resume_time: datetime | None = None - """ISO 8601 timestamp when the session was resumed""" - - error_type: str | None = None - """Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", - "context_limit", "query") - """ - message: str | None = None - """Human-readable error message - - Human-readable informational message for display in the timeline - - Human-readable warning message for display in the timeline - - Message describing what information is needed from the user - """ - provider_call_id: str | None = None - """GitHub request tracing ID (x-github-request-id header) for correlating with server-side - logs - - GitHub request tracing ID (x-github-request-id header) for server-side log correlation - """ - stack: str | None = None - """Error stack trace, when available""" - - status_code: int | None = None - """HTTP status code from the upstream request, if applicable""" - - url: str | None = None - """Optional URL associated with this error that the user can open in a browser - - Optional URL associated with this message that the user can open in a browser - - Optional URL associated with this warning that the user can open in a browser - - URL to open in the user's browser (url mode only) - """ - aborted: bool | None = None - """True when the preceding agentic loop was cancelled via abort signal""" - - title: str | None = None - """The new display title for the session""" - - info_type: str | None = None - """Category of informational message (e.g., "notification", "timing", "context_window", - "mcp", "snapshot", "configuration", "authentication", "model") - """ - warning_type: str | None = None - """Category of warning (e.g., "subscription", "policy", "mcp")""" +class AssistantMessageDeltaData: + """Streaming assistant message delta for incremental response updates""" + message_id: str + delta_content: str + parent_tool_call_id: str | None = None - new_model: str | None = None - """Newly selected model identifier""" + @staticmethod + def from_dict(obj: Any) -> "AssistantMessageDeltaData": + assert isinstance(obj, dict) + message_id = from_str(obj.get("messageId")) + delta_content = from_str(obj.get("deltaContent")) + parent_tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("parentToolCallId")) + return AssistantMessageDeltaData( + message_id=message_id, + delta_content=delta_content, + parent_tool_call_id=parent_tool_call_id, + ) - previous_model: str | None = None - """Model that was previously selected, if any""" + def to_dict(self) -> dict: + result: dict = {} + result["messageId"] = from_str(self.message_id) + result["deltaContent"] = from_str(self.delta_content) + if self.parent_tool_call_id is not None: + result["parentToolCallId"] = from_union([from_none, lambda x: from_str(x)], self.parent_tool_call_id) + return result - previous_reasoning_effort: str | None = None - """Reasoning effort level before the model change, if applicable""" - new_mode: str | None = None - """Agent mode after the change (e.g., "interactive", "plan", "autopilot")""" +@dataclass +class AssistantTurnEndData: + """Turn completion metadata including the turn identifier""" + turn_id: str - previous_mode: str | None = None - """Agent mode before the change (e.g., "interactive", "plan", "autopilot")""" + @staticmethod + def from_dict(obj: Any) -> "AssistantTurnEndData": + assert isinstance(obj, dict) + turn_id = from_str(obj.get("turnId")) + return AssistantTurnEndData( + turn_id=turn_id, + ) - operation: ChangedOperation | None = None - """The type of operation performed on the plan file - - Whether the file was newly created or updated - """ - path: str | None = None - """Relative path within the session workspace files directory - - File path to the SKILL.md definition - """ - handoff_time: datetime | None = None - """ISO 8601 timestamp when the handoff occurred""" + def to_dict(self) -> dict: + result: dict = {} + result["turnId"] = from_str(self.turn_id) + return result - host: str | None = None - """GitHub host URL for the source session (e.g., https://github.com or - https://tenant.ghe.com) - """ - remote_session_id: str | None = None - """Session ID of the remote session being handed off""" - - repository: HandoffRepository | str | None = None - """Repository context for the handed-off session - - Repository identifier derived from the git remote URL ("owner/name" for GitHub, - "org/project/repo" for Azure DevOps) - """ - source_type: HandoffSourceType | None = None - """Origin type of the session being handed off""" - summary: str | None = None - """Summary of the work done in the source session - - Summary of the completed task, provided by the agent - - Summary of the plan that was created - """ - messages_removed_during_truncation: float | None = None - """Number of messages removed by truncation""" +@dataclass +class AssistantUsageDataQuotaSnapshotsValue: + is_unlimited_entitlement: bool + entitlement_requests: float + used_requests: float + usage_allowed_with_exhausted_quota: bool + overage: float + overage_allowed_with_exhausted_quota: bool + remaining_percentage: float + reset_date: datetime | None = None - performed_by: str | None = None - """Identifier of the component that performed truncation (e.g., "BasicTruncator")""" + @staticmethod + def from_dict(obj: Any) -> "AssistantUsageDataQuotaSnapshotsValue": + assert isinstance(obj, dict) + is_unlimited_entitlement = from_bool(obj.get("isUnlimitedEntitlement")) + entitlement_requests = from_float(obj.get("entitlementRequests")) + used_requests = from_float(obj.get("usedRequests")) + usage_allowed_with_exhausted_quota = from_bool(obj.get("usageAllowedWithExhaustedQuota")) + overage = from_float(obj.get("overage")) + overage_allowed_with_exhausted_quota = from_bool(obj.get("overageAllowedWithExhaustedQuota")) + remaining_percentage = from_float(obj.get("remainingPercentage")) + reset_date = from_union([from_none, lambda x: from_datetime(x)], obj.get("resetDate")) + return AssistantUsageDataQuotaSnapshotsValue( + is_unlimited_entitlement=is_unlimited_entitlement, + entitlement_requests=entitlement_requests, + used_requests=used_requests, + usage_allowed_with_exhausted_quota=usage_allowed_with_exhausted_quota, + overage=overage, + overage_allowed_with_exhausted_quota=overage_allowed_with_exhausted_quota, + remaining_percentage=remaining_percentage, + reset_date=reset_date, + ) - post_truncation_messages_length: float | None = None - """Number of conversation messages after truncation""" + def to_dict(self) -> dict: + result: dict = {} + result["isUnlimitedEntitlement"] = from_bool(self.is_unlimited_entitlement) + result["entitlementRequests"] = to_float(self.entitlement_requests) + result["usedRequests"] = to_float(self.used_requests) + result["usageAllowedWithExhaustedQuota"] = from_bool(self.usage_allowed_with_exhausted_quota) + result["overage"] = to_float(self.overage) + result["overageAllowedWithExhaustedQuota"] = from_bool(self.overage_allowed_with_exhausted_quota) + result["remainingPercentage"] = to_float(self.remaining_percentage) + if self.reset_date is not None: + result["resetDate"] = from_union([from_none, lambda x: to_datetime(x)], self.reset_date) + return result - post_truncation_tokens_in_messages: float | None = None - """Total tokens in conversation messages after truncation""" - pre_truncation_messages_length: float | None = None - """Number of conversation messages before truncation""" +@dataclass +class AssistantUsageDataCopilotUsageTokenDetailsItem: + """Token usage detail for a single billing category""" + batch_size: float + cost_per_batch: float + token_count: float + token_type: str - pre_truncation_tokens_in_messages: float | None = None - """Total tokens in conversation messages before truncation""" + @staticmethod + def from_dict(obj: Any) -> "AssistantUsageDataCopilotUsageTokenDetailsItem": + assert isinstance(obj, dict) + batch_size = from_float(obj.get("batchSize")) + cost_per_batch = from_float(obj.get("costPerBatch")) + token_count = from_float(obj.get("tokenCount")) + token_type = from_str(obj.get("tokenType")) + return AssistantUsageDataCopilotUsageTokenDetailsItem( + batch_size=batch_size, + cost_per_batch=cost_per_batch, + token_count=token_count, + token_type=token_type, + ) - token_limit: float | None = None - """Maximum token count for the model's context window""" + def to_dict(self) -> dict: + result: dict = {} + result["batchSize"] = to_float(self.batch_size) + result["costPerBatch"] = to_float(self.cost_per_batch) + result["tokenCount"] = to_float(self.token_count) + result["tokenType"] = from_str(self.token_type) + return result - tokens_removed_during_truncation: float | None = None - """Number of tokens removed by truncation""" - events_removed: float | None = None - """Number of events that were removed by the rewind""" +@dataclass +class AssistantUsageDataCopilotUsage: + """Per-request cost and usage data from the CAPI copilot_usage response field""" + token_details: list[AssistantUsageDataCopilotUsageTokenDetailsItem] + total_nano_aiu: float - up_to_event_id: str | None = None - """Event ID that was rewound to; this event and all after it were removed""" + @staticmethod + def from_dict(obj: Any) -> "AssistantUsageDataCopilotUsage": + assert isinstance(obj, dict) + token_details = from_list(lambda x: AssistantUsageDataCopilotUsageTokenDetailsItem.from_dict(x), obj.get("tokenDetails")) + total_nano_aiu = from_float(obj.get("totalNanoAiu")) + return AssistantUsageDataCopilotUsage( + token_details=token_details, + total_nano_aiu=total_nano_aiu, + ) - code_changes: ShutdownCodeChanges | None = None - """Aggregate code change metrics for the session""" + def to_dict(self) -> dict: + result: dict = {} + result["tokenDetails"] = from_list(lambda x: to_class(AssistantUsageDataCopilotUsageTokenDetailsItem, x), self.token_details) + result["totalNanoAiu"] = to_float(self.total_nano_aiu) + return result - conversation_tokens: float | None = None - """Non-system message token count at shutdown - - Token count from non-system messages (user, assistant, tool) - - Token count from non-system messages (user, assistant, tool) at compaction start - - Token count from non-system messages (user, assistant, tool) after compaction - """ - current_model: str | None = None - """Model that was selected at the time of shutdown""" - current_tokens: float | None = None - """Total tokens in context window at shutdown - - Current number of tokens in the context window - """ - error_reason: str | None = None - """Error description when shutdownType is "error\"""" +@dataclass +class AssistantUsageData: + """LLM API call usage metrics including tokens, costs, quotas, and billing information""" + model: str + input_tokens: float | None = None + output_tokens: float | None = None + cache_read_tokens: float | None = None + cache_write_tokens: float | None = None + cost: float | None = None + duration: float | None = None + ttft_ms: float | None = None + inter_token_latency_ms: float | None = None + initiator: str | None = None + api_call_id: str | None = None + provider_call_id: str | None = None + parent_tool_call_id: str | None = None + quota_snapshots: dict[str, AssistantUsageDataQuotaSnapshotsValue] | None = None + copilot_usage: AssistantUsageDataCopilotUsage | None = None + reasoning_effort: str | None = None - model_metrics: dict[str, ShutdownModelMetric] | None = None - """Per-model usage breakdown, keyed by model identifier""" + @staticmethod + def from_dict(obj: Any) -> "AssistantUsageData": + assert isinstance(obj, dict) + model = from_str(obj.get("model")) + input_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("inputTokens")) + output_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("outputTokens")) + cache_read_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("cacheReadTokens")) + cache_write_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("cacheWriteTokens")) + cost = from_union([from_none, lambda x: from_float(x)], obj.get("cost")) + duration = from_union([from_none, lambda x: from_float(x)], obj.get("duration")) + ttft_ms = from_union([from_none, lambda x: from_float(x)], obj.get("ttftMs")) + inter_token_latency_ms = from_union([from_none, lambda x: from_float(x)], obj.get("interTokenLatencyMs")) + initiator = from_union([from_none, lambda x: from_str(x)], obj.get("initiator")) + api_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("apiCallId")) + provider_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("providerCallId")) + parent_tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("parentToolCallId")) + quota_snapshots = from_union([from_none, lambda x: from_dict(lambda x: AssistantUsageDataQuotaSnapshotsValue.from_dict(x), x)], obj.get("quotaSnapshots")) + copilot_usage = from_union([from_none, lambda x: AssistantUsageDataCopilotUsage.from_dict(x)], obj.get("copilotUsage")) + reasoning_effort = from_union([from_none, lambda x: from_str(x)], obj.get("reasoningEffort")) + return AssistantUsageData( + model=model, + input_tokens=input_tokens, + output_tokens=output_tokens, + cache_read_tokens=cache_read_tokens, + cache_write_tokens=cache_write_tokens, + cost=cost, + duration=duration, + ttft_ms=ttft_ms, + inter_token_latency_ms=inter_token_latency_ms, + initiator=initiator, + api_call_id=api_call_id, + provider_call_id=provider_call_id, + parent_tool_call_id=parent_tool_call_id, + quota_snapshots=quota_snapshots, + copilot_usage=copilot_usage, + reasoning_effort=reasoning_effort, + ) - session_start_time: float | None = None - """Unix timestamp (milliseconds) when the session started""" + def to_dict(self) -> dict: + result: dict = {} + result["model"] = from_str(self.model) + if self.input_tokens is not None: + result["inputTokens"] = from_union([from_none, lambda x: to_float(x)], self.input_tokens) + if self.output_tokens is not None: + result["outputTokens"] = from_union([from_none, lambda x: to_float(x)], self.output_tokens) + if self.cache_read_tokens is not None: + result["cacheReadTokens"] = from_union([from_none, lambda x: to_float(x)], self.cache_read_tokens) + if self.cache_write_tokens is not None: + result["cacheWriteTokens"] = from_union([from_none, lambda x: to_float(x)], self.cache_write_tokens) + if self.cost is not None: + result["cost"] = from_union([from_none, lambda x: to_float(x)], self.cost) + if self.duration is not None: + result["duration"] = from_union([from_none, lambda x: to_float(x)], self.duration) + if self.ttft_ms is not None: + result["ttftMs"] = from_union([from_none, lambda x: to_float(x)], self.ttft_ms) + if self.inter_token_latency_ms is not None: + result["interTokenLatencyMs"] = from_union([from_none, lambda x: to_float(x)], self.inter_token_latency_ms) + if self.initiator is not None: + result["initiator"] = from_union([from_none, lambda x: from_str(x)], self.initiator) + if self.api_call_id is not None: + result["apiCallId"] = from_union([from_none, lambda x: from_str(x)], self.api_call_id) + if self.provider_call_id is not None: + result["providerCallId"] = from_union([from_none, lambda x: from_str(x)], self.provider_call_id) + if self.parent_tool_call_id is not None: + result["parentToolCallId"] = from_union([from_none, lambda x: from_str(x)], self.parent_tool_call_id) + if self.quota_snapshots is not None: + result["quotaSnapshots"] = from_union([from_none, lambda x: from_dict(lambda x: to_class(AssistantUsageDataQuotaSnapshotsValue, x), x)], self.quota_snapshots) + if self.copilot_usage is not None: + result["copilotUsage"] = from_union([from_none, lambda x: to_class(AssistantUsageDataCopilotUsage, x)], self.copilot_usage) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_none, lambda x: from_str(x)], self.reasoning_effort) + return result - shutdown_type: ShutdownType | None = None - """Whether the session ended normally ("routine") or due to a crash/fatal error ("error")""" - system_tokens: float | None = None - """System message token count at shutdown - - Token count from system message(s) - - Token count from system message(s) at compaction start - - Token count from system message(s) after compaction - """ - tool_definitions_tokens: float | None = None - """Tool definitions token count at shutdown - - Token count from tool definitions - - Token count from tool definitions at compaction start - - Token count from tool definitions after compaction - """ - total_api_duration_ms: float | None = None - """Cumulative time spent in API calls during the session, in milliseconds""" - - total_premium_requests: float | None = None - """Total number of premium API requests used during the session""" +@dataclass +class AbortData: + """Turn abort information including the reason for termination""" + reason: str - base_commit: str | None = None - """Base commit of current git branch at session start time""" + @staticmethod + def from_dict(obj: Any) -> "AbortData": + assert isinstance(obj, dict) + reason = from_str(obj.get("reason")) + return AbortData( + reason=reason, + ) - branch: str | None = None - """Current git branch name""" + def to_dict(self) -> dict: + result: dict = {} + result["reason"] = from_str(self.reason) + return result - cwd: str | None = None - """Current working directory path""" - git_root: str | None = None - """Root directory of the git repository, resolved via git rev-parse""" +@dataclass +class ToolUserRequestedData: + """User-initiated tool invocation request with tool name and arguments""" + tool_call_id: str + tool_name: str + arguments: Any = None - head_commit: str | None = None - """Head commit of current git branch at session start time""" + @staticmethod + def from_dict(obj: Any) -> "ToolUserRequestedData": + assert isinstance(obj, dict) + tool_call_id = from_str(obj.get("toolCallId")) + tool_name = from_str(obj.get("toolName")) + arguments = obj.get("arguments") + return ToolUserRequestedData( + tool_call_id=tool_call_id, + tool_name=tool_name, + arguments=arguments, + ) - host_type: ContextChangedHostType | None = None - """Hosting platform type of the repository (github or ado)""" + def to_dict(self) -> dict: + result: dict = {} + result["toolCallId"] = from_str(self.tool_call_id) + result["toolName"] = from_str(self.tool_name) + if self.arguments is not None: + result["arguments"] = self.arguments + return result - is_initial: bool | None = None - """Whether this is the first usage_info event emitted in this session""" - messages_length: float | None = None - """Current number of messages in the conversation""" +@dataclass +class ToolExecutionStartData: + """Tool execution startup details including MCP server information when applicable""" + tool_call_id: str + tool_name: str + arguments: Any = None + mcp_server_name: str | None = None + mcp_tool_name: str | None = None + parent_tool_call_id: str | None = None - checkpoint_number: float | None = None - """Checkpoint snapshot number created for recovery""" + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionStartData": + assert isinstance(obj, dict) + tool_call_id = from_str(obj.get("toolCallId")) + tool_name = from_str(obj.get("toolName")) + arguments = obj.get("arguments") + mcp_server_name = from_union([from_none, lambda x: from_str(x)], obj.get("mcpServerName")) + mcp_tool_name = from_union([from_none, lambda x: from_str(x)], obj.get("mcpToolName")) + parent_tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("parentToolCallId")) + return ToolExecutionStartData( + tool_call_id=tool_call_id, + tool_name=tool_name, + arguments=arguments, + mcp_server_name=mcp_server_name, + mcp_tool_name=mcp_tool_name, + parent_tool_call_id=parent_tool_call_id, + ) - checkpoint_path: str | None = None - """File path where the checkpoint was stored""" - - compaction_tokens_used: CompactionCompleteCompactionTokensUsed | None = None - """Token usage breakdown for the compaction LLM call""" - - error: Error | str | None = None - """Error message if compaction failed - - Error details when the tool execution failed - - Error message describing why the sub-agent failed - - Error details when the hook failed - """ - messages_removed: float | None = None - """Number of messages removed during compaction""" - - post_compaction_tokens: float | None = None - """Total tokens in conversation after compaction""" + def to_dict(self) -> dict: + result: dict = {} + result["toolCallId"] = from_str(self.tool_call_id) + result["toolName"] = from_str(self.tool_name) + if self.arguments is not None: + result["arguments"] = self.arguments + if self.mcp_server_name is not None: + result["mcpServerName"] = from_union([from_none, lambda x: from_str(x)], self.mcp_server_name) + if self.mcp_tool_name is not None: + result["mcpToolName"] = from_union([from_none, lambda x: from_str(x)], self.mcp_tool_name) + if self.parent_tool_call_id is not None: + result["parentToolCallId"] = from_union([from_none, lambda x: from_str(x)], self.parent_tool_call_id) + return result - pre_compaction_messages_length: float | None = None - """Number of messages before compaction""" - pre_compaction_tokens: float | None = None - """Total tokens in conversation before compaction""" +@dataclass +class ToolExecutionPartialResultData: + """Streaming tool execution output for incremental result display""" + tool_call_id: str + partial_output: str - request_id: str | None = None - """GitHub request tracing ID (x-github-request-id header) for the compaction LLM call - - GitHub request tracing ID (x-github-request-id header) for correlating with server-side - logs - - Unique identifier for this permission request; used to respond via - session.respondToPermission() - - Request ID of the resolved permission request; clients should dismiss any UI for this - request - - Unique identifier for this input request; used to respond via - session.respondToUserInput() - - Request ID of the resolved user input request; clients should dismiss any UI for this - request - - Unique identifier for this elicitation request; used to respond via - session.respondToElicitation() - - Request ID of the resolved elicitation request; clients should dismiss any UI for this - request - - Unique identifier for this sampling request; used to respond via - session.respondToSampling() - - Request ID of the resolved sampling request; clients should dismiss any UI for this - request - - Unique identifier for this OAuth request; used to respond via - session.respondToMcpOAuth() - - Request ID of the resolved OAuth request - - Unique identifier for this request; used to respond via session.respondToExternalTool() - - Request ID of the resolved external tool request; clients should dismiss any UI for this - request - - Unique identifier for this request; used to respond via session.respondToQueuedCommand() - - Unique identifier; used to respond via session.commands.handlePendingCommand() - - Request ID of the resolved command request; clients should dismiss any UI for this - request - - Unique identifier for this request; used to respond via session.respondToExitPlanMode() - - Request ID of the resolved exit plan mode request; clients should dismiss any UI for this - request - """ - success: bool | None = None - """Whether compaction completed successfully - - Whether the tool call succeeded. False when validation failed (e.g., invalid arguments) - - Whether the tool execution completed successfully - - Whether the hook completed successfully - """ - summary_content: str | None = None - """LLM-generated summary of the compacted conversation history""" + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionPartialResultData": + assert isinstance(obj, dict) + tool_call_id = from_str(obj.get("toolCallId")) + partial_output = from_str(obj.get("partialOutput")) + return ToolExecutionPartialResultData( + tool_call_id=tool_call_id, + partial_output=partial_output, + ) - tokens_removed: float | None = None - """Number of tokens removed during compaction""" + def to_dict(self) -> dict: + result: dict = {} + result["toolCallId"] = from_str(self.tool_call_id) + result["partialOutput"] = from_str(self.partial_output) + return result - agent_mode: UserMessageAgentMode | None = None - """The agent mode that was active when this message was sent""" - attachments: list[UserMessageAttachment] | None = None - """Files, selections, or GitHub references attached to the message""" - - content: str | dict[str, float | bool | list[str] | str] | None = None - """The user's message text as displayed in the timeline - - The complete extended thinking text from the model - - The assistant's text response content - - Full content of the skill file, injected into the conversation for the model - - The system or developer prompt text - - The notification text, typically wrapped in XML tags - - The submitted form data when action is 'accept'; keys match the requested schema fields - """ - interaction_id: str | None = None - """CAPI interaction ID for correlating this user message with its turn - - CAPI interaction ID for correlating this turn with upstream telemetry - - CAPI interaction ID for correlating this message with upstream telemetry - - CAPI interaction ID for correlating this tool execution with upstream telemetry - """ - source: str | None = None - """Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected - messages that should be hidden from the user) - """ - transformed_content: str | None = None - """Transformed version of the message sent to the model, with XML wrapping, timestamps, and - other augmentations for prompt caching - """ - turn_id: str | None = None - """Identifier for this turn within the agentic loop, typically a stringified turn number - - Identifier of the turn that has ended, matching the corresponding assistant.turn_start - event - """ - intent: str | None = None - """Short description of what the agent is currently doing or planning to do""" - - reasoning_id: str | None = None - """Unique identifier for this reasoning block - - Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning - event - """ - delta_content: str | None = None - """Incremental text chunk to append to the reasoning content - - Incremental text chunk to append to the message content - """ - total_response_size_bytes: float | None = None - """Cumulative total bytes received from the streaming response so far""" +@dataclass +class ToolExecutionProgressData: + """Tool execution progress notification with status message""" + tool_call_id: str + progress_message: str - encrypted_content: str | None = None - """Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume.""" + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionProgressData": + assert isinstance(obj, dict) + tool_call_id = from_str(obj.get("toolCallId")) + progress_message = from_str(obj.get("progressMessage")) + return ToolExecutionProgressData( + tool_call_id=tool_call_id, + progress_message=progress_message, + ) - message_id: str | None = None - """Unique identifier for this assistant message - - Message ID this delta belongs to, matching the corresponding assistant.message event - """ - output_tokens: float | None = None - """Actual output token count from the API response (completion_tokens), used for accurate - token accounting - - Number of output tokens produced - """ - parent_tool_call_id: str | None = None - """Tool call ID of the parent tool invocation when this event originates from a sub-agent - - Parent tool call ID when this usage originates from a sub-agent - """ - phase: str | None = None - """Generation phase for phased-output models (e.g., thinking vs. response phases)""" + def to_dict(self) -> dict: + result: dict = {} + result["toolCallId"] = from_str(self.tool_call_id) + result["progressMessage"] = from_str(self.progress_message) + return result - reasoning_opaque: str | None = None - """Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped - on resume. - """ - reasoning_text: str | None = None - """Readable reasoning text from the model's extended thinking""" - tool_requests: list[AssistantMessageToolRequest] | None = None - """Tool invocations requested by the assistant in this message""" +@dataclass +class ToolExecutionCompleteDataResultContentsItemIconsItem: + """Icon image for a resource""" + src: str + mime_type: str | None = None + sizes: list[str] | None = None + theme: ToolExecutionCompleteDataResultContentsItemIconsItemTheme | None = None - api_call_id: str | None = None - """Completion ID from the model provider (e.g., chatcmpl-abc123)""" + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteDataResultContentsItemIconsItem": + assert isinstance(obj, dict) + src = from_str(obj.get("src")) + mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType")) + sizes = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], obj.get("sizes")) + theme = from_union([from_none, lambda x: parse_enum(ToolExecutionCompleteDataResultContentsItemIconsItemTheme, x)], obj.get("theme")) + return ToolExecutionCompleteDataResultContentsItemIconsItem( + src=src, + mime_type=mime_type, + sizes=sizes, + theme=theme, + ) - cache_read_tokens: float | None = None - """Number of tokens read from prompt cache""" + def to_dict(self) -> dict: + result: dict = {} + result["src"] = from_str(self.src) + if self.mime_type is not None: + result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type) + if self.sizes is not None: + result["sizes"] = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], self.sizes) + if self.theme is not None: + result["theme"] = from_union([from_none, lambda x: to_enum(ToolExecutionCompleteDataResultContentsItemIconsItemTheme, x)], self.theme) + return result - cache_write_tokens: float | None = None - """Number of tokens written to prompt cache""" - copilot_usage: AssistantUsageCopilotUsage | None = None - """Per-request cost and usage data from the CAPI copilot_usage response field""" +@dataclass +class ToolExecutionCompleteDataResultContentsItem: + """A content block within a tool result, which may be text, terminal output, image, audio, or a resource""" + type: ToolExecutionCompleteDataResultContentsItemType + text: str | None = None + exit_code: float | None = None + cwd: str | None = None + data: str | None = None + mime_type: str | None = None + icons: list[ToolExecutionCompleteDataResultContentsItemIconsItem] | None = None + name: str | None = None + title: str | None = None + uri: str | None = None + description: str | None = None + size: float | None = None + resource: Any = None - cost: float | None = None - """Model multiplier cost for billing purposes""" + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteDataResultContentsItem": + assert isinstance(obj, dict) + type = parse_enum(ToolExecutionCompleteDataResultContentsItemType, obj.get("type")) + text = from_union([from_none, lambda x: from_str(x)], obj.get("text")) + exit_code = from_union([from_none, lambda x: from_float(x)], obj.get("exitCode")) + cwd = from_union([from_none, lambda x: from_str(x)], obj.get("cwd")) + data = from_union([from_none, lambda x: from_str(x)], obj.get("data")) + mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType")) + icons = from_union([from_none, lambda x: from_list(lambda x: ToolExecutionCompleteDataResultContentsItemIconsItem.from_dict(x), x)], obj.get("icons")) + name = from_union([from_none, lambda x: from_str(x)], obj.get("name")) + title = from_union([from_none, lambda x: from_str(x)], obj.get("title")) + uri = from_union([from_none, lambda x: from_str(x)], obj.get("uri")) + description = from_union([from_none, lambda x: from_str(x)], obj.get("description")) + size = from_union([from_none, lambda x: from_float(x)], obj.get("size")) + resource = obj.get("resource") + return ToolExecutionCompleteDataResultContentsItem( + type=type, + text=text, + exit_code=exit_code, + cwd=cwd, + data=data, + mime_type=mime_type, + icons=icons, + name=name, + title=title, + uri=uri, + description=description, + size=size, + resource=resource, + ) - duration: float | None = None - """Duration of the API call in milliseconds""" + def to_dict(self) -> dict: + result: dict = {} + result["type"] = to_enum(ToolExecutionCompleteDataResultContentsItemType, self.type) + if self.text is not None: + result["text"] = from_union([from_none, lambda x: from_str(x)], self.text) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, lambda x: to_float(x)], self.exit_code) + if self.cwd is not None: + result["cwd"] = from_union([from_none, lambda x: from_str(x)], self.cwd) + if self.data is not None: + result["data"] = from_union([from_none, lambda x: from_str(x)], self.data) + if self.mime_type is not None: + result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type) + if self.icons is not None: + result["icons"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ToolExecutionCompleteDataResultContentsItemIconsItem, x), x)], self.icons) + if self.name is not None: + result["name"] = from_union([from_none, lambda x: from_str(x)], self.name) + if self.title is not None: + result["title"] = from_union([from_none, lambda x: from_str(x)], self.title) + if self.uri is not None: + result["uri"] = from_union([from_none, lambda x: from_str(x)], self.uri) + if self.description is not None: + result["description"] = from_union([from_none, lambda x: from_str(x)], self.description) + if self.size is not None: + result["size"] = from_union([from_none, lambda x: to_float(x)], self.size) + if self.resource is not None: + result["resource"] = self.resource + return result - initiator: str | None = None - """What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for - user-initiated calls - """ - input_tokens: float | None = None - """Number of input tokens consumed""" - inter_token_latency_ms: float | None = None - """Average inter-token latency in milliseconds. Only available for streaming requests""" +@dataclass +class ToolExecutionCompleteDataResult: + """Tool execution result on success""" + content: str + detailed_content: str | None = None + contents: list[ToolExecutionCompleteDataResultContentsItem] | None = None - model: str | None = None - """Model identifier used for this API call - - Model identifier that generated this tool call - - Model used by the sub-agent - - Model used by the sub-agent (if any model calls succeeded before failure) - """ - quota_snapshots: dict[str, AssistantUsageQuotaSnapshot] | None = None - """Per-quota resource usage snapshots, keyed by quota identifier""" - - reasoning_tokens: float | None = None - """Number of output tokens used for reasoning (e.g., chain-of-thought)""" + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteDataResult": + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + detailed_content = from_union([from_none, lambda x: from_str(x)], obj.get("detailedContent")) + contents = from_union([from_none, lambda x: from_list(ToolExecutionCompleteDataResultContentsItem.from_dict, x)], obj.get("contents")) + return ToolExecutionCompleteDataResult( + content=content, + detailed_content=detailed_content, + contents=contents, + ) - ttft_ms: float | None = None - """Time to first token in milliseconds. Only available for streaming requests""" + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + if self.detailed_content is not None: + result["detailedContent"] = from_union([from_none, lambda x: from_str(x)], self.detailed_content) + if self.contents is not None: + result["contents"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ToolExecutionCompleteDataResultContentsItem, x), x)], self.contents) + return result - reason: str | None = None - """Reason the current turn was aborted (e.g., "user initiated")""" - arguments: Any = None - """Arguments for the tool invocation - - Arguments passed to the tool - - Arguments to pass to the external tool - """ - tool_call_id: str | None = None - """Unique identifier for this tool call - - Tool call ID this partial result belongs to - - Tool call ID this progress notification belongs to - - Unique identifier for the completed tool call - - Tool call ID of the parent tool invocation that spawned this sub-agent - - The LLM-assigned tool call ID that triggered this request; used by remote UIs to - correlate responses - - Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id - for remote UIs - - Tool call ID assigned to this external tool invocation - """ - tool_name: str | None = None - """Name of the tool the user wants to invoke - - Name of the tool being executed - - Name of the external tool to invoke - """ - mcp_server_name: str | None = None - """Name of the MCP server hosting this tool, when the tool is an MCP tool""" +@dataclass +class ToolExecutionCompleteDataError: + """Error details when the tool execution failed""" + message: str + code: str | None = None - mcp_tool_name: str | None = None - """Original tool name on the MCP server, when the tool is an MCP tool""" + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteDataError": + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + code = from_union([from_none, lambda x: from_str(x)], obj.get("code")) + return ToolExecutionCompleteDataError( + message=message, + code=code, + ) - partial_output: str | None = None - """Incremental output chunk from the running tool""" + def to_dict(self) -> dict: + result: dict = {} + result["message"] = from_str(self.message) + if self.code is not None: + result["code"] = from_union([from_none, lambda x: from_str(x)], self.code) + return result - progress_message: str | None = None - """Human-readable progress status message (e.g., from an MCP server)""" +@dataclass +class ToolExecutionCompleteData: + """Tool execution completion results including success status, detailed output, and error information""" + tool_call_id: str + success: bool + model: str | None = None + interaction_id: str | None = None is_user_requested: bool | None = None - """Whether this tool call was explicitly requested by the user rather than the assistant""" - - result: Result | None = None - """Tool execution result on success - - The result of the permission request - """ + result: ToolExecutionCompleteDataResult | None = None + error: ToolExecutionCompleteDataError | None = None tool_telemetry: dict[str, Any] | None = None - """Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts)""" + parent_tool_call_id: str | None = None - allowed_tools: list[str] | None = None - """Tool names that should be auto-approved when this skill is active""" + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteData": + assert isinstance(obj, dict) + tool_call_id = from_str(obj.get("toolCallId")) + success = from_bool(obj.get("success")) + model = from_union([from_none, lambda x: from_str(x)], obj.get("model")) + interaction_id = from_union([from_none, lambda x: from_str(x)], obj.get("interactionId")) + is_user_requested = from_union([from_none, lambda x: from_bool(x)], obj.get("isUserRequested")) + result = from_union([from_none, lambda x: ToolExecutionCompleteDataResult.from_dict(x)], obj.get("result")) + error = from_union([from_none, lambda x: ToolExecutionCompleteDataError.from_dict(x)], obj.get("error")) + tool_telemetry = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("toolTelemetry")) + parent_tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("parentToolCallId")) + return ToolExecutionCompleteData( + tool_call_id=tool_call_id, + success=success, + model=model, + interaction_id=interaction_id, + is_user_requested=is_user_requested, + result=result, + error=error, + tool_telemetry=tool_telemetry, + parent_tool_call_id=parent_tool_call_id, + ) - description: str | None = None - """Description of the skill from its SKILL.md frontmatter""" + def to_dict(self) -> dict: + result: dict = {} + result["toolCallId"] = from_str(self.tool_call_id) + result["success"] = from_bool(self.success) + if self.model is not None: + result["model"] = from_union([from_none, lambda x: from_str(x)], self.model) + if self.interaction_id is not None: + result["interactionId"] = from_union([from_none, lambda x: from_str(x)], self.interaction_id) + if self.is_user_requested is not None: + result["isUserRequested"] = from_union([from_none, lambda x: from_bool(x)], self.is_user_requested) + if self.result is not None: + result["result"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteDataResult, x)], self.result) + if self.error is not None: + result["error"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteDataError, x)], self.error) + if self.tool_telemetry is not None: + result["toolTelemetry"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.tool_telemetry) + if self.parent_tool_call_id is not None: + result["parentToolCallId"] = from_union([from_none, lambda x: from_str(x)], self.parent_tool_call_id) + return result - name: str | None = None - """Name of the invoked skill - - Optional name identifier for the message source - """ - plugin_name: str | None = None - """Name of the plugin this skill originated from, when applicable""" +@dataclass +class SkillInvokedData: + """Skill invocation details including content, allowed tools, and plugin metadata""" + name: str + path: str + content: str + allowed_tools: list[str] | None = None + plugin_name: str | None = None plugin_version: str | None = None - """Version of the plugin this skill originated from, when applicable""" - - agent_description: str | None = None - """Description of what the sub-agent does""" - - agent_display_name: str | None = None - """Human-readable display name of the sub-agent - - Human-readable display name of the selected custom agent - """ - agent_name: str | None = None - """Internal name of the sub-agent - - Internal name of the selected custom agent - """ - duration_ms: float | None = None - """Wall-clock duration of the sub-agent execution in milliseconds""" + description: str | None = None - total_tokens: float | None = None - """Total tokens (input + output) consumed by the sub-agent - - Total tokens (input + output) consumed before the sub-agent failed - """ - total_tool_calls: float | None = None - """Total number of tool calls made by the sub-agent - - Total number of tool calls made before the sub-agent failed - """ - tools: list[str] | None = None - """List of tool names available to this agent, or null for all tools""" - - hook_invocation_id: str | None = None - """Unique identifier for this hook invocation - - Identifier matching the corresponding hook.start event - """ - hook_type: str | None = None - """Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") - - Type of hook that was invoked (e.g., "preToolUse", "postToolUse", "sessionStart") - """ - input: Any = None - """Input data passed to the hook""" + @staticmethod + def from_dict(obj: Any) -> "SkillInvokedData": + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + path = from_str(obj.get("path")) + content = from_str(obj.get("content")) + allowed_tools = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], obj.get("allowedTools")) + plugin_name = from_union([from_none, lambda x: from_str(x)], obj.get("pluginName")) + plugin_version = from_union([from_none, lambda x: from_str(x)], obj.get("pluginVersion")) + description = from_union([from_none, lambda x: from_str(x)], obj.get("description")) + return SkillInvokedData( + name=name, + path=path, + content=content, + allowed_tools=allowed_tools, + plugin_name=plugin_name, + plugin_version=plugin_version, + description=description, + ) - output: Any = None - """Output data produced by the hook""" + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["path"] = from_str(self.path) + result["content"] = from_str(self.content) + if self.allowed_tools is not None: + result["allowedTools"] = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], self.allowed_tools) + if self.plugin_name is not None: + result["pluginName"] = from_union([from_none, lambda x: from_str(x)], self.plugin_name) + if self.plugin_version is not None: + result["pluginVersion"] = from_union([from_none, lambda x: from_str(x)], self.plugin_version) + if self.description is not None: + result["description"] = from_union([from_none, lambda x: from_str(x)], self.description) + return result - metadata: SystemMessageMetadata | None = None - """Metadata about the prompt template and its construction""" - role: SystemMessageRole | None = None - """Message role: "system" for system prompts, "developer" for developer-injected instructions""" +@dataclass +class SubagentStartedData: + """Sub-agent startup details including parent tool call and agent information""" + tool_call_id: str + agent_name: str + agent_display_name: str + agent_description: str - kind: SystemNotification | None = None - """Structured metadata identifying what triggered this notification""" + @staticmethod + def from_dict(obj: Any) -> "SubagentStartedData": + assert isinstance(obj, dict) + tool_call_id = from_str(obj.get("toolCallId")) + agent_name = from_str(obj.get("agentName")) + agent_display_name = from_str(obj.get("agentDisplayName")) + agent_description = from_str(obj.get("agentDescription")) + return SubagentStartedData( + tool_call_id=tool_call_id, + agent_name=agent_name, + agent_display_name=agent_display_name, + agent_description=agent_description, + ) - permission_request: PermissionRequest | None = None - """Details of the permission being requested""" + def to_dict(self) -> dict: + result: dict = {} + result["toolCallId"] = from_str(self.tool_call_id) + result["agentName"] = from_str(self.agent_name) + result["agentDisplayName"] = from_str(self.agent_display_name) + result["agentDescription"] = from_str(self.agent_description) + return result - resolved_by_hook: bool | None = None - """When true, this permission was already resolved by a permissionRequest hook and requires - no client action - """ - allow_freeform: bool | None = None - """Whether the user can provide a free-form text response in addition to predefined choices""" - choices: list[str] | None = None - """Predefined choices for the user to select from, if applicable""" +@dataclass +class SubagentCompletedData: + """Sub-agent completion details for successful execution""" + tool_call_id: str + agent_name: str + agent_display_name: str + model: str | None = None + total_tool_calls: float | None = None + total_tokens: float | None = None + duration_ms: float | None = None - question: str | None = None - """The question or prompt to present to the user""" + @staticmethod + def from_dict(obj: Any) -> "SubagentCompletedData": + assert isinstance(obj, dict) + tool_call_id = from_str(obj.get("toolCallId")) + agent_name = from_str(obj.get("agentName")) + agent_display_name = from_str(obj.get("agentDisplayName")) + model = from_union([from_none, lambda x: from_str(x)], obj.get("model")) + total_tool_calls = from_union([from_none, lambda x: from_float(x)], obj.get("totalToolCalls")) + total_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("totalTokens")) + duration_ms = from_union([from_none, lambda x: from_float(x)], obj.get("durationMs")) + return SubagentCompletedData( + tool_call_id=tool_call_id, + agent_name=agent_name, + agent_display_name=agent_display_name, + model=model, + total_tool_calls=total_tool_calls, + total_tokens=total_tokens, + duration_ms=duration_ms, + ) - answer: str | None = None - """The user's answer to the input request""" + def to_dict(self) -> dict: + result: dict = {} + result["toolCallId"] = from_str(self.tool_call_id) + result["agentName"] = from_str(self.agent_name) + result["agentDisplayName"] = from_str(self.agent_display_name) + if self.model is not None: + result["model"] = from_union([from_none, lambda x: from_str(x)], self.model) + if self.total_tool_calls is not None: + result["totalToolCalls"] = from_union([from_none, lambda x: to_float(x)], self.total_tool_calls) + if self.total_tokens is not None: + result["totalTokens"] = from_union([from_none, lambda x: to_float(x)], self.total_tokens) + if self.duration_ms is not None: + result["durationMs"] = from_union([from_none, lambda x: to_float(x)], self.duration_ms) + return result - was_freeform: bool | None = None - """Whether the answer was typed as free-form text rather than selected from choices""" - elicitation_source: str | None = None - """The source that initiated the request (MCP server name, or absent for agent-initiated)""" +@dataclass +class SubagentFailedData: + """Sub-agent failure details including error message and agent information""" + tool_call_id: str + agent_name: str + agent_display_name: str + error: str + model: str | None = None + total_tool_calls: float | None = None + total_tokens: float | None = None + duration_ms: float | None = None - mode: ElicitationRequestedMode | None = None - """Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to - "form" when absent. - """ - requested_schema: ElicitationRequestedSchema | None = None - """JSON Schema describing the form fields to present to the user (form mode only)""" + @staticmethod + def from_dict(obj: Any) -> "SubagentFailedData": + assert isinstance(obj, dict) + tool_call_id = from_str(obj.get("toolCallId")) + agent_name = from_str(obj.get("agentName")) + agent_display_name = from_str(obj.get("agentDisplayName")) + error = from_str(obj.get("error")) + model = from_union([from_none, lambda x: from_str(x)], obj.get("model")) + total_tool_calls = from_union([from_none, lambda x: from_float(x)], obj.get("totalToolCalls")) + total_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("totalTokens")) + duration_ms = from_union([from_none, lambda x: from_float(x)], obj.get("durationMs")) + return SubagentFailedData( + tool_call_id=tool_call_id, + agent_name=agent_name, + agent_display_name=agent_display_name, + error=error, + model=model, + total_tool_calls=total_tool_calls, + total_tokens=total_tokens, + duration_ms=duration_ms, + ) - action: ElicitationCompletedAction | None = None - """The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" - (dismissed) - """ - mcp_request_id: float | str | None = None - """The JSON-RPC request ID from the MCP protocol""" + def to_dict(self) -> dict: + result: dict = {} + result["toolCallId"] = from_str(self.tool_call_id) + result["agentName"] = from_str(self.agent_name) + result["agentDisplayName"] = from_str(self.agent_display_name) + result["error"] = from_str(self.error) + if self.model is not None: + result["model"] = from_union([from_none, lambda x: from_str(x)], self.model) + if self.total_tool_calls is not None: + result["totalToolCalls"] = from_union([from_none, lambda x: to_float(x)], self.total_tool_calls) + if self.total_tokens is not None: + result["totalTokens"] = from_union([from_none, lambda x: to_float(x)], self.total_tokens) + if self.duration_ms is not None: + result["durationMs"] = from_union([from_none, lambda x: to_float(x)], self.duration_ms) + return result - server_name: str | None = None - """Name of the MCP server that initiated the sampling request - - Display name of the MCP server that requires OAuth - - Name of the MCP server whose status changed - """ - server_url: str | None = None - """URL of the MCP server that requires OAuth""" - - static_client_config: MCPOauthRequiredStaticClientConfig | None = None - """Static OAuth client configuration, if the server specifies one""" - traceparent: str | None = None - """W3C Trace Context traceparent header for the execute_tool span""" +@dataclass +class SubagentSelectedData: + """Custom agent selection details including name and available tools""" + agent_name: str + agent_display_name: str + tools: list[str] | None - tracestate: str | None = None - """W3C Trace Context tracestate header for the execute_tool span""" + @staticmethod + def from_dict(obj: Any) -> "SubagentSelectedData": + assert isinstance(obj, dict) + agent_name = from_str(obj.get("agentName")) + agent_display_name = from_str(obj.get("agentDisplayName")) + tools = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], obj.get("tools")) + return SubagentSelectedData( + agent_name=agent_name, + agent_display_name=agent_display_name, + tools=tools, + ) - command: str | None = None - """The slash command text to be executed (e.g., /help, /clear) - - The full command text (e.g., /deploy production) - """ - args: str | None = None - """Raw argument string after the command name""" + def to_dict(self) -> dict: + result: dict = {} + result["agentName"] = from_str(self.agent_name) + result["agentDisplayName"] = from_str(self.agent_display_name) + result["tools"] = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], self.tools) + return result - command_name: str | None = None - """Command name without leading /""" - commands: list[CommandsChangedCommand] | None = None - """Current list of registered SDK commands""" +@dataclass +class SubagentDeselectedData: + """Empty payload; the event signals that the custom agent was deselected, returning to the default agent""" + @staticmethod + def from_dict(obj: Any) -> "SubagentDeselectedData": + assert isinstance(obj, dict) + return SubagentDeselectedData() - ui: CapabilitiesChangedUI | None = None - """UI capability changes""" + def to_dict(self) -> dict: + return {} - actions: list[str] | None = None - """Available actions the user can take (e.g., approve, edit, reject)""" - plan_content: str | None = None - """Full content of the plan file""" +@dataclass +class HookStartData: + """Hook invocation start details including type and input data""" + hook_invocation_id: str + hook_type: str + input: Any = None - recommended_action: str | None = None - """The recommended action for the user to take""" + @staticmethod + def from_dict(obj: Any) -> "HookStartData": + assert isinstance(obj, dict) + hook_invocation_id = from_str(obj.get("hookInvocationId")) + hook_type = from_str(obj.get("hookType")) + input = obj.get("input") + return HookStartData( + hook_invocation_id=hook_invocation_id, + hook_type=hook_type, + input=input, + ) - approved: bool | None = None - """Whether the plan was approved by the user""" + def to_dict(self) -> dict: + result: dict = {} + result["hookInvocationId"] = from_str(self.hook_invocation_id) + result["hookType"] = from_str(self.hook_type) + if self.input is not None: + result["input"] = self.input + return result - auto_approve_edits: bool | None = None - """Whether edits should be auto-approved without confirmation""" - feedback: str | None = None - """Free-form feedback from the user if they requested changes to the plan""" +@dataclass +class HookEndDataError: + """Error details when the hook failed""" + message: str + stack: str | None = None - selected_action: str | None = None - """Which action the user selected (e.g. 'autopilot', 'interactive', 'exit_only')""" - - skills: list[SkillsLoadedSkill] | None = None - """Array of resolved skill metadata""" - - agents: list[CustomAgentsUpdatedAgent] | None = None - """Array of loaded custom agent metadata""" - - errors: list[str] | None = None - """Fatal errors from agent loading""" - - warnings: list[str] | None = None - """Non-fatal warnings from agent loading""" - - servers: list[MCPServersLoadedServer] | None = None - """Array of MCP server status summaries""" - - status: MCPServerStatus | None = None - """New connection status: connected, failed, needs-auth, pending, disabled, or not_configured""" - - extensions: list[ExtensionsLoadedExtension] | None = None - """Array of discovered extensions and their status""" - - @staticmethod - def from_dict(obj: Any) -> 'Data': - assert isinstance(obj, dict) - already_in_use = from_union([from_bool, from_none], obj.get("alreadyInUse")) - context = from_union([Context.from_dict, from_str, from_none], obj.get("context")) - copilot_version = from_union([from_str, from_none], obj.get("copilotVersion")) - producer = from_union([from_str, from_none], obj.get("producer")) - reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) - remote_steerable = from_union([from_bool, from_none], obj.get("remoteSteerable")) - selected_model = from_union([from_str, from_none], obj.get("selectedModel")) - session_id = from_union([from_str, from_none], obj.get("sessionId")) - start_time = from_union([from_datetime, from_none], obj.get("startTime")) - version = from_union([from_float, from_none], obj.get("version")) - event_count = from_union([from_float, from_none], obj.get("eventCount")) - resume_time = from_union([from_datetime, from_none], obj.get("resumeTime")) - error_type = from_union([from_str, from_none], obj.get("errorType")) - message = from_union([from_str, from_none], obj.get("message")) - provider_call_id = from_union([from_str, from_none], obj.get("providerCallId")) - stack = from_union([from_str, from_none], obj.get("stack")) - status_code = from_union([from_int, from_none], obj.get("statusCode")) - url = from_union([from_str, from_none], obj.get("url")) - aborted = from_union([from_bool, from_none], obj.get("aborted")) - title = from_union([from_str, from_none], obj.get("title")) - info_type = from_union([from_str, from_none], obj.get("infoType")) - warning_type = from_union([from_str, from_none], obj.get("warningType")) - new_model = from_union([from_str, from_none], obj.get("newModel")) - previous_model = from_union([from_str, from_none], obj.get("previousModel")) - previous_reasoning_effort = from_union([from_str, from_none], obj.get("previousReasoningEffort")) - new_mode = from_union([from_str, from_none], obj.get("newMode")) - previous_mode = from_union([from_str, from_none], obj.get("previousMode")) - operation = from_union([ChangedOperation, from_none], obj.get("operation")) - path = from_union([from_str, from_none], obj.get("path")) - handoff_time = from_union([from_datetime, from_none], obj.get("handoffTime")) - host = from_union([from_str, from_none], obj.get("host")) - remote_session_id = from_union([from_str, from_none], obj.get("remoteSessionId")) - repository = from_union([HandoffRepository.from_dict, from_str, from_none], obj.get("repository")) - source_type = from_union([HandoffSourceType, from_none], obj.get("sourceType")) - summary = from_union([from_str, from_none], obj.get("summary")) - messages_removed_during_truncation = from_union([from_float, from_none], obj.get("messagesRemovedDuringTruncation")) - performed_by = from_union([from_str, from_none], obj.get("performedBy")) - post_truncation_messages_length = from_union([from_float, from_none], obj.get("postTruncationMessagesLength")) - post_truncation_tokens_in_messages = from_union([from_float, from_none], obj.get("postTruncationTokensInMessages")) - pre_truncation_messages_length = from_union([from_float, from_none], obj.get("preTruncationMessagesLength")) - pre_truncation_tokens_in_messages = from_union([from_float, from_none], obj.get("preTruncationTokensInMessages")) - token_limit = from_union([from_float, from_none], obj.get("tokenLimit")) - tokens_removed_during_truncation = from_union([from_float, from_none], obj.get("tokensRemovedDuringTruncation")) - events_removed = from_union([from_float, from_none], obj.get("eventsRemoved")) - up_to_event_id = from_union([from_str, from_none], obj.get("upToEventId")) - code_changes = from_union([ShutdownCodeChanges.from_dict, from_none], obj.get("codeChanges")) - conversation_tokens = from_union([from_float, from_none], obj.get("conversationTokens")) - current_model = from_union([from_str, from_none], obj.get("currentModel")) - current_tokens = from_union([from_float, from_none], obj.get("currentTokens")) - error_reason = from_union([from_str, from_none], obj.get("errorReason")) - model_metrics = from_union([lambda x: from_dict(ShutdownModelMetric.from_dict, x), from_none], obj.get("modelMetrics")) - session_start_time = from_union([from_float, from_none], obj.get("sessionStartTime")) - shutdown_type = from_union([ShutdownType, from_none], obj.get("shutdownType")) - system_tokens = from_union([from_float, from_none], obj.get("systemTokens")) - tool_definitions_tokens = from_union([from_float, from_none], obj.get("toolDefinitionsTokens")) - total_api_duration_ms = from_union([from_float, from_none], obj.get("totalApiDurationMs")) - total_premium_requests = from_union([from_float, from_none], obj.get("totalPremiumRequests")) - base_commit = from_union([from_str, from_none], obj.get("baseCommit")) - branch = from_union([from_str, from_none], obj.get("branch")) - cwd = from_union([from_str, from_none], obj.get("cwd")) - git_root = from_union([from_str, from_none], obj.get("gitRoot")) - head_commit = from_union([from_str, from_none], obj.get("headCommit")) - host_type = from_union([ContextChangedHostType, from_none], obj.get("hostType")) - is_initial = from_union([from_bool, from_none], obj.get("isInitial")) - messages_length = from_union([from_float, from_none], obj.get("messagesLength")) - checkpoint_number = from_union([from_float, from_none], obj.get("checkpointNumber")) - checkpoint_path = from_union([from_str, from_none], obj.get("checkpointPath")) - compaction_tokens_used = from_union([CompactionCompleteCompactionTokensUsed.from_dict, from_none], obj.get("compactionTokensUsed")) - error = from_union([Error.from_dict, from_str, from_none], obj.get("error")) - messages_removed = from_union([from_float, from_none], obj.get("messagesRemoved")) - post_compaction_tokens = from_union([from_float, from_none], obj.get("postCompactionTokens")) - pre_compaction_messages_length = from_union([from_float, from_none], obj.get("preCompactionMessagesLength")) - pre_compaction_tokens = from_union([from_float, from_none], obj.get("preCompactionTokens")) - request_id = from_union([from_str, from_none], obj.get("requestId")) - success = from_union([from_bool, from_none], obj.get("success")) - summary_content = from_union([from_str, from_none], obj.get("summaryContent")) - tokens_removed = from_union([from_float, from_none], obj.get("tokensRemoved")) - agent_mode = from_union([UserMessageAgentMode, from_none], obj.get("agentMode")) - attachments = from_union([lambda x: from_list(UserMessageAttachment.from_dict, x), from_none], obj.get("attachments")) - content = from_union([from_str, lambda x: from_dict(lambda x: from_union([from_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], obj.get("content")) - interaction_id = from_union([from_str, from_none], obj.get("interactionId")) - source = from_union([from_str, from_none], obj.get("source")) - transformed_content = from_union([from_str, from_none], obj.get("transformedContent")) - turn_id = from_union([from_str, from_none], obj.get("turnId")) - intent = from_union([from_str, from_none], obj.get("intent")) - reasoning_id = from_union([from_str, from_none], obj.get("reasoningId")) - delta_content = from_union([from_str, from_none], obj.get("deltaContent")) - total_response_size_bytes = from_union([from_float, from_none], obj.get("totalResponseSizeBytes")) - encrypted_content = from_union([from_str, from_none], obj.get("encryptedContent")) - message_id = from_union([from_str, from_none], obj.get("messageId")) - output_tokens = from_union([from_float, from_none], obj.get("outputTokens")) - parent_tool_call_id = from_union([from_str, from_none], obj.get("parentToolCallId")) - phase = from_union([from_str, from_none], obj.get("phase")) - reasoning_opaque = from_union([from_str, from_none], obj.get("reasoningOpaque")) - reasoning_text = from_union([from_str, from_none], obj.get("reasoningText")) - tool_requests = from_union([lambda x: from_list(AssistantMessageToolRequest.from_dict, x), from_none], obj.get("toolRequests")) - api_call_id = from_union([from_str, from_none], obj.get("apiCallId")) - cache_read_tokens = from_union([from_float, from_none], obj.get("cacheReadTokens")) - cache_write_tokens = from_union([from_float, from_none], obj.get("cacheWriteTokens")) - copilot_usage = from_union([AssistantUsageCopilotUsage.from_dict, from_none], obj.get("copilotUsage")) - cost = from_union([from_float, from_none], obj.get("cost")) - duration = from_union([from_float, from_none], obj.get("duration")) - initiator = from_union([from_str, from_none], obj.get("initiator")) - input_tokens = from_union([from_float, from_none], obj.get("inputTokens")) - inter_token_latency_ms = from_union([from_float, from_none], obj.get("interTokenLatencyMs")) - model = from_union([from_str, from_none], obj.get("model")) - quota_snapshots = from_union([lambda x: from_dict(AssistantUsageQuotaSnapshot.from_dict, x), from_none], obj.get("quotaSnapshots")) - reasoning_tokens = from_union([from_float, from_none], obj.get("reasoningTokens")) - ttft_ms = from_union([from_float, from_none], obj.get("ttftMs")) - reason = from_union([from_str, from_none], obj.get("reason")) - arguments = obj.get("arguments") - tool_call_id = from_union([from_str, from_none], obj.get("toolCallId")) - tool_name = from_union([from_str, from_none], obj.get("toolName")) - mcp_server_name = from_union([from_str, from_none], obj.get("mcpServerName")) - mcp_tool_name = from_union([from_str, from_none], obj.get("mcpToolName")) - partial_output = from_union([from_str, from_none], obj.get("partialOutput")) - progress_message = from_union([from_str, from_none], obj.get("progressMessage")) - is_user_requested = from_union([from_bool, from_none], obj.get("isUserRequested")) - result = from_union([Result.from_dict, from_none], obj.get("result")) - tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry")) - allowed_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("allowedTools")) - description = from_union([from_str, from_none], obj.get("description")) - name = from_union([from_str, from_none], obj.get("name")) - plugin_name = from_union([from_str, from_none], obj.get("pluginName")) - plugin_version = from_union([from_str, from_none], obj.get("pluginVersion")) - agent_description = from_union([from_str, from_none], obj.get("agentDescription")) - agent_display_name = from_union([from_str, from_none], obj.get("agentDisplayName")) - agent_name = from_union([from_str, from_none], obj.get("agentName")) - duration_ms = from_union([from_float, from_none], obj.get("durationMs")) - total_tokens = from_union([from_float, from_none], obj.get("totalTokens")) - total_tool_calls = from_union([from_float, from_none], obj.get("totalToolCalls")) - tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tools")) - hook_invocation_id = from_union([from_str, from_none], obj.get("hookInvocationId")) - hook_type = from_union([from_str, from_none], obj.get("hookType")) - input = obj.get("input") - output = obj.get("output") - metadata = from_union([SystemMessageMetadata.from_dict, from_none], obj.get("metadata")) - role = from_union([SystemMessageRole, from_none], obj.get("role")) - kind = from_union([SystemNotification.from_dict, from_none], obj.get("kind")) - permission_request = from_union([PermissionRequest.from_dict, from_none], obj.get("permissionRequest")) - resolved_by_hook = from_union([from_bool, from_none], obj.get("resolvedByHook")) - allow_freeform = from_union([from_bool, from_none], obj.get("allowFreeform")) - choices = from_union([lambda x: from_list(from_str, x), from_none], obj.get("choices")) - question = from_union([from_str, from_none], obj.get("question")) - answer = from_union([from_str, from_none], obj.get("answer")) - was_freeform = from_union([from_bool, from_none], obj.get("wasFreeform")) - elicitation_source = from_union([from_str, from_none], obj.get("elicitationSource")) - mode = from_union([ElicitationRequestedMode, from_none], obj.get("mode")) - requested_schema = from_union([ElicitationRequestedSchema.from_dict, from_none], obj.get("requestedSchema")) - action = from_union([ElicitationCompletedAction, from_none], obj.get("action")) - mcp_request_id = from_union([from_float, from_str, from_none], obj.get("mcpRequestId")) - server_name = from_union([from_str, from_none], obj.get("serverName")) - server_url = from_union([from_str, from_none], obj.get("serverUrl")) - static_client_config = from_union([MCPOauthRequiredStaticClientConfig.from_dict, from_none], obj.get("staticClientConfig")) - traceparent = from_union([from_str, from_none], obj.get("traceparent")) - tracestate = from_union([from_str, from_none], obj.get("tracestate")) - command = from_union([from_str, from_none], obj.get("command")) - args = from_union([from_str, from_none], obj.get("args")) - command_name = from_union([from_str, from_none], obj.get("commandName")) - commands = from_union([lambda x: from_list(CommandsChangedCommand.from_dict, x), from_none], obj.get("commands")) - ui = from_union([CapabilitiesChangedUI.from_dict, from_none], obj.get("ui")) - actions = from_union([lambda x: from_list(from_str, x), from_none], obj.get("actions")) - plan_content = from_union([from_str, from_none], obj.get("planContent")) - recommended_action = from_union([from_str, from_none], obj.get("recommendedAction")) - approved = from_union([from_bool, from_none], obj.get("approved")) - auto_approve_edits = from_union([from_bool, from_none], obj.get("autoApproveEdits")) - feedback = from_union([from_str, from_none], obj.get("feedback")) - selected_action = from_union([from_str, from_none], obj.get("selectedAction")) - skills = from_union([lambda x: from_list(SkillsLoadedSkill.from_dict, x), from_none], obj.get("skills")) - agents = from_union([lambda x: from_list(CustomAgentsUpdatedAgent.from_dict, x), from_none], obj.get("agents")) - errors = from_union([lambda x: from_list(from_str, x), from_none], obj.get("errors")) - warnings = from_union([lambda x: from_list(from_str, x), from_none], obj.get("warnings")) - servers = from_union([lambda x: from_list(MCPServersLoadedServer.from_dict, x), from_none], obj.get("servers")) - status = from_union([MCPServerStatus, from_none], obj.get("status")) - extensions = from_union([lambda x: from_list(ExtensionsLoadedExtension.from_dict, x), from_none], obj.get("extensions")) - return Data(already_in_use, context, copilot_version, producer, reasoning_effort, remote_steerable, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, url, aborted, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, host, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, conversation_tokens, current_model, current_tokens, error_reason, model_metrics, session_start_time, shutdown_type, system_tokens, tool_definitions_tokens, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, is_initial, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, inter_token_latency_ms, model, quota_snapshots, reasoning_tokens, ttft_ms, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, description, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, duration_ms, total_tokens, total_tool_calls, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, resolved_by_hook, allow_freeform, choices, question, answer, was_freeform, elicitation_source, mode, requested_schema, action, mcp_request_id, server_name, server_url, static_client_config, traceparent, tracestate, command, args, command_name, commands, ui, actions, plan_content, recommended_action, approved, auto_approve_edits, feedback, selected_action, skills, agents, errors, warnings, servers, status, extensions) + @staticmethod + def from_dict(obj: Any) -> "HookEndDataError": + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + stack = from_union([from_none, lambda x: from_str(x)], obj.get("stack")) + return HookEndDataError( + message=message, + stack=stack, + ) def to_dict(self) -> dict: result: dict = {} - if self.already_in_use is not None: - result["alreadyInUse"] = from_union([from_bool, from_none], self.already_in_use) - if self.context is not None: - result["context"] = from_union([lambda x: to_class(Context, x), from_str, from_none], self.context) - if self.copilot_version is not None: - result["copilotVersion"] = from_union([from_str, from_none], self.copilot_version) - if self.producer is not None: - result["producer"] = from_union([from_str, from_none], self.producer) - if self.reasoning_effort is not None: - result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) - if self.remote_steerable is not None: - result["remoteSteerable"] = from_union([from_bool, from_none], self.remote_steerable) - if self.selected_model is not None: - result["selectedModel"] = from_union([from_str, from_none], self.selected_model) - if self.session_id is not None: - result["sessionId"] = from_union([from_str, from_none], self.session_id) - if self.start_time is not None: - result["startTime"] = from_union([lambda x: x.isoformat(), from_none], self.start_time) - if self.version is not None: - result["version"] = from_union([to_float, from_none], self.version) - if self.event_count is not None: - result["eventCount"] = from_union([to_float, from_none], self.event_count) - if self.resume_time is not None: - result["resumeTime"] = from_union([lambda x: x.isoformat(), from_none], self.resume_time) - if self.error_type is not None: - result["errorType"] = from_union([from_str, from_none], self.error_type) - if self.message is not None: - result["message"] = from_union([from_str, from_none], self.message) - if self.provider_call_id is not None: - result["providerCallId"] = from_union([from_str, from_none], self.provider_call_id) + result["message"] = from_str(self.message) if self.stack is not None: - result["stack"] = from_union([from_str, from_none], self.stack) - if self.status_code is not None: - result["statusCode"] = from_union([from_int, from_none], self.status_code) - if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) - if self.aborted is not None: - result["aborted"] = from_union([from_bool, from_none], self.aborted) - if self.title is not None: - result["title"] = from_union([from_str, from_none], self.title) - if self.info_type is not None: - result["infoType"] = from_union([from_str, from_none], self.info_type) - if self.warning_type is not None: - result["warningType"] = from_union([from_str, from_none], self.warning_type) - if self.new_model is not None: - result["newModel"] = from_union([from_str, from_none], self.new_model) - if self.previous_model is not None: - result["previousModel"] = from_union([from_str, from_none], self.previous_model) - if self.previous_reasoning_effort is not None: - result["previousReasoningEffort"] = from_union([from_str, from_none], self.previous_reasoning_effort) - if self.new_mode is not None: - result["newMode"] = from_union([from_str, from_none], self.new_mode) - if self.previous_mode is not None: - result["previousMode"] = from_union([from_str, from_none], self.previous_mode) - if self.operation is not None: - result["operation"] = from_union([lambda x: to_enum(ChangedOperation, x), from_none], self.operation) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.handoff_time is not None: - result["handoffTime"] = from_union([lambda x: x.isoformat(), from_none], self.handoff_time) - if self.host is not None: - result["host"] = from_union([from_str, from_none], self.host) - if self.remote_session_id is not None: - result["remoteSessionId"] = from_union([from_str, from_none], self.remote_session_id) - if self.repository is not None: - result["repository"] = from_union([lambda x: to_class(HandoffRepository, x), from_str, from_none], self.repository) - if self.source_type is not None: - result["sourceType"] = from_union([lambda x: to_enum(HandoffSourceType, x), from_none], self.source_type) - if self.summary is not None: - result["summary"] = from_union([from_str, from_none], self.summary) - if self.messages_removed_during_truncation is not None: - result["messagesRemovedDuringTruncation"] = from_union([to_float, from_none], self.messages_removed_during_truncation) - if self.performed_by is not None: - result["performedBy"] = from_union([from_str, from_none], self.performed_by) - if self.post_truncation_messages_length is not None: - result["postTruncationMessagesLength"] = from_union([to_float, from_none], self.post_truncation_messages_length) - if self.post_truncation_tokens_in_messages is not None: - result["postTruncationTokensInMessages"] = from_union([to_float, from_none], self.post_truncation_tokens_in_messages) - if self.pre_truncation_messages_length is not None: - result["preTruncationMessagesLength"] = from_union([to_float, from_none], self.pre_truncation_messages_length) - if self.pre_truncation_tokens_in_messages is not None: - result["preTruncationTokensInMessages"] = from_union([to_float, from_none], self.pre_truncation_tokens_in_messages) - if self.token_limit is not None: - result["tokenLimit"] = from_union([to_float, from_none], self.token_limit) - if self.tokens_removed_during_truncation is not None: - result["tokensRemovedDuringTruncation"] = from_union([to_float, from_none], self.tokens_removed_during_truncation) - if self.events_removed is not None: - result["eventsRemoved"] = from_union([to_float, from_none], self.events_removed) - if self.up_to_event_id is not None: - result["upToEventId"] = from_union([from_str, from_none], self.up_to_event_id) - if self.code_changes is not None: - result["codeChanges"] = from_union([lambda x: to_class(ShutdownCodeChanges, x), from_none], self.code_changes) - if self.conversation_tokens is not None: - result["conversationTokens"] = from_union([to_float, from_none], self.conversation_tokens) - if self.current_model is not None: - result["currentModel"] = from_union([from_str, from_none], self.current_model) - if self.current_tokens is not None: - result["currentTokens"] = from_union([to_float, from_none], self.current_tokens) - if self.error_reason is not None: - result["errorReason"] = from_union([from_str, from_none], self.error_reason) - if self.model_metrics is not None: - result["modelMetrics"] = from_union([lambda x: from_dict(lambda x: to_class(ShutdownModelMetric, x), x), from_none], self.model_metrics) - if self.session_start_time is not None: - result["sessionStartTime"] = from_union([to_float, from_none], self.session_start_time) - if self.shutdown_type is not None: - result["shutdownType"] = from_union([lambda x: to_enum(ShutdownType, x), from_none], self.shutdown_type) - if self.system_tokens is not None: - result["systemTokens"] = from_union([to_float, from_none], self.system_tokens) - if self.tool_definitions_tokens is not None: - result["toolDefinitionsTokens"] = from_union([to_float, from_none], self.tool_definitions_tokens) - if self.total_api_duration_ms is not None: - result["totalApiDurationMs"] = from_union([to_float, from_none], self.total_api_duration_ms) - if self.total_premium_requests is not None: - result["totalPremiumRequests"] = from_union([to_float, from_none], self.total_premium_requests) - if self.base_commit is not None: - result["baseCommit"] = from_union([from_str, from_none], self.base_commit) - if self.branch is not None: - result["branch"] = from_union([from_str, from_none], self.branch) - if self.cwd is not None: - result["cwd"] = from_union([from_str, from_none], self.cwd) - if self.git_root is not None: - result["gitRoot"] = from_union([from_str, from_none], self.git_root) - if self.head_commit is not None: - result["headCommit"] = from_union([from_str, from_none], self.head_commit) - if self.host_type is not None: - result["hostType"] = from_union([lambda x: to_enum(ContextChangedHostType, x), from_none], self.host_type) - if self.is_initial is not None: - result["isInitial"] = from_union([from_bool, from_none], self.is_initial) - if self.messages_length is not None: - result["messagesLength"] = from_union([to_float, from_none], self.messages_length) - if self.checkpoint_number is not None: - result["checkpointNumber"] = from_union([to_float, from_none], self.checkpoint_number) - if self.checkpoint_path is not None: - result["checkpointPath"] = from_union([from_str, from_none], self.checkpoint_path) - if self.compaction_tokens_used is not None: - result["compactionTokensUsed"] = from_union([lambda x: to_class(CompactionCompleteCompactionTokensUsed, x), from_none], self.compaction_tokens_used) - if self.error is not None: - result["error"] = from_union([lambda x: to_class(Error, x), from_str, from_none], self.error) - if self.messages_removed is not None: - result["messagesRemoved"] = from_union([to_float, from_none], self.messages_removed) - if self.post_compaction_tokens is not None: - result["postCompactionTokens"] = from_union([to_float, from_none], self.post_compaction_tokens) - if self.pre_compaction_messages_length is not None: - result["preCompactionMessagesLength"] = from_union([to_float, from_none], self.pre_compaction_messages_length) - if self.pre_compaction_tokens is not None: - result["preCompactionTokens"] = from_union([to_float, from_none], self.pre_compaction_tokens) - if self.request_id is not None: - result["requestId"] = from_union([from_str, from_none], self.request_id) - if self.success is not None: - result["success"] = from_union([from_bool, from_none], self.success) - if self.summary_content is not None: - result["summaryContent"] = from_union([from_str, from_none], self.summary_content) - if self.tokens_removed is not None: - result["tokensRemoved"] = from_union([to_float, from_none], self.tokens_removed) - if self.agent_mode is not None: - result["agentMode"] = from_union([lambda x: to_enum(UserMessageAgentMode, x), from_none], self.agent_mode) - if self.attachments is not None: - result["attachments"] = from_union([lambda x: from_list(lambda x: to_class(UserMessageAttachment, x), x), from_none], self.attachments) - if self.content is not None: - result["content"] = from_union([from_str, lambda x: from_dict(lambda x: from_union([to_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], self.content) - if self.interaction_id is not None: - result["interactionId"] = from_union([from_str, from_none], self.interaction_id) - if self.source is not None: - result["source"] = from_union([from_str, from_none], self.source) - if self.transformed_content is not None: - result["transformedContent"] = from_union([from_str, from_none], self.transformed_content) - if self.turn_id is not None: - result["turnId"] = from_union([from_str, from_none], self.turn_id) - if self.intent is not None: - result["intent"] = from_union([from_str, from_none], self.intent) - if self.reasoning_id is not None: - result["reasoningId"] = from_union([from_str, from_none], self.reasoning_id) - if self.delta_content is not None: - result["deltaContent"] = from_union([from_str, from_none], self.delta_content) - if self.total_response_size_bytes is not None: - result["totalResponseSizeBytes"] = from_union([to_float, from_none], self.total_response_size_bytes) - if self.encrypted_content is not None: - result["encryptedContent"] = from_union([from_str, from_none], self.encrypted_content) - if self.message_id is not None: - result["messageId"] = from_union([from_str, from_none], self.message_id) - if self.output_tokens is not None: - result["outputTokens"] = from_union([to_float, from_none], self.output_tokens) - if self.parent_tool_call_id is not None: - result["parentToolCallId"] = from_union([from_str, from_none], self.parent_tool_call_id) - if self.phase is not None: - result["phase"] = from_union([from_str, from_none], self.phase) - if self.reasoning_opaque is not None: - result["reasoningOpaque"] = from_union([from_str, from_none], self.reasoning_opaque) - if self.reasoning_text is not None: - result["reasoningText"] = from_union([from_str, from_none], self.reasoning_text) - if self.tool_requests is not None: - result["toolRequests"] = from_union([lambda x: from_list(lambda x: to_class(AssistantMessageToolRequest, x), x), from_none], self.tool_requests) - if self.api_call_id is not None: - result["apiCallId"] = from_union([from_str, from_none], self.api_call_id) - if self.cache_read_tokens is not None: - result["cacheReadTokens"] = from_union([to_float, from_none], self.cache_read_tokens) - if self.cache_write_tokens is not None: - result["cacheWriteTokens"] = from_union([to_float, from_none], self.cache_write_tokens) - if self.copilot_usage is not None: - result["copilotUsage"] = from_union([lambda x: to_class(AssistantUsageCopilotUsage, x), from_none], self.copilot_usage) - if self.cost is not None: - result["cost"] = from_union([to_float, from_none], self.cost) - if self.duration is not None: - result["duration"] = from_union([to_float, from_none], self.duration) - if self.initiator is not None: - result["initiator"] = from_union([from_str, from_none], self.initiator) - if self.input_tokens is not None: - result["inputTokens"] = from_union([to_float, from_none], self.input_tokens) - if self.inter_token_latency_ms is not None: - result["interTokenLatencyMs"] = from_union([to_float, from_none], self.inter_token_latency_ms) - if self.model is not None: - result["model"] = from_union([from_str, from_none], self.model) - if self.quota_snapshots is not None: - result["quotaSnapshots"] = from_union([lambda x: from_dict(lambda x: to_class(AssistantUsageQuotaSnapshot, x), x), from_none], self.quota_snapshots) - if self.reasoning_tokens is not None: - result["reasoningTokens"] = from_union([to_float, from_none], self.reasoning_tokens) - if self.ttft_ms is not None: - result["ttftMs"] = from_union([to_float, from_none], self.ttft_ms) - if self.reason is not None: - result["reason"] = from_union([from_str, from_none], self.reason) - if self.arguments is not None: - result["arguments"] = self.arguments - if self.tool_call_id is not None: - result["toolCallId"] = from_union([from_str, from_none], self.tool_call_id) - if self.tool_name is not None: - result["toolName"] = from_union([from_str, from_none], self.tool_name) - if self.mcp_server_name is not None: - result["mcpServerName"] = from_union([from_str, from_none], self.mcp_server_name) - if self.mcp_tool_name is not None: - result["mcpToolName"] = from_union([from_str, from_none], self.mcp_tool_name) - if self.partial_output is not None: - result["partialOutput"] = from_union([from_str, from_none], self.partial_output) - if self.progress_message is not None: - result["progressMessage"] = from_union([from_str, from_none], self.progress_message) - if self.is_user_requested is not None: - result["isUserRequested"] = from_union([from_bool, from_none], self.is_user_requested) - if self.result is not None: - result["result"] = from_union([lambda x: to_class(Result, x), from_none], self.result) - if self.tool_telemetry is not None: - result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry) - if self.allowed_tools is not None: - result["allowedTools"] = from_union([lambda x: from_list(from_str, x), from_none], self.allowed_tools) - if self.description is not None: - result["description"] = from_union([from_str, from_none], self.description) - if self.name is not None: - result["name"] = from_union([from_str, from_none], self.name) - if self.plugin_name is not None: - result["pluginName"] = from_union([from_str, from_none], self.plugin_name) - if self.plugin_version is not None: - result["pluginVersion"] = from_union([from_str, from_none], self.plugin_version) - if self.agent_description is not None: - result["agentDescription"] = from_union([from_str, from_none], self.agent_description) - if self.agent_display_name is not None: - result["agentDisplayName"] = from_union([from_str, from_none], self.agent_display_name) - if self.agent_name is not None: - result["agentName"] = from_union([from_str, from_none], self.agent_name) - if self.duration_ms is not None: - result["durationMs"] = from_union([to_float, from_none], self.duration_ms) - if self.total_tokens is not None: - result["totalTokens"] = from_union([to_float, from_none], self.total_tokens) - if self.total_tool_calls is not None: - result["totalToolCalls"] = from_union([to_float, from_none], self.total_tool_calls) - if self.tools is not None: - result["tools"] = from_union([lambda x: from_list(from_str, x), from_none], self.tools) - if self.hook_invocation_id is not None: - result["hookInvocationId"] = from_union([from_str, from_none], self.hook_invocation_id) - if self.hook_type is not None: - result["hookType"] = from_union([from_str, from_none], self.hook_type) - if self.input is not None: - result["input"] = self.input + result["stack"] = from_union([from_none, lambda x: from_str(x)], self.stack) + return result + + +@dataclass +class HookEndData: + """Hook invocation completion details including output, success status, and error information""" + hook_invocation_id: str + hook_type: str + success: bool + output: Any = None + error: HookEndDataError | None = None + + @staticmethod + def from_dict(obj: Any) -> "HookEndData": + assert isinstance(obj, dict) + hook_invocation_id = from_str(obj.get("hookInvocationId")) + hook_type = from_str(obj.get("hookType")) + success = from_bool(obj.get("success")) + output = obj.get("output") + error = from_union([from_none, lambda x: HookEndDataError.from_dict(x)], obj.get("error")) + return HookEndData( + hook_invocation_id=hook_invocation_id, + hook_type=hook_type, + success=success, + output=output, + error=error, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["hookInvocationId"] = from_str(self.hook_invocation_id) + result["hookType"] = from_str(self.hook_type) + result["success"] = from_bool(self.success) if self.output is not None: result["output"] = self.output + if self.error is not None: + result["error"] = from_union([from_none, lambda x: to_class(HookEndDataError, x)], self.error) + return result + + +@dataclass +class SystemMessageDataMetadata: + """Metadata about the prompt template and its construction""" + prompt_version: str | None = None + variables: dict[str, Any] | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemMessageDataMetadata": + assert isinstance(obj, dict) + prompt_version = from_union([from_none, lambda x: from_str(x)], obj.get("promptVersion")) + variables = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("variables")) + return SystemMessageDataMetadata( + prompt_version=prompt_version, + variables=variables, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.prompt_version is not None: + result["promptVersion"] = from_union([from_none, lambda x: from_str(x)], self.prompt_version) + if self.variables is not None: + result["variables"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.variables) + return result + + +@dataclass +class SystemMessageData: + """System or developer message content with role and optional template metadata""" + content: str + role: SystemMessageDataRole + name: str | None = None + metadata: SystemMessageDataMetadata | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemMessageData": + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + role = parse_enum(SystemMessageDataRole, obj.get("role")) + name = from_union([from_none, lambda x: from_str(x)], obj.get("name")) + metadata = from_union([from_none, lambda x: SystemMessageDataMetadata.from_dict(x)], obj.get("metadata")) + return SystemMessageData( + content=content, + role=role, + name=name, + metadata=metadata, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + result["role"] = to_enum(SystemMessageDataRole, self.role) + if self.name is not None: + result["name"] = from_union([from_none, lambda x: from_str(x)], self.name) if self.metadata is not None: - result["metadata"] = from_union([lambda x: to_class(SystemMessageMetadata, x), from_none], self.metadata) - if self.role is not None: - result["role"] = from_union([lambda x: to_enum(SystemMessageRole, x), from_none], self.role) - if self.kind is not None: - result["kind"] = from_union([lambda x: to_class(SystemNotification, x), from_none], self.kind) - if self.permission_request is not None: - result["permissionRequest"] = from_union([lambda x: to_class(PermissionRequest, x), from_none], self.permission_request) - if self.resolved_by_hook is not None: - result["resolvedByHook"] = from_union([from_bool, from_none], self.resolved_by_hook) - if self.allow_freeform is not None: - result["allowFreeform"] = from_union([from_bool, from_none], self.allow_freeform) - if self.choices is not None: - result["choices"] = from_union([lambda x: from_list(from_str, x), from_none], self.choices) - if self.question is not None: - result["question"] = from_union([from_str, from_none], self.question) - if self.answer is not None: - result["answer"] = from_union([from_str, from_none], self.answer) - if self.was_freeform is not None: - result["wasFreeform"] = from_union([from_bool, from_none], self.was_freeform) - if self.elicitation_source is not None: - result["elicitationSource"] = from_union([from_str, from_none], self.elicitation_source) - if self.mode is not None: - result["mode"] = from_union([lambda x: to_enum(ElicitationRequestedMode, x), from_none], self.mode) - if self.requested_schema is not None: - result["requestedSchema"] = from_union([lambda x: to_class(ElicitationRequestedSchema, x), from_none], self.requested_schema) - if self.action is not None: - result["action"] = from_union([lambda x: to_enum(ElicitationCompletedAction, x), from_none], self.action) - if self.mcp_request_id is not None: - result["mcpRequestId"] = from_union([to_float, from_str, from_none], self.mcp_request_id) - if self.server_name is not None: - result["serverName"] = from_union([from_str, from_none], self.server_name) - if self.server_url is not None: - result["serverUrl"] = from_union([from_str, from_none], self.server_url) - if self.static_client_config is not None: - result["staticClientConfig"] = from_union([lambda x: to_class(MCPOauthRequiredStaticClientConfig, x), from_none], self.static_client_config) - if self.traceparent is not None: - result["traceparent"] = from_union([from_str, from_none], self.traceparent) - if self.tracestate is not None: - result["tracestate"] = from_union([from_str, from_none], self.tracestate) - if self.command is not None: - result["command"] = from_union([from_str, from_none], self.command) - if self.args is not None: - result["args"] = from_union([from_str, from_none], self.args) - if self.command_name is not None: - result["commandName"] = from_union([from_str, from_none], self.command_name) - if self.commands is not None: - result["commands"] = from_union([lambda x: from_list(lambda x: to_class(CommandsChangedCommand, x), x), from_none], self.commands) - if self.ui is not None: - result["ui"] = from_union([lambda x: to_class(CapabilitiesChangedUI, x), from_none], self.ui) - if self.actions is not None: - result["actions"] = from_union([lambda x: from_list(from_str, x), from_none], self.actions) - if self.plan_content is not None: - result["planContent"] = from_union([from_str, from_none], self.plan_content) - if self.recommended_action is not None: - result["recommendedAction"] = from_union([from_str, from_none], self.recommended_action) - if self.approved is not None: - result["approved"] = from_union([from_bool, from_none], self.approved) - if self.auto_approve_edits is not None: - result["autoApproveEdits"] = from_union([from_bool, from_none], self.auto_approve_edits) - if self.feedback is not None: - result["feedback"] = from_union([from_str, from_none], self.feedback) - if self.selected_action is not None: - result["selectedAction"] = from_union([from_str, from_none], self.selected_action) - if self.skills is not None: - result["skills"] = from_union([lambda x: from_list(lambda x: to_class(SkillsLoadedSkill, x), x), from_none], self.skills) - if self.agents is not None: - result["agents"] = from_union([lambda x: from_list(lambda x: to_class(CustomAgentsUpdatedAgent, x), x), from_none], self.agents) - if self.errors is not None: - result["errors"] = from_union([lambda x: from_list(from_str, x), from_none], self.errors) - if self.warnings is not None: - result["warnings"] = from_union([lambda x: from_list(from_str, x), from_none], self.warnings) - if self.servers is not None: - result["servers"] = from_union([lambda x: from_list(lambda x: to_class(MCPServersLoadedServer, x), x), from_none], self.servers) + result["metadata"] = from_union([from_none, lambda x: to_class(SystemMessageDataMetadata, x)], self.metadata) + return result + + +@dataclass +class SystemNotificationDataKind: + """Structured metadata identifying what triggered this notification""" + type: SystemNotificationDataKindType + agent_id: str | None = None + agent_type: str | None = None + status: SystemNotificationDataKindStatus | None = None + description: str | None = None + prompt: str | None = None + shell_id: str | None = None + exit_code: float | None = None + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationDataKind": + assert isinstance(obj, dict) + type = parse_enum(SystemNotificationDataKindType, obj.get("type")) + agent_id = from_union([from_none, lambda x: from_str(x)], obj.get("agentId")) + agent_type = from_union([from_none, lambda x: from_str(x)], obj.get("agentType")) + status = from_union([from_none, lambda x: parse_enum(SystemNotificationDataKindStatus, x)], obj.get("status")) + description = from_union([from_none, lambda x: from_str(x)], obj.get("description")) + prompt = from_union([from_none, lambda x: from_str(x)], obj.get("prompt")) + shell_id = from_union([from_none, lambda x: from_str(x)], obj.get("shellId")) + exit_code = from_union([from_none, lambda x: from_float(x)], obj.get("exitCode")) + return SystemNotificationDataKind( + type=type, + agent_id=agent_id, + agent_type=agent_type, + status=status, + description=description, + prompt=prompt, + shell_id=shell_id, + exit_code=exit_code, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["type"] = to_enum(SystemNotificationDataKindType, self.type) + if self.agent_id is not None: + result["agentId"] = from_union([from_none, lambda x: from_str(x)], self.agent_id) + if self.agent_type is not None: + result["agentType"] = from_union([from_none, lambda x: from_str(x)], self.agent_type) if self.status is not None: - result["status"] = from_union([lambda x: to_enum(MCPServerStatus, x), from_none], self.status) - if self.extensions is not None: - result["extensions"] = from_union([lambda x: from_list(lambda x: to_class(ExtensionsLoadedExtension, x), x), from_none], self.extensions) + result["status"] = from_union([from_none, lambda x: to_enum(SystemNotificationDataKindStatus, x)], self.status) + if self.description is not None: + result["description"] = from_union([from_none, lambda x: from_str(x)], self.description) + if self.prompt is not None: + result["prompt"] = from_union([from_none, lambda x: from_str(x)], self.prompt) + if self.shell_id is not None: + result["shellId"] = from_union([from_none, lambda x: from_str(x)], self.shell_id) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, lambda x: to_float(x)], self.exit_code) + return result + + +@dataclass +class SystemNotificationData: + """System-generated notification for runtime events like background task completion""" + content: str + kind: SystemNotificationDataKind + + @staticmethod + def from_dict(obj: Any) -> "SystemNotificationData": + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + kind = SystemNotificationDataKind.from_dict(obj.get("kind")) + return SystemNotificationData( + content=content, + kind=kind, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + result["kind"] = to_class(SystemNotificationDataKind, self.kind) return result -class SessionEventType(Enum): - ABORT = "abort" - ASSISTANT_INTENT = "assistant.intent" - ASSISTANT_MESSAGE = "assistant.message" - ASSISTANT_MESSAGE_DELTA = "assistant.message_delta" - ASSISTANT_REASONING = "assistant.reasoning" - ASSISTANT_REASONING_DELTA = "assistant.reasoning_delta" - ASSISTANT_STREAMING_DELTA = "assistant.streaming_delta" - ASSISTANT_TURN_END = "assistant.turn_end" - ASSISTANT_TURN_START = "assistant.turn_start" - ASSISTANT_USAGE = "assistant.usage" - CAPABILITIES_CHANGED = "capabilities.changed" - COMMANDS_CHANGED = "commands.changed" - COMMAND_COMPLETED = "command.completed" - COMMAND_EXECUTE = "command.execute" - COMMAND_QUEUED = "command.queued" - ELICITATION_COMPLETED = "elicitation.completed" - ELICITATION_REQUESTED = "elicitation.requested" - EXIT_PLAN_MODE_COMPLETED = "exit_plan_mode.completed" - EXIT_PLAN_MODE_REQUESTED = "exit_plan_mode.requested" - EXTERNAL_TOOL_COMPLETED = "external_tool.completed" - EXTERNAL_TOOL_REQUESTED = "external_tool.requested" - HOOK_END = "hook.end" - HOOK_START = "hook.start" - MCP_OAUTH_COMPLETED = "mcp.oauth_completed" - MCP_OAUTH_REQUIRED = "mcp.oauth_required" - PENDING_MESSAGES_MODIFIED = "pending_messages.modified" - PERMISSION_COMPLETED = "permission.completed" - PERMISSION_REQUESTED = "permission.requested" - SAMPLING_COMPLETED = "sampling.completed" - SAMPLING_REQUESTED = "sampling.requested" - SESSION_BACKGROUND_TASKS_CHANGED = "session.background_tasks_changed" - SESSION_COMPACTION_COMPLETE = "session.compaction_complete" - SESSION_COMPACTION_START = "session.compaction_start" - SESSION_CONTEXT_CHANGED = "session.context_changed" - SESSION_CUSTOM_AGENTS_UPDATED = "session.custom_agents_updated" - SESSION_ERROR = "session.error" - SESSION_EXTENSIONS_LOADED = "session.extensions_loaded" - SESSION_HANDOFF = "session.handoff" - SESSION_IDLE = "session.idle" - SESSION_INFO = "session.info" - SESSION_MCP_SERVERS_LOADED = "session.mcp_servers_loaded" - SESSION_MCP_SERVER_STATUS_CHANGED = "session.mcp_server_status_changed" - SESSION_MODEL_CHANGE = "session.model_change" - SESSION_MODE_CHANGED = "session.mode_changed" - SESSION_PLAN_CHANGED = "session.plan_changed" - SESSION_REMOTE_STEERABLE_CHANGED = "session.remote_steerable_changed" - SESSION_RESUME = "session.resume" - SESSION_SHUTDOWN = "session.shutdown" - SESSION_SKILLS_LOADED = "session.skills_loaded" - SESSION_SNAPSHOT_REWIND = "session.snapshot_rewind" - SESSION_START = "session.start" - SESSION_TASK_COMPLETE = "session.task_complete" - SESSION_TITLE_CHANGED = "session.title_changed" - SESSION_TOOLS_UPDATED = "session.tools_updated" - SESSION_TRUNCATION = "session.truncation" - SESSION_USAGE_INFO = "session.usage_info" - SESSION_WARNING = "session.warning" - SESSION_WORKSPACE_FILE_CHANGED = "session.workspace_file_changed" - SKILL_INVOKED = "skill.invoked" - SUBAGENT_COMPLETED = "subagent.completed" - SUBAGENT_DESELECTED = "subagent.deselected" - SUBAGENT_FAILED = "subagent.failed" - SUBAGENT_SELECTED = "subagent.selected" - SUBAGENT_STARTED = "subagent.started" - SYSTEM_MESSAGE = "system.message" - SYSTEM_NOTIFICATION = "system.notification" - TOOL_EXECUTION_COMPLETE = "tool.execution_complete" - TOOL_EXECUTION_PARTIAL_RESULT = "tool.execution_partial_result" - TOOL_EXECUTION_PROGRESS = "tool.execution_progress" - TOOL_EXECUTION_START = "tool.execution_start" - TOOL_USER_REQUESTED = "tool.user_requested" - USER_INPUT_COMPLETED = "user_input.completed" - USER_INPUT_REQUESTED = "user_input.requested" - USER_MESSAGE = "user.message" - # UNKNOWN is used for forward compatibility - UNKNOWN = "unknown" +@dataclass +class PermissionRequestedDataPermissionRequestCommandsItem: + identifier: str + read_only: bool + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestedDataPermissionRequestCommandsItem": + assert isinstance(obj, dict) + identifier = from_str(obj.get("identifier")) + read_only = from_bool(obj.get("readOnly")) + return PermissionRequestedDataPermissionRequestCommandsItem( + identifier=identifier, + read_only=read_only, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["identifier"] = from_str(self.identifier) + result["readOnly"] = from_bool(self.read_only) + return result + + +@dataclass +class PermissionRequestedDataPermissionRequestPossibleUrlsItem: + url: str + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestedDataPermissionRequestPossibleUrlsItem": + assert isinstance(obj, dict) + url = from_str(obj.get("url")) + return PermissionRequestedDataPermissionRequestPossibleUrlsItem( + url=url, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["url"] = from_str(self.url) + return result + + +@dataclass +class PermissionRequestedDataPermissionRequest: + """Details of the permission being requested""" + kind: PermissionRequestedDataPermissionRequestKind + tool_call_id: str | None = None + full_command_text: str | None = None + intention: str | None = None + commands: list[PermissionRequestedDataPermissionRequestCommandsItem] | None = None + possible_paths: list[str] | None = None + possible_urls: list[PermissionRequestedDataPermissionRequestPossibleUrlsItem] | None = None + has_write_file_redirection: bool | None = None + can_offer_session_approval: bool | None = None + warning: str | None = None + file_name: str | None = None + diff: str | None = None + new_file_contents: str | None = None + path: str | None = None + server_name: str | None = None + tool_name: str | None = None + tool_title: str | None = None + args: Any = None + read_only: bool | None = None + url: str | None = None + action: PermissionRequestedDataPermissionRequestAction | None = None + subject: str | None = None + fact: str | None = None + citations: str | None = None + direction: PermissionRequestedDataPermissionRequestDirection | None = None + reason: str | None = None + tool_description: str | None = None + tool_args: Any = None + hook_message: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestedDataPermissionRequest": + assert isinstance(obj, dict) + kind = parse_enum(PermissionRequestedDataPermissionRequestKind, obj.get("kind")) + tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("toolCallId")) + full_command_text = from_union([from_none, lambda x: from_str(x)], obj.get("fullCommandText")) + intention = from_union([from_none, lambda x: from_str(x)], obj.get("intention")) + commands = from_union([from_none, lambda x: from_list(lambda x: PermissionRequestedDataPermissionRequestCommandsItem.from_dict(x), x)], obj.get("commands")) + possible_paths = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], obj.get("possiblePaths")) + possible_urls = from_union([from_none, lambda x: from_list(lambda x: PermissionRequestedDataPermissionRequestPossibleUrlsItem.from_dict(x), x)], obj.get("possibleUrls")) + has_write_file_redirection = from_union([from_none, lambda x: from_bool(x)], obj.get("hasWriteFileRedirection")) + can_offer_session_approval = from_union([from_none, lambda x: from_bool(x)], obj.get("canOfferSessionApproval")) + warning = from_union([from_none, lambda x: from_str(x)], obj.get("warning")) + file_name = from_union([from_none, lambda x: from_str(x)], obj.get("fileName")) + diff = from_union([from_none, lambda x: from_str(x)], obj.get("diff")) + new_file_contents = from_union([from_none, lambda x: from_str(x)], obj.get("newFileContents")) + path = from_union([from_none, lambda x: from_str(x)], obj.get("path")) + server_name = from_union([from_none, lambda x: from_str(x)], obj.get("serverName")) + tool_name = from_union([from_none, lambda x: from_str(x)], obj.get("toolName")) + tool_title = from_union([from_none, lambda x: from_str(x)], obj.get("toolTitle")) + args = obj.get("args") + read_only = from_union([from_none, lambda x: from_bool(x)], obj.get("readOnly")) + url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) + action = from_union([from_none, lambda x: parse_enum(PermissionRequestedDataPermissionRequestAction, x)], obj.get("action")) + subject = from_union([from_none, lambda x: from_str(x)], obj.get("subject")) + fact = from_union([from_none, lambda x: from_str(x)], obj.get("fact")) + citations = from_union([from_none, lambda x: from_str(x)], obj.get("citations")) + direction = from_union([from_none, lambda x: parse_enum(PermissionRequestedDataPermissionRequestDirection, x)], obj.get("direction")) + reason = from_union([from_none, lambda x: from_str(x)], obj.get("reason")) + tool_description = from_union([from_none, lambda x: from_str(x)], obj.get("toolDescription")) + tool_args = obj.get("toolArgs") + hook_message = from_union([from_none, lambda x: from_str(x)], obj.get("hookMessage")) + return PermissionRequestedDataPermissionRequest( + kind=kind, + tool_call_id=tool_call_id, + full_command_text=full_command_text, + intention=intention, + commands=commands, + possible_paths=possible_paths, + possible_urls=possible_urls, + has_write_file_redirection=has_write_file_redirection, + can_offer_session_approval=can_offer_session_approval, + warning=warning, + file_name=file_name, + diff=diff, + new_file_contents=new_file_contents, + path=path, + server_name=server_name, + tool_name=tool_name, + tool_title=tool_title, + args=args, + read_only=read_only, + url=url, + action=action, + subject=subject, + fact=fact, + citations=citations, + direction=direction, + reason=reason, + tool_description=tool_description, + tool_args=tool_args, + hook_message=hook_message, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionRequestedDataPermissionRequestKind, self.kind) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, lambda x: from_str(x)], self.tool_call_id) + if self.full_command_text is not None: + result["fullCommandText"] = from_union([from_none, lambda x: from_str(x)], self.full_command_text) + if self.intention is not None: + result["intention"] = from_union([from_none, lambda x: from_str(x)], self.intention) + if self.commands is not None: + result["commands"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestedDataPermissionRequestCommandsItem, x), x)], self.commands) + if self.possible_paths is not None: + result["possiblePaths"] = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], self.possible_paths) + if self.possible_urls is not None: + result["possibleUrls"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestedDataPermissionRequestPossibleUrlsItem, x), x)], self.possible_urls) + if self.has_write_file_redirection is not None: + result["hasWriteFileRedirection"] = from_union([from_none, lambda x: from_bool(x)], self.has_write_file_redirection) + if self.can_offer_session_approval is not None: + result["canOfferSessionApproval"] = from_union([from_none, lambda x: from_bool(x)], self.can_offer_session_approval) + if self.warning is not None: + result["warning"] = from_union([from_none, lambda x: from_str(x)], self.warning) + if self.file_name is not None: + result["fileName"] = from_union([from_none, lambda x: from_str(x)], self.file_name) + if self.diff is not None: + result["diff"] = from_union([from_none, lambda x: from_str(x)], self.diff) + if self.new_file_contents is not None: + result["newFileContents"] = from_union([from_none, lambda x: from_str(x)], self.new_file_contents) + if self.path is not None: + result["path"] = from_union([from_none, lambda x: from_str(x)], self.path) + if self.server_name is not None: + result["serverName"] = from_union([from_none, lambda x: from_str(x)], self.server_name) + if self.tool_name is not None: + result["toolName"] = from_union([from_none, lambda x: from_str(x)], self.tool_name) + if self.tool_title is not None: + result["toolTitle"] = from_union([from_none, lambda x: from_str(x)], self.tool_title) + if self.args is not None: + result["args"] = self.args + if self.read_only is not None: + result["readOnly"] = from_union([from_none, lambda x: from_bool(x)], self.read_only) + if self.url is not None: + result["url"] = from_union([from_none, lambda x: from_str(x)], self.url) + if self.action is not None: + result["action"] = from_union([from_none, lambda x: to_enum(PermissionRequestedDataPermissionRequestAction, x)], self.action) + if self.subject is not None: + result["subject"] = from_union([from_none, lambda x: from_str(x)], self.subject) + if self.fact is not None: + result["fact"] = from_union([from_none, lambda x: from_str(x)], self.fact) + if self.citations is not None: + result["citations"] = from_union([from_none, lambda x: from_str(x)], self.citations) + if self.direction is not None: + result["direction"] = from_union([from_none, lambda x: to_enum(PermissionRequestedDataPermissionRequestDirection, x)], self.direction) + if self.reason is not None: + result["reason"] = from_union([from_none, lambda x: from_str(x)], self.reason) + if self.tool_description is not None: + result["toolDescription"] = from_union([from_none, lambda x: from_str(x)], self.tool_description) + if self.tool_args is not None: + result["toolArgs"] = self.tool_args + if self.hook_message is not None: + result["hookMessage"] = from_union([from_none, lambda x: from_str(x)], self.hook_message) + return result + + +@dataclass +class PermissionRequestedData: + """Permission request notification requiring client approval with request details""" + request_id: str + permission_request: PermissionRequestedDataPermissionRequest + resolved_by_hook: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "PermissionRequestedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + permission_request = PermissionRequestedDataPermissionRequest.from_dict(obj.get("permissionRequest")) + resolved_by_hook = from_union([from_none, lambda x: from_bool(x)], obj.get("resolvedByHook")) + return PermissionRequestedData( + request_id=request_id, + permission_request=permission_request, + resolved_by_hook=resolved_by_hook, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["permissionRequest"] = to_class(PermissionRequestedDataPermissionRequest, self.permission_request) + if self.resolved_by_hook is not None: + result["resolvedByHook"] = from_union([from_none, lambda x: from_bool(x)], self.resolved_by_hook) + return result + + +@dataclass +class PermissionCompletedDataResult: + """The result of the permission request""" + kind: PermissionCompletedDataResultKind + + @staticmethod + def from_dict(obj: Any) -> "PermissionCompletedDataResult": + assert isinstance(obj, dict) + kind = parse_enum(PermissionCompletedDataResultKind, obj.get("kind")) + return PermissionCompletedDataResult( + kind=kind, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionCompletedDataResultKind, self.kind) + return result + + +@dataclass +class PermissionCompletedData: + """Permission request completion notification signaling UI dismissal""" + request_id: str + result: PermissionCompletedDataResult + + @staticmethod + def from_dict(obj: Any) -> "PermissionCompletedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + result = PermissionCompletedDataResult.from_dict(obj.get("result")) + return PermissionCompletedData( + request_id=request_id, + result=result, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = to_class(PermissionCompletedDataResult, self.result) + return result + + +@dataclass +class UserInputRequestedData: + """User input request notification with question and optional predefined choices""" + request_id: str + question: str + choices: list[str] | None = None + allow_freeform: bool | None = None + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "UserInputRequestedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + question = from_str(obj.get("question")) + choices = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], obj.get("choices")) + allow_freeform = from_union([from_none, lambda x: from_bool(x)], obj.get("allowFreeform")) + tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("toolCallId")) + return UserInputRequestedData( + request_id=request_id, + question=question, + choices=choices, + allow_freeform=allow_freeform, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["question"] = from_str(self.question) + if self.choices is not None: + result["choices"] = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], self.choices) + if self.allow_freeform is not None: + result["allowFreeform"] = from_union([from_none, lambda x: from_bool(x)], self.allow_freeform) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, lambda x: from_str(x)], self.tool_call_id) + return result + + +@dataclass +class UserInputCompletedData: + """User input request completion with the user's response""" + request_id: str + answer: str | None = None + was_freeform: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "UserInputCompletedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + answer = from_union([from_none, lambda x: from_str(x)], obj.get("answer")) + was_freeform = from_union([from_none, lambda x: from_bool(x)], obj.get("wasFreeform")) + return UserInputCompletedData( + request_id=request_id, + answer=answer, + was_freeform=was_freeform, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + if self.answer is not None: + result["answer"] = from_union([from_none, lambda x: from_str(x)], self.answer) + if self.was_freeform is not None: + result["wasFreeform"] = from_union([from_none, lambda x: from_bool(x)], self.was_freeform) + return result + + +@dataclass +class ElicitationRequestedDataRequestedSchema: + """JSON Schema describing the form fields to present to the user (form mode only)""" + type: str + properties: dict[str, Any] + required: list[str] | None = None + + @staticmethod + def from_dict(obj: Any) -> "ElicitationRequestedDataRequestedSchema": + assert isinstance(obj, dict) + type = from_str(obj.get("type")) + properties = from_dict(lambda x: x, obj.get("properties")) + required = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], obj.get("required")) + return ElicitationRequestedDataRequestedSchema( + type=type, + properties=properties, + required=required, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["type"] = from_str(self.type) + result["properties"] = from_dict(lambda x: x, self.properties) + if self.required is not None: + result["required"] = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], self.required) + return result + + +@dataclass +class ElicitationRequestedData: + """Elicitation request; may be form-based (structured input) or URL-based (browser redirect)""" + request_id: str + message: str + tool_call_id: str | None = None + elicitation_source: str | None = None + mode: ElicitationRequestedDataMode | None = None + requested_schema: ElicitationRequestedDataRequestedSchema | None = None + url: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "ElicitationRequestedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + message = from_str(obj.get("message")) + tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("toolCallId")) + elicitation_source = from_union([from_none, lambda x: from_str(x)], obj.get("elicitationSource")) + mode = from_union([from_none, lambda x: parse_enum(ElicitationRequestedDataMode, x)], obj.get("mode")) + requested_schema = from_union([from_none, lambda x: ElicitationRequestedDataRequestedSchema.from_dict(x)], obj.get("requestedSchema")) + url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) + return ElicitationRequestedData( + request_id=request_id, + message=message, + tool_call_id=tool_call_id, + elicitation_source=elicitation_source, + mode=mode, + requested_schema=requested_schema, + url=url, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["message"] = from_str(self.message) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, lambda x: from_str(x)], self.tool_call_id) + if self.elicitation_source is not None: + result["elicitationSource"] = from_union([from_none, lambda x: from_str(x)], self.elicitation_source) + if self.mode is not None: + result["mode"] = from_union([from_none, lambda x: to_enum(ElicitationRequestedDataMode, x)], self.mode) + if self.requested_schema is not None: + result["requestedSchema"] = from_union([from_none, lambda x: to_class(ElicitationRequestedDataRequestedSchema, x)], self.requested_schema) + if self.url is not None: + result["url"] = from_union([from_none, lambda x: from_str(x)], self.url) + return result + + +@dataclass +class ElicitationCompletedData: + """Elicitation request completion with the user's response""" + request_id: str + action: ElicitationCompletedDataAction | None = None + content: dict[str, Any] | None = None + + @staticmethod + def from_dict(obj: Any) -> "ElicitationCompletedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + action = from_union([from_none, lambda x: parse_enum(ElicitationCompletedDataAction, x)], obj.get("action")) + content = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("content")) + return ElicitationCompletedData( + request_id=request_id, + action=action, + content=content, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + if self.action is not None: + result["action"] = from_union([from_none, lambda x: to_enum(ElicitationCompletedDataAction, x)], self.action) + if self.content is not None: + result["content"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.content) + return result + + +@dataclass +class SamplingRequestedData: + """Sampling request from an MCP server; contains the server name and a requestId for correlation""" + request_id: str + server_name: str + mcp_request_id: Any + + @staticmethod + def from_dict(obj: Any) -> "SamplingRequestedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + server_name = from_str(obj.get("serverName")) + mcp_request_id = obj.get("mcpRequestId") + return SamplingRequestedData( + request_id=request_id, + server_name=server_name, + mcp_request_id=mcp_request_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["serverName"] = from_str(self.server_name) + result["mcpRequestId"] = self.mcp_request_id + return result + + +@dataclass +class SamplingCompletedData: + """Sampling request completion notification signaling UI dismissal""" + request_id: str + + @staticmethod + def from_dict(obj: Any) -> "SamplingCompletedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + return SamplingCompletedData( + request_id=request_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + return result + + +@dataclass +class McpOauthRequiredDataStaticClientConfig: + """Static OAuth client configuration, if the server specifies one""" + client_id: str + public_client: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "McpOauthRequiredDataStaticClientConfig": + assert isinstance(obj, dict) + client_id = from_str(obj.get("clientId")) + public_client = from_union([from_none, lambda x: from_bool(x)], obj.get("publicClient")) + return McpOauthRequiredDataStaticClientConfig( + client_id=client_id, + public_client=public_client, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["clientId"] = from_str(self.client_id) + if self.public_client is not None: + result["publicClient"] = from_union([from_none, lambda x: from_bool(x)], self.public_client) + return result + + +@dataclass +class McpOauthRequiredData: + """OAuth authentication request for an MCP server""" + request_id: str + server_name: str + server_url: str + static_client_config: McpOauthRequiredDataStaticClientConfig | None = None + + @staticmethod + def from_dict(obj: Any) -> "McpOauthRequiredData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + server_name = from_str(obj.get("serverName")) + server_url = from_str(obj.get("serverUrl")) + static_client_config = from_union([from_none, lambda x: McpOauthRequiredDataStaticClientConfig.from_dict(x)], obj.get("staticClientConfig")) + return McpOauthRequiredData( + request_id=request_id, + server_name=server_name, + server_url=server_url, + static_client_config=static_client_config, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["serverName"] = from_str(self.server_name) + result["serverUrl"] = from_str(self.server_url) + if self.static_client_config is not None: + result["staticClientConfig"] = from_union([from_none, lambda x: to_class(McpOauthRequiredDataStaticClientConfig, x)], self.static_client_config) + return result + + +@dataclass +class McpOauthCompletedData: + """MCP OAuth request completion notification""" + request_id: str + + @staticmethod + def from_dict(obj: Any) -> "McpOauthCompletedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + return McpOauthCompletedData( + request_id=request_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + return result + + +@dataclass +class ExternalToolRequestedData: + """External tool invocation request for client-side tool execution""" + request_id: str + session_id: str + tool_call_id: str + tool_name: str + arguments: Any = None + traceparent: str | None = None + tracestate: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "ExternalToolRequestedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + session_id = from_str(obj.get("sessionId")) + tool_call_id = from_str(obj.get("toolCallId")) + tool_name = from_str(obj.get("toolName")) + arguments = obj.get("arguments") + traceparent = from_union([from_none, lambda x: from_str(x)], obj.get("traceparent")) + tracestate = from_union([from_none, lambda x: from_str(x)], obj.get("tracestate")) + return ExternalToolRequestedData( + request_id=request_id, + session_id=session_id, + tool_call_id=tool_call_id, + tool_name=tool_name, + arguments=arguments, + traceparent=traceparent, + tracestate=tracestate, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["sessionId"] = from_str(self.session_id) + result["toolCallId"] = from_str(self.tool_call_id) + result["toolName"] = from_str(self.tool_name) + if self.arguments is not None: + result["arguments"] = self.arguments + if self.traceparent is not None: + result["traceparent"] = from_union([from_none, lambda x: from_str(x)], self.traceparent) + if self.tracestate is not None: + result["tracestate"] = from_union([from_none, lambda x: from_str(x)], self.tracestate) + return result + + +@dataclass +class ExternalToolCompletedData: + """External tool completion notification signaling UI dismissal""" + request_id: str + + @staticmethod + def from_dict(obj: Any) -> "ExternalToolCompletedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + return ExternalToolCompletedData( + request_id=request_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + return result + + +@dataclass +class CommandQueuedData: + """Queued slash command dispatch request for client execution""" + request_id: str + command: str + + @staticmethod + def from_dict(obj: Any) -> "CommandQueuedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + command = from_str(obj.get("command")) + return CommandQueuedData( + request_id=request_id, + command=command, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["command"] = from_str(self.command) + return result + + +@dataclass +class CommandExecuteData: + """Registered command dispatch request routed to the owning client""" + request_id: str + command: str + command_name: str + args: str + + @staticmethod + def from_dict(obj: Any) -> "CommandExecuteData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + command = from_str(obj.get("command")) + command_name = from_str(obj.get("commandName")) + args = from_str(obj.get("args")) + return CommandExecuteData( + request_id=request_id, + command=command, + command_name=command_name, + args=args, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["command"] = from_str(self.command) + result["commandName"] = from_str(self.command_name) + result["args"] = from_str(self.args) + return result + + +@dataclass +class CommandCompletedData: + """Queued command completion notification signaling UI dismissal""" + request_id: str + + @staticmethod + def from_dict(obj: Any) -> "CommandCompletedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + return CommandCompletedData( + request_id=request_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + return result + + +@dataclass +class CommandsChangedDataCommandsItem: + name: str + description: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "CommandsChangedDataCommandsItem": + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + description = from_union([from_none, lambda x: from_str(x)], obj.get("description")) + return CommandsChangedDataCommandsItem( + name=name, + description=description, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.description is not None: + result["description"] = from_union([from_none, lambda x: from_str(x)], self.description) + return result + + +@dataclass +class CommandsChangedData: + """SDK command registration change notification""" + commands: list[CommandsChangedDataCommandsItem] + + @staticmethod + def from_dict(obj: Any) -> "CommandsChangedData": + assert isinstance(obj, dict) + commands = from_list(lambda x: CommandsChangedDataCommandsItem.from_dict(x), obj.get("commands")) + return CommandsChangedData( + commands=commands, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["commands"] = from_list(lambda x: to_class(CommandsChangedDataCommandsItem, x), self.commands) + return result + + +@dataclass +class CapabilitiesChangedDataUi: + """UI capability changes""" + elicitation: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "CapabilitiesChangedDataUi": + assert isinstance(obj, dict) + elicitation = from_union([from_none, lambda x: from_bool(x)], obj.get("elicitation")) + return CapabilitiesChangedDataUi( + elicitation=elicitation, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.elicitation is not None: + result["elicitation"] = from_union([from_none, lambda x: from_bool(x)], self.elicitation) + return result + + +@dataclass +class CapabilitiesChangedData: + """Session capability change notification""" + ui: CapabilitiesChangedDataUi | None = None + + @staticmethod + def from_dict(obj: Any) -> "CapabilitiesChangedData": + assert isinstance(obj, dict) + ui = from_union([from_none, lambda x: CapabilitiesChangedDataUi.from_dict(x)], obj.get("ui")) + return CapabilitiesChangedData( + ui=ui, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.ui is not None: + result["ui"] = from_union([from_none, lambda x: to_class(CapabilitiesChangedDataUi, x)], self.ui) + return result + + +@dataclass +class ExitPlanModeRequestedData: + """Plan approval request with plan content and available user actions""" + request_id: str + summary: str + plan_content: str + actions: list[str] + recommended_action: str + + @staticmethod + def from_dict(obj: Any) -> "ExitPlanModeRequestedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + summary = from_str(obj.get("summary")) + plan_content = from_str(obj.get("planContent")) + actions = from_list(lambda x: from_str(x), obj.get("actions")) + recommended_action = from_str(obj.get("recommendedAction")) + return ExitPlanModeRequestedData( + request_id=request_id, + summary=summary, + plan_content=plan_content, + actions=actions, + recommended_action=recommended_action, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["summary"] = from_str(self.summary) + result["planContent"] = from_str(self.plan_content) + result["actions"] = from_list(lambda x: from_str(x), self.actions) + result["recommendedAction"] = from_str(self.recommended_action) + return result + + +@dataclass +class ExitPlanModeCompletedData: + """Plan mode exit completion with the user's approval decision and optional feedback""" + request_id: str + approved: bool | None = None + selected_action: str | None = None + auto_approve_edits: bool | None = None + feedback: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "ExitPlanModeCompletedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + approved = from_union([from_none, lambda x: from_bool(x)], obj.get("approved")) + selected_action = from_union([from_none, lambda x: from_str(x)], obj.get("selectedAction")) + auto_approve_edits = from_union([from_none, lambda x: from_bool(x)], obj.get("autoApproveEdits")) + feedback = from_union([from_none, lambda x: from_str(x)], obj.get("feedback")) + return ExitPlanModeCompletedData( + request_id=request_id, + approved=approved, + selected_action=selected_action, + auto_approve_edits=auto_approve_edits, + feedback=feedback, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + if self.approved is not None: + result["approved"] = from_union([from_none, lambda x: from_bool(x)], self.approved) + if self.selected_action is not None: + result["selectedAction"] = from_union([from_none, lambda x: from_str(x)], self.selected_action) + if self.auto_approve_edits is not None: + result["autoApproveEdits"] = from_union([from_none, lambda x: from_bool(x)], self.auto_approve_edits) + if self.feedback is not None: + result["feedback"] = from_union([from_none, lambda x: from_str(x)], self.feedback) + return result + + +@dataclass +class SessionToolsUpdatedData: + model: str + + @staticmethod + def from_dict(obj: Any) -> "SessionToolsUpdatedData": + assert isinstance(obj, dict) + model = from_str(obj.get("model")) + return SessionToolsUpdatedData( + model=model, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["model"] = from_str(self.model) + return result + + +@dataclass +class SessionBackgroundTasksChangedData: + @staticmethod + def from_dict(obj: Any) -> "SessionBackgroundTasksChangedData": + assert isinstance(obj, dict) + return SessionBackgroundTasksChangedData() + + def to_dict(self) -> dict: + return {} + + +@dataclass +class SessionSkillsLoadedDataSkillsItem: + name: str + description: str + source: str + user_invocable: bool + enabled: bool + path: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionSkillsLoadedDataSkillsItem": + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + description = from_str(obj.get("description")) + source = from_str(obj.get("source")) + user_invocable = from_bool(obj.get("userInvocable")) + enabled = from_bool(obj.get("enabled")) + path = from_union([from_none, lambda x: from_str(x)], obj.get("path")) + return SessionSkillsLoadedDataSkillsItem( + name=name, + description=description, + source=source, + user_invocable=user_invocable, + enabled=enabled, + path=path, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["description"] = from_str(self.description) + result["source"] = from_str(self.source) + result["userInvocable"] = from_bool(self.user_invocable) + result["enabled"] = from_bool(self.enabled) + if self.path is not None: + result["path"] = from_union([from_none, lambda x: from_str(x)], self.path) + return result + + +@dataclass +class SessionSkillsLoadedData: + skills: list[SessionSkillsLoadedDataSkillsItem] + + @staticmethod + def from_dict(obj: Any) -> "SessionSkillsLoadedData": + assert isinstance(obj, dict) + skills = from_list(lambda x: SessionSkillsLoadedDataSkillsItem.from_dict(x), obj.get("skills")) + return SessionSkillsLoadedData( + skills=skills, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["skills"] = from_list(lambda x: to_class(SessionSkillsLoadedDataSkillsItem, x), self.skills) + return result + + +@dataclass +class SessionCustomAgentsUpdatedDataAgentsItem: + id: str + name: str + display_name: str + description: str + source: str + tools: list[str] + user_invocable: bool + model: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionCustomAgentsUpdatedDataAgentsItem": + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + name = from_str(obj.get("name")) + display_name = from_str(obj.get("displayName")) + description = from_str(obj.get("description")) + source = from_str(obj.get("source")) + tools = from_list(lambda x: from_str(x), obj.get("tools")) + user_invocable = from_bool(obj.get("userInvocable")) + model = from_union([from_none, lambda x: from_str(x)], obj.get("model")) + return SessionCustomAgentsUpdatedDataAgentsItem( + id=id, + name=name, + display_name=display_name, + description=description, + source=source, + tools=tools, + user_invocable=user_invocable, + model=model, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["name"] = from_str(self.name) + result["displayName"] = from_str(self.display_name) + result["description"] = from_str(self.description) + result["source"] = from_str(self.source) + result["tools"] = from_list(lambda x: from_str(x), self.tools) + result["userInvocable"] = from_bool(self.user_invocable) + if self.model is not None: + result["model"] = from_union([from_none, lambda x: from_str(x)], self.model) + return result + + +@dataclass +class SessionCustomAgentsUpdatedData: + agents: list[SessionCustomAgentsUpdatedDataAgentsItem] + warnings: list[str] + errors: list[str] + + @staticmethod + def from_dict(obj: Any) -> "SessionCustomAgentsUpdatedData": + assert isinstance(obj, dict) + agents = from_list(lambda x: SessionCustomAgentsUpdatedDataAgentsItem.from_dict(x), obj.get("agents")) + warnings = from_list(lambda x: from_str(x), obj.get("warnings")) + errors = from_list(lambda x: from_str(x), obj.get("errors")) + return SessionCustomAgentsUpdatedData( + agents=agents, + warnings=warnings, + errors=errors, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["agents"] = from_list(lambda x: to_class(SessionCustomAgentsUpdatedDataAgentsItem, x), self.agents) + result["warnings"] = from_list(lambda x: from_str(x), self.warnings) + result["errors"] = from_list(lambda x: from_str(x), self.errors) + return result + + +@dataclass +class SessionMcpServersLoadedDataServersItem: + name: str + status: SessionMcpServersLoadedDataServersItemStatus + source: str | None = None + error: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionMcpServersLoadedDataServersItem": + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + status = parse_enum(SessionMcpServersLoadedDataServersItemStatus, obj.get("status")) + source = from_union([from_none, lambda x: from_str(x)], obj.get("source")) + error = from_union([from_none, lambda x: from_str(x)], obj.get("error")) + return SessionMcpServersLoadedDataServersItem( + name=name, + status=status, + source=source, + error=error, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["status"] = to_enum(SessionMcpServersLoadedDataServersItemStatus, self.status) + if self.source is not None: + result["source"] = from_union([from_none, lambda x: from_str(x)], self.source) + if self.error is not None: + result["error"] = from_union([from_none, lambda x: from_str(x)], self.error) + return result + + +@dataclass +class SessionMcpServersLoadedData: + servers: list[SessionMcpServersLoadedDataServersItem] + + @staticmethod + def from_dict(obj: Any) -> "SessionMcpServersLoadedData": + assert isinstance(obj, dict) + servers = from_list(lambda x: SessionMcpServersLoadedDataServersItem.from_dict(x), obj.get("servers")) + return SessionMcpServersLoadedData( + servers=servers, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["servers"] = from_list(lambda x: to_class(SessionMcpServersLoadedDataServersItem, x), self.servers) + return result + + +@dataclass +class SessionMcpServerStatusChangedData: + server_name: str + status: SessionMcpServersLoadedDataServersItemStatus + + @staticmethod + def from_dict(obj: Any) -> "SessionMcpServerStatusChangedData": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + status = parse_enum(SessionMcpServersLoadedDataServersItemStatus, obj.get("status")) + return SessionMcpServerStatusChangedData( + server_name=server_name, + status=status, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + result["status"] = to_enum(SessionMcpServersLoadedDataServersItemStatus, self.status) + return result + + +@dataclass +class SessionExtensionsLoadedDataExtensionsItem: + id: str + name: str + source: SessionExtensionsLoadedDataExtensionsItemSource + status: SessionExtensionsLoadedDataExtensionsItemStatus + + @staticmethod + def from_dict(obj: Any) -> "SessionExtensionsLoadedDataExtensionsItem": + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + name = from_str(obj.get("name")) + source = parse_enum(SessionExtensionsLoadedDataExtensionsItemSource, obj.get("source")) + status = parse_enum(SessionExtensionsLoadedDataExtensionsItemStatus, obj.get("status")) + return SessionExtensionsLoadedDataExtensionsItem( + id=id, + name=name, + source=source, + status=status, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["name"] = from_str(self.name) + result["source"] = to_enum(SessionExtensionsLoadedDataExtensionsItemSource, self.source) + result["status"] = to_enum(SessionExtensionsLoadedDataExtensionsItemStatus, self.status) + return result + + +@dataclass +class SessionExtensionsLoadedData: + extensions: list[SessionExtensionsLoadedDataExtensionsItem] + + @staticmethod + def from_dict(obj: Any) -> "SessionExtensionsLoadedData": + assert isinstance(obj, dict) + extensions = from_list(lambda x: SessionExtensionsLoadedDataExtensionsItem.from_dict(x), obj.get("extensions")) + return SessionExtensionsLoadedData( + extensions=extensions, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["extensions"] = from_list(lambda x: to_class(SessionExtensionsLoadedDataExtensionsItem, x), self.extensions) + return result + + +class SessionStartDataContextHostType(Enum): + """Hosting platform type of the repository (github or ado)""" + GITHUB = "github" + ADO = "ado" + + +class SessionPlanChangedDataOperation(Enum): + """The type of operation performed on the plan file""" + CREATE = "create" + UPDATE = "update" + DELETE = "delete" + + +class SessionWorkspaceFileChangedDataOperation(Enum): + """Whether the file was newly created or updated""" + CREATE = "create" + UPDATE = "update" + + +class SessionImportLegacyDataLegacySessionChatMessagesItemRole(Enum): + """SessionImportLegacyDataLegacySessionChatMessagesItem discriminator""" + DEVELOPER = "developer" + SYSTEM = "system" + USER = "user" + ASSISTANT = "assistant" + TOOL = "tool" + FUNCTION = "function" + + +class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType(Enum): + """SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem discriminator""" + FUNCTION = "function" + CUSTOM = "custom" + + +class SessionImportLegacyDataLegacySessionSelectedModel(Enum): + CLAUDE_SONNET_4_6 = "claude-sonnet-4.6" + CLAUDE_SONNET_4_5 = "claude-sonnet-4.5" + CLAUDE_HAIKU_4_5 = "claude-haiku-4.5" + CLAUDE_OPUS_4_6 = "claude-opus-4.6" + CLAUDE_OPUS_4_6_FAST = "claude-opus-4.6-fast" + CLAUDE_OPUS_4_6_1M = "claude-opus-4.6-1m" + CLAUDE_OPUS_4_5 = "claude-opus-4.5" + CLAUDE_SONNET_4 = "claude-sonnet-4" + GOLDENEYE = "goldeneye" + GPT_5_4 = "gpt-5.4" + GPT_5_3_CODEX = "gpt-5.3-codex" + GPT_5_2_CODEX = "gpt-5.2-codex" + GPT_5_2 = "gpt-5.2" + GPT_5_1 = "gpt-5.1" + GPT_5_4_MINI = "gpt-5.4-mini" + GPT_5_MINI = "gpt-5-mini" + GPT_4_1 = "gpt-4.1" + + +class SessionHandoffDataSourceType(Enum): + """Origin type of the session being handed off""" + REMOTE = "remote" + LOCAL = "local" + + +class SessionShutdownDataShutdownType(Enum): + """Whether the session ended normally (\"routine\") or due to a crash/fatal error (\"error\")""" + ROUTINE = "routine" + ERROR = "error" + + +class UserMessageDataAttachmentsItemType(Enum): + """A user message attachment — a file, directory, code selection, blob, or GitHub reference discriminator""" + FILE = "file" + DIRECTORY = "directory" + SELECTION = "selection" + GITHUB_REFERENCE = "github_reference" + BLOB = "blob" + + +class UserMessageDataAttachmentsItemReferenceType(Enum): + """Type of GitHub reference""" + ISSUE = "issue" + PR = "pr" + DISCUSSION = "discussion" + + +class UserMessageDataAgentMode(Enum): + """The agent mode that was active when this message was sent""" + INTERACTIVE = "interactive" + PLAN = "plan" + AUTOPILOT = "autopilot" + SHELL = "shell" + + +class ToolExecutionCompleteDataResultContentsItemType(Enum): + """A content block within a tool result, which may be text, terminal output, image, audio, or a resource discriminator""" + TEXT = "text" + TERMINAL = "terminal" + IMAGE = "image" + AUDIO = "audio" + RESOURCE_LINK = "resource_link" + RESOURCE = "resource" + + +class ToolExecutionCompleteDataResultContentsItemIconsItemTheme(Enum): + """Theme variant this icon is intended for""" + LIGHT = "light" + DARK = "dark" - @classmethod - def _missing_(cls, value: object) -> "SessionEventType": - """Handle unknown event types gracefully for forward compatibility.""" - return cls.UNKNOWN + +class SystemMessageDataRole(Enum): + """Message role: \"system\" for system prompts, \"developer\" for developer-injected instructions""" + SYSTEM = "system" + DEVELOPER = "developer" + + +class SystemNotificationDataKindType(Enum): + """Structured metadata identifying what triggered this notification discriminator""" + AGENT_COMPLETED = "agent_completed" + AGENT_IDLE = "agent_idle" + SHELL_COMPLETED = "shell_completed" + SHELL_DETACHED_COMPLETED = "shell_detached_completed" + + +class SystemNotificationDataKindStatus(Enum): + """Whether the agent completed successfully or failed""" + COMPLETED = "completed" + FAILED = "failed" + + +class PermissionRequestedDataPermissionRequestKind(Enum): + """Details of the permission being requested discriminator""" + SHELL = "shell" + WRITE = "write" + READ = "read" + MCP = "mcp" + URL = "url" + MEMORY = "memory" + CUSTOM_TOOL = "custom-tool" + HOOK = "hook" + + +class PermissionRequestedDataPermissionRequestAction(Enum): + """Whether this is a store or vote memory operation""" + STORE = "store" + VOTE = "vote" + + +class PermissionRequestedDataPermissionRequestDirection(Enum): + """Vote direction (vote only)""" + UPVOTE = "upvote" + DOWNVOTE = "downvote" + + +class PermissionCompletedDataResultKind(Enum): + """The outcome of the permission request""" + APPROVED = "approved" + DENIED_BY_RULES = "denied-by-rules" + DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user" + DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user" + DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy" + DENIED_BY_PERMISSION_REQUEST_HOOK = "denied-by-permission-request-hook" + + +class ElicitationRequestedDataMode(Enum): + """Elicitation mode; \"form\" for structured input, \"url\" for browser-based. Defaults to \"form\" when absent.""" + FORM = "form" + URL = "url" + + +class ElicitationCompletedDataAction(Enum): + """The user action: \"accept\" (submitted form), \"decline\" (explicitly refused), or \"cancel\" (dismissed)""" + ACCEPT = "accept" + DECLINE = "decline" + CANCEL = "cancel" + + +class SessionMcpServersLoadedDataServersItemStatus(Enum): + """Connection status: connected, failed, needs-auth, pending, disabled, or not_configured""" + CONNECTED = "connected" + FAILED = "failed" + NEEDS_AUTH = "needs-auth" + PENDING = "pending" + DISABLED = "disabled" + NOT_CONFIGURED = "not_configured" + + +class SessionExtensionsLoadedDataExtensionsItemSource(Enum): + """Discovery source""" + PROJECT = "project" + USER = "user" + + +class SessionExtensionsLoadedDataExtensionsItemStatus(Enum): + """Current status: running, disabled, failed, or starting""" + RUNNING = "running" + DISABLED = "disabled" + FAILED = "failed" + STARTING = "starting" +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPlanChangedData | SessionWorkspaceFileChangedData | SessionImportLegacyData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | RawSessionEventData | Data + @dataclass class SessionEvent: - data: Data - """Session initialization metadata including context and configuration - - Session resume metadata including current context and event count - - Notifies Mission Control that the session's remote steering capability has changed - - Error details for timeline display including message and optional diagnostic information - - Payload indicating the session is idle with no background agents in flight - - Session title change payload containing the new display title - - Informational message for timeline display with categorization - - Warning message for timeline display with categorization - - Model change details including previous and new model identifiers - - Agent mode change details including previous and new modes - - Plan file operation details indicating what changed - - Workspace file change details including path and operation type - - Session handoff metadata including source, context, and repository information - - Conversation truncation statistics including token counts and removed content metrics - - Session rewind details including target event and count of removed events - - Session termination metrics including usage statistics, code changes, and shutdown - reason - - Updated working directory and git context after the change - - Current context window usage statistics including token and message counts - - Context window breakdown at the start of LLM-powered conversation compaction - - Conversation compaction results including success status, metrics, and optional error - details - - Task completion notification with summary from the agent - - Empty payload; the event signals that the pending message queue has changed - - Turn initialization metadata including identifier and interaction tracking - - Agent intent description for current activity or plan - - Assistant reasoning content for timeline display with complete thinking text - - Streaming reasoning delta for incremental extended thinking updates - - Streaming response progress with cumulative byte count - - Assistant response containing text content, optional tool requests, and interaction - metadata - - Streaming assistant message delta for incremental response updates - - Turn completion metadata including the turn identifier - - LLM API call usage metrics including tokens, costs, quotas, and billing information - - Turn abort information including the reason for termination - - User-initiated tool invocation request with tool name and arguments - - Tool execution startup details including MCP server information when applicable - - Streaming tool execution output for incremental result display - - Tool execution progress notification with status message - - Tool execution completion results including success status, detailed output, and error - information - - Skill invocation details including content, allowed tools, and plugin metadata - - Sub-agent startup details including parent tool call and agent information - - Sub-agent completion details for successful execution - - Sub-agent failure details including error message and agent information - - Custom agent selection details including name and available tools - - Empty payload; the event signals that the custom agent was deselected, returning to the - default agent - - Hook invocation start details including type and input data - - Hook invocation completion details including output, success status, and error - information - - System or developer message content with role and optional template metadata - - System-generated notification for runtime events like background task completion - - Permission request notification requiring client approval with request details - - Permission request completion notification signaling UI dismissal - - User input request notification with question and optional predefined choices - - User input request completion with the user's response - - Elicitation request; may be form-based (structured input) or URL-based (browser - redirect) - - Elicitation request completion with the user's response - - Sampling request from an MCP server; contains the server name and a requestId for - correlation - - Sampling request completion notification signaling UI dismissal - - OAuth authentication request for an MCP server - - MCP OAuth request completion notification - - External tool invocation request for client-side tool execution - - External tool completion notification signaling UI dismissal - - Queued slash command dispatch request for client execution - - Registered command dispatch request routed to the owning client - - Queued command completion notification signaling UI dismissal - - SDK command registration change notification - - Session capability change notification - - Plan approval request with plan content and available user actions - - Plan mode exit completion with the user's approval decision and optional feedback - """ + data: SessionEventData id: UUID - """Unique event identifier (UUID v4), generated when the event is emitted""" - timestamp: datetime - """ISO 8601 timestamp when the event was created""" - type: SessionEventType ephemeral: bool | None = None - """When true, the event is transient and not persisted to the session event log on disk""" - parent_id: UUID | None = None - """ID of the chronologically preceding event in the session, forming a linked chain. Null - for the first event. - """ + raw_type: str | None = None @staticmethod - def from_dict(obj: Any) -> 'SessionEvent': + def from_dict(obj: Any) -> "SessionEvent": assert isinstance(obj, dict) - data = Data.from_dict(obj.get("data")) - id = UUID(obj.get("id")) + raw_type = from_str(obj.get("type")) + event_type = SessionEventType(raw_type) + event_id = from_uuid(obj.get("id")) timestamp = from_datetime(obj.get("timestamp")) - type = SessionEventType(obj.get("type")) ephemeral = from_union([from_bool, from_none], obj.get("ephemeral")) - parent_id = from_union([from_none, lambda x: UUID(x)], obj.get("parentId")) - return SessionEvent(data, id, timestamp, type, ephemeral, parent_id) + parent_id = from_union([from_none, from_uuid], obj.get("parentId")) + data_obj = obj.get("data") + match event_type: + case SessionEventType.SESSION_START: data = SessionStartData.from_dict(data_obj) + case SessionEventType.SESSION_RESUME: data = SessionResumeData.from_dict(data_obj) + case SessionEventType.SESSION_REMOTE_STEERABLE_CHANGED: data = SessionRemoteSteerableChangedData.from_dict(data_obj) + case SessionEventType.SESSION_ERROR: data = SessionErrorData.from_dict(data_obj) + case SessionEventType.SESSION_IDLE: data = SessionIdleData.from_dict(data_obj) + case SessionEventType.SESSION_TITLE_CHANGED: data = SessionTitleChangedData.from_dict(data_obj) + case SessionEventType.SESSION_INFO: data = SessionInfoData.from_dict(data_obj) + case SessionEventType.SESSION_WARNING: data = SessionWarningData.from_dict(data_obj) + case SessionEventType.SESSION_MODEL_CHANGE: data = SessionModelChangeData.from_dict(data_obj) + case SessionEventType.SESSION_MODE_CHANGED: data = SessionModeChangedData.from_dict(data_obj) + case SessionEventType.SESSION_PLAN_CHANGED: data = SessionPlanChangedData.from_dict(data_obj) + case SessionEventType.SESSION_WORKSPACE_FILE_CHANGED: data = SessionWorkspaceFileChangedData.from_dict(data_obj) + case SessionEventType.SESSION_IMPORT_LEGACY: data = SessionImportLegacyData.from_dict(data_obj) + case SessionEventType.SESSION_HANDOFF: data = SessionHandoffData.from_dict(data_obj) + case SessionEventType.SESSION_TRUNCATION: data = SessionTruncationData.from_dict(data_obj) + case SessionEventType.SESSION_SNAPSHOT_REWIND: data = SessionSnapshotRewindData.from_dict(data_obj) + case SessionEventType.SESSION_SHUTDOWN: data = SessionShutdownData.from_dict(data_obj) + case SessionEventType.SESSION_CONTEXT_CHANGED: data = SessionContextChangedData.from_dict(data_obj) + case SessionEventType.SESSION_USAGE_INFO: data = SessionUsageInfoData.from_dict(data_obj) + case SessionEventType.SESSION_COMPACTION_START: data = SessionCompactionStartData.from_dict(data_obj) + case SessionEventType.SESSION_COMPACTION_COMPLETE: data = SessionCompactionCompleteData.from_dict(data_obj) + case SessionEventType.SESSION_TASK_COMPLETE: data = SessionTaskCompleteData.from_dict(data_obj) + case SessionEventType.USER_MESSAGE: data = UserMessageData.from_dict(data_obj) + case SessionEventType.PENDING_MESSAGES_MODIFIED: data = PendingMessagesModifiedData.from_dict(data_obj) + case SessionEventType.ASSISTANT_TURN_START: data = AssistantTurnStartData.from_dict(data_obj) + case SessionEventType.ASSISTANT_INTENT: data = AssistantIntentData.from_dict(data_obj) + case SessionEventType.ASSISTANT_REASONING: data = AssistantReasoningData.from_dict(data_obj) + case SessionEventType.ASSISTANT_REASONING_DELTA: data = AssistantReasoningDeltaData.from_dict(data_obj) + case SessionEventType.ASSISTANT_STREAMING_DELTA: data = AssistantStreamingDeltaData.from_dict(data_obj) + case SessionEventType.ASSISTANT_MESSAGE: data = AssistantMessageData.from_dict(data_obj) + case SessionEventType.ASSISTANT_MESSAGE_DELTA: data = AssistantMessageDeltaData.from_dict(data_obj) + case SessionEventType.ASSISTANT_TURN_END: data = AssistantTurnEndData.from_dict(data_obj) + case SessionEventType.ASSISTANT_USAGE: data = AssistantUsageData.from_dict(data_obj) + case SessionEventType.ABORT: data = AbortData.from_dict(data_obj) + case SessionEventType.TOOL_USER_REQUESTED: data = ToolUserRequestedData.from_dict(data_obj) + case SessionEventType.TOOL_EXECUTION_START: data = ToolExecutionStartData.from_dict(data_obj) + case SessionEventType.TOOL_EXECUTION_PARTIAL_RESULT: data = ToolExecutionPartialResultData.from_dict(data_obj) + case SessionEventType.TOOL_EXECUTION_PROGRESS: data = ToolExecutionProgressData.from_dict(data_obj) + case SessionEventType.TOOL_EXECUTION_COMPLETE: data = ToolExecutionCompleteData.from_dict(data_obj) + case SessionEventType.SKILL_INVOKED: data = SkillInvokedData.from_dict(data_obj) + case SessionEventType.SUBAGENT_STARTED: data = SubagentStartedData.from_dict(data_obj) + case SessionEventType.SUBAGENT_COMPLETED: data = SubagentCompletedData.from_dict(data_obj) + case SessionEventType.SUBAGENT_FAILED: data = SubagentFailedData.from_dict(data_obj) + case SessionEventType.SUBAGENT_SELECTED: data = SubagentSelectedData.from_dict(data_obj) + case SessionEventType.SUBAGENT_DESELECTED: data = SubagentDeselectedData.from_dict(data_obj) + case SessionEventType.HOOK_START: data = HookStartData.from_dict(data_obj) + case SessionEventType.HOOK_END: data = HookEndData.from_dict(data_obj) + case SessionEventType.SYSTEM_MESSAGE: data = SystemMessageData.from_dict(data_obj) + case SessionEventType.SYSTEM_NOTIFICATION: data = SystemNotificationData.from_dict(data_obj) + case SessionEventType.PERMISSION_REQUESTED: data = PermissionRequestedData.from_dict(data_obj) + case SessionEventType.PERMISSION_COMPLETED: data = PermissionCompletedData.from_dict(data_obj) + case SessionEventType.USER_INPUT_REQUESTED: data = UserInputRequestedData.from_dict(data_obj) + case SessionEventType.USER_INPUT_COMPLETED: data = UserInputCompletedData.from_dict(data_obj) + case SessionEventType.ELICITATION_REQUESTED: data = ElicitationRequestedData.from_dict(data_obj) + case SessionEventType.ELICITATION_COMPLETED: data = ElicitationCompletedData.from_dict(data_obj) + case SessionEventType.SAMPLING_REQUESTED: data = SamplingRequestedData.from_dict(data_obj) + case SessionEventType.SAMPLING_COMPLETED: data = SamplingCompletedData.from_dict(data_obj) + case SessionEventType.MCP_OAUTH_REQUIRED: data = McpOauthRequiredData.from_dict(data_obj) + case SessionEventType.MCP_OAUTH_COMPLETED: data = McpOauthCompletedData.from_dict(data_obj) + case SessionEventType.EXTERNAL_TOOL_REQUESTED: data = ExternalToolRequestedData.from_dict(data_obj) + case SessionEventType.EXTERNAL_TOOL_COMPLETED: data = ExternalToolCompletedData.from_dict(data_obj) + case SessionEventType.COMMAND_QUEUED: data = CommandQueuedData.from_dict(data_obj) + case SessionEventType.COMMAND_EXECUTE: data = CommandExecuteData.from_dict(data_obj) + case SessionEventType.COMMAND_COMPLETED: data = CommandCompletedData.from_dict(data_obj) + case SessionEventType.COMMANDS_CHANGED: data = CommandsChangedData.from_dict(data_obj) + case SessionEventType.CAPABILITIES_CHANGED: data = CapabilitiesChangedData.from_dict(data_obj) + case SessionEventType.EXIT_PLAN_MODE_REQUESTED: data = ExitPlanModeRequestedData.from_dict(data_obj) + case SessionEventType.EXIT_PLAN_MODE_COMPLETED: data = ExitPlanModeCompletedData.from_dict(data_obj) + case SessionEventType.SESSION_TOOLS_UPDATED: data = SessionToolsUpdatedData.from_dict(data_obj) + case SessionEventType.SESSION_BACKGROUND_TASKS_CHANGED: data = SessionBackgroundTasksChangedData.from_dict(data_obj) + case SessionEventType.SESSION_SKILLS_LOADED: data = SessionSkillsLoadedData.from_dict(data_obj) + case SessionEventType.SESSION_CUSTOM_AGENTS_UPDATED: data = SessionCustomAgentsUpdatedData.from_dict(data_obj) + case SessionEventType.SESSION_MCP_SERVERS_LOADED: data = SessionMcpServersLoadedData.from_dict(data_obj) + case SessionEventType.SESSION_MCP_SERVER_STATUS_CHANGED: data = SessionMcpServerStatusChangedData.from_dict(data_obj) + case SessionEventType.SESSION_EXTENSIONS_LOADED: data = SessionExtensionsLoadedData.from_dict(data_obj) + case _: data = RawSessionEventData.from_dict(data_obj) + return SessionEvent( + data=data, + id=event_id, + timestamp=timestamp, + type=event_type, + ephemeral=ephemeral, + parent_id=parent_id, + raw_type=raw_type if event_type == SessionEventType.UNKNOWN else None, + ) def to_dict(self) -> dict: result: dict = {} - result["data"] = to_class(Data, self.data) - result["id"] = str(self.id) - result["timestamp"] = self.timestamp.isoformat() - result["type"] = to_enum(SessionEventType, self.type) + result["data"] = self.data.to_dict() + result["id"] = to_uuid(self.id) + result["timestamp"] = to_datetime(self.timestamp) + result["type"] = self.raw_type if self.type == SessionEventType.UNKNOWN and self.raw_type is not None else to_enum(SessionEventType, self.type) if self.ephemeral is not None: - result["ephemeral"] = from_union([from_bool, from_none], self.ephemeral) - result["parentId"] = from_union([from_none, lambda x: str(x)], self.parent_id) + result["ephemeral"] = from_bool(self.ephemeral) + result["parentId"] = from_union([from_none, to_uuid], self.parent_id) return result @@ -3427,4 +4485,241 @@ def session_event_from_dict(s: Any) -> SessionEvent: def session_event_to_dict(x: SessionEvent) -> Any: - return to_class(SessionEvent, x) + return x.to_dict() + + +# Compatibility shims for pre-refactor top-level generated types. +class RequestedSchemaType(str, Enum): + OBJECT = "object" + + +@dataclass +class ErrorClass: + """Backward-compatible shim for generic error payloads.""" + message: str + code: str | None = None + stack: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "ErrorClass": + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + code = from_union([from_none, lambda x: from_str(x)], obj.get("code")) + stack = from_union([from_none, lambda x: from_str(x)], obj.get("stack")) + return ErrorClass( + message=message, + code=code, + stack=stack, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = from_str(self.message) + if self.code is not None: + result["code"] = from_union([from_none, lambda x: from_str(x)], self.code) + if self.stack is not None: + result["stack"] = from_union([from_none, lambda x: from_str(x)], self.stack) + return result + + +@dataclass +class Resource: + """Backward-compatible shim for embedded tool result resources.""" + uri: str + mime_type: str | None = None + text: str | None = None + blob: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "Resource": + assert isinstance(obj, dict) + uri = from_str(obj.get("uri")) + mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType")) + text = from_union([from_none, lambda x: from_str(x)], obj.get("text")) + blob = from_union([from_none, lambda x: from_str(x)], obj.get("blob")) + return Resource( + uri=uri, + mime_type=mime_type, + text=text, + blob=blob, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["uri"] = from_str(self.uri) + if self.mime_type is not None: + result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type) + if self.text is not None: + result["text"] = from_union([from_none, lambda x: from_str(x)], self.text) + if self.blob is not None: + result["blob"] = from_union([from_none, lambda x: from_str(x)], self.blob) + return result + + +ContentType = ToolExecutionCompleteDataResultContentsItemType +Theme = ToolExecutionCompleteDataResultContentsItemIconsItemTheme +Icon = ToolExecutionCompleteDataResultContentsItemIconsItem +ResultKind = PermissionCompletedDataResultKind + + +@dataclass +class ContentElement: + """Backward-compatible shim for tool result content blocks.""" + type: ContentType + text: str | None = None + cwd: str | None = None + exit_code: float | None = None + data: str | None = None + mime_type: str | None = None + description: str | None = None + icons: list[Icon] | None = None + name: str | None = None + size: float | None = None + title: str | None = None + uri: str | None = None + resource: Resource | None = None + + @staticmethod + def from_dict(obj: Any) -> "ContentElement": + assert isinstance(obj, dict) + type = parse_enum(ContentType, obj.get("type")) + text = from_union([from_none, lambda x: from_str(x)], obj.get("text")) + cwd = from_union([from_none, lambda x: from_str(x)], obj.get("cwd")) + exit_code = from_union([from_none, lambda x: from_float(x)], obj.get("exitCode")) + data = from_union([from_none, lambda x: from_str(x)], obj.get("data")) + mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType")) + description = from_union([from_none, lambda x: from_str(x)], obj.get("description")) + icons = from_union([from_none, lambda x: from_list(Icon.from_dict, x)], obj.get("icons")) + name = from_union([from_none, lambda x: from_str(x)], obj.get("name")) + size = from_union([from_none, lambda x: from_float(x)], obj.get("size")) + title = from_union([from_none, lambda x: from_str(x)], obj.get("title")) + uri = from_union([from_none, lambda x: from_str(x)], obj.get("uri")) + resource = from_union([from_none, lambda x: Resource.from_dict(x)], obj.get("resource")) + return ContentElement( + type=type, + text=text, + cwd=cwd, + exit_code=exit_code, + data=data, + mime_type=mime_type, + description=description, + icons=icons, + name=name, + size=size, + title=title, + uri=uri, + resource=resource, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["type"] = to_enum(ContentType, self.type) + if self.text is not None: + result["text"] = from_union([from_none, lambda x: from_str(x)], self.text) + if self.cwd is not None: + result["cwd"] = from_union([from_none, lambda x: from_str(x)], self.cwd) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, lambda x: to_float(x)], self.exit_code) + if self.data is not None: + result["data"] = from_union([from_none, lambda x: from_str(x)], self.data) + if self.mime_type is not None: + result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type) + if self.description is not None: + result["description"] = from_union([from_none, lambda x: from_str(x)], self.description) + if self.icons is not None: + result["icons"] = from_union([from_none, lambda x: from_list(lambda x: to_class(Icon, x), x)], self.icons) + if self.name is not None: + result["name"] = from_union([from_none, lambda x: from_str(x)], self.name) + if self.size is not None: + result["size"] = from_union([from_none, lambda x: to_float(x)], self.size) + if self.title is not None: + result["title"] = from_union([from_none, lambda x: from_str(x)], self.title) + if self.uri is not None: + result["uri"] = from_union([from_none, lambda x: from_str(x)], self.uri) + if self.resource is not None: + result["resource"] = from_union([from_none, lambda x: to_class(Resource, x)], self.resource) + return result + + +@dataclass +class Result: + """Backward-compatible shim for generic result payloads.""" + content: str | None = None + contents: list[ContentElement] | None = None + detailed_content: str | None = None + kind: ResultKind | None = None + + @staticmethod + def from_dict(obj: Any) -> "Result": + assert isinstance(obj, dict) + content = from_union([from_none, lambda x: from_str(x)], obj.get("content")) + contents = from_union([from_none, lambda x: from_list(ContentElement.from_dict, x)], obj.get("contents")) + detailed_content = from_union([from_none, lambda x: from_str(x)], obj.get("detailedContent")) + kind = from_union([from_none, lambda x: parse_enum(ResultKind, x)], obj.get("kind")) + return Result( + content=content, + contents=contents, + detailed_content=detailed_content, + kind=kind, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.content is not None: + result["content"] = from_union([from_none, lambda x: from_str(x)], self.content) + if self.contents is not None: + result["contents"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ContentElement, x), x)], self.contents) + if self.detailed_content is not None: + result["detailedContent"] = from_union([from_none, lambda x: from_str(x)], self.detailed_content) + if self.kind is not None: + result["kind"] = from_union([from_none, lambda x: to_enum(ResultKind, x)], self.kind) + return result + + +# Convenience aliases for commonly used nested event types. +Action = ElicitationCompletedDataAction +Agent = SessionCustomAgentsUpdatedDataAgentsItem +AgentMode = UserMessageDataAgentMode +Attachment = UserMessageDataAttachmentsItem +AttachmentType = UserMessageDataAttachmentsItemType +CodeChanges = SessionShutdownDataCodeChanges +CompactionTokensUsed = SessionCompactionCompleteDataCompactionTokensUsed +ContextClass = SessionStartDataContext +CopilotUsage = AssistantUsageDataCopilotUsage +DataCommand = CommandsChangedDataCommandsItem +End = UserMessageDataAttachmentsItemSelectionEnd +Extension = SessionExtensionsLoadedDataExtensionsItem +ExtensionStatus = SessionExtensionsLoadedDataExtensionsItemStatus +HostType = SessionStartDataContextHostType +KindClass = SystemNotificationDataKind +KindStatus = SystemNotificationDataKindStatus +KindType = SystemNotificationDataKindType +LineRange = UserMessageDataAttachmentsItemLineRange +Metadata = SystemMessageDataMetadata +Mode = ElicitationRequestedDataMode +ModelMetric = SessionShutdownDataModelMetricsValue +Operation = SessionPlanChangedDataOperation +PermissionRequest = PermissionRequestedDataPermissionRequest +PermissionRequestKind = PermissionRequestedDataPermissionRequestKind +PermissionRequestCommand = PermissionRequestedDataPermissionRequestCommandsItem +PossibleURL = PermissionRequestedDataPermissionRequestPossibleUrlsItem +QuotaSnapshot = AssistantUsageDataQuotaSnapshotsValue +ReferenceType = UserMessageDataAttachmentsItemReferenceType +RepositoryClass = SessionHandoffDataRepository +RequestedSchema = ElicitationRequestedDataRequestedSchema +Requests = SessionShutdownDataModelMetricsValueRequests +Role = SystemMessageDataRole +Selection = UserMessageDataAttachmentsItemSelection +Server = SessionMcpServersLoadedDataServersItem +ServerStatus = SessionMcpServersLoadedDataServersItemStatus +ShutdownType = SessionShutdownDataShutdownType +Skill = SessionSkillsLoadedDataSkillsItem +Source = SessionExtensionsLoadedDataExtensionsItemSource +SourceType = SessionHandoffDataSourceType +Start = UserMessageDataAttachmentsItemSelectionStart +StaticClientConfig = McpOauthRequiredDataStaticClientConfig +TokenDetail = AssistantUsageDataCopilotUsageTokenDetailsItem +ToolRequest = AssistantMessageDataToolRequestsItem +ToolRequestType = AssistantMessageDataToolRequestsItemType +UI = CapabilitiesChangedDataUi +Usage = SessionShutdownDataModelMetricsValueUsage \ No newline at end of file diff --git a/python/copilot/session.py b/python/copilot/session.py index 5edbe924b..6b0a7e03e 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -45,7 +45,12 @@ ) from .generated.rpc import ModelCapabilitiesOverride as _RpcModelCapabilitiesOverride from .generated.session_events import ( + CapabilitiesChangedData, + CommandExecuteData, + ElicitationRequestedData, + ExternalToolRequestedData, PermissionRequest, + PermissionRequestedData, SessionEvent, SessionEventType, session_event_from_dict, @@ -1226,8 +1231,9 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: are broadcast as session events to all clients. """ if event.type == SessionEventType.EXTERNAL_TOOL_REQUESTED: - request_id = event.data.request_id - tool_name = event.data.tool_name + data = cast(ExternalToolRequestedData, event.data) + request_id = data.request_id + tool_name = data.tool_name if not request_id or not tool_name: return @@ -1235,10 +1241,10 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: if not handler: return # This client doesn't handle this tool; another client will. - tool_call_id = event.data.tool_call_id or "" - arguments = event.data.arguments - tp = getattr(event.data, "traceparent", None) - ts = getattr(event.data, "tracestate", None) + tool_call_id = data.tool_call_id or "" + arguments = data.arguments + tp = getattr(data, "traceparent", None) + ts = getattr(data, "tracestate", None) asyncio.ensure_future( self._execute_tool_and_respond( request_id, tool_name, tool_call_id, arguments, handler, tp, ts @@ -1246,12 +1252,13 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: ) elif event.type == SessionEventType.PERMISSION_REQUESTED: - request_id = event.data.request_id - permission_request = event.data.permission_request + data = cast(PermissionRequestedData, event.data) + request_id = data.request_id + permission_request = data.permission_request if not request_id or not permission_request: return - resolved_by_hook = getattr(event.data, "resolved_by_hook", None) + resolved_by_hook = getattr(data, "resolved_by_hook", None) if resolved_by_hook: return # Already resolved by a permissionRequest hook; no client action needed. @@ -1265,10 +1272,11 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: ) elif event.type == SessionEventType.COMMAND_EXECUTE: - request_id = event.data.request_id - command_name = event.data.command_name - command = event.data.command - args = event.data.args + data = cast(CommandExecuteData, event.data) + request_id = data.request_id + command_name = data.command_name + command = data.command + args = data.args if not request_id or not command_name: return asyncio.ensure_future( @@ -1278,33 +1286,35 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: ) elif event.type == SessionEventType.ELICITATION_REQUESTED: + data = cast(ElicitationRequestedData, event.data) with self._elicitation_handler_lock: handler = self._elicitation_handler if not handler: return - request_id = event.data.request_id + request_id = data.request_id if not request_id: return context: ElicitationContext = { "session_id": self.session_id, - "message": event.data.message or "", + "message": data.message or "", } - if event.data.requested_schema is not None: - context["requestedSchema"] = event.data.requested_schema.to_dict() - if event.data.mode is not None: - context["mode"] = event.data.mode.value - if event.data.elicitation_source is not None: - context["elicitationSource"] = event.data.elicitation_source - if event.data.url is not None: - context["url"] = event.data.url + if data.requested_schema is not None: + context["requestedSchema"] = data.requested_schema.to_dict() + if data.mode is not None: + context["mode"] = data.mode.value + if data.elicitation_source is not None: + context["elicitationSource"] = data.elicitation_source + if data.url is not None: + context["url"] = data.url asyncio.ensure_future(self._handle_elicitation_request(context, request_id)) elif event.type == SessionEventType.CAPABILITIES_CHANGED: + data = cast(CapabilitiesChangedData, event.data) cap: SessionCapabilities = {} - if event.data.ui is not None: + if data.ui is not None: ui_cap: SessionUiCapabilities = {} - if event.data.ui.elicitation is not None: - ui_cap["elicitation"] = event.data.ui.elicitation + if data.ui.elicitation is not None: + ui_cap["elicitation"] = data.ui.elicitation cap["ui"] = ui_cap self._capabilities = {**self._capabilities, **cap} diff --git a/python/test_commands_and_elicitation.py b/python/test_commands_and_elicitation.py index 6b8518e26..bab9d0b98 100644 --- a/python/test_commands_and_elicitation.py +++ b/python/test_commands_and_elicitation.py @@ -136,13 +136,13 @@ async def mock_request(method, params): # Simulate a command.execute broadcast event from copilot.generated.session_events import ( - Data, + CommandExecuteData, SessionEvent, SessionEventType, ) event = SessionEvent( - data=Data( + data=CommandExecuteData( request_id="req-1", command="/deploy production", command_name="deploy", @@ -203,13 +203,13 @@ async def mock_request(method, params): client._client.request = mock_request from copilot.generated.session_events import ( - Data, + CommandExecuteData, SessionEvent, SessionEventType, ) event = SessionEvent( - data=Data( + data=CommandExecuteData( request_id="req-2", command="/fail", command_name="fail", @@ -257,13 +257,13 @@ async def mock_request(method, params): client._client.request = mock_request from copilot.generated.session_events import ( - Data, + CommandExecuteData, SessionEvent, SessionEventType, ) event = SessionEvent( - data=Data( + data=CommandExecuteData( request_id="req-3", command="/unknown", command_name="unknown", @@ -519,13 +519,13 @@ async def mock_request(method, params): client._client.request = mock_request from copilot.generated.session_events import ( - Data, + ElicitationRequestedData, SessionEvent, SessionEventType, ) event = SessionEvent( - data=Data( + data=ElicitationRequestedData( request_id="req-elicit-1", message="Pick a color", ), @@ -578,19 +578,18 @@ async def mock_request(method, params): client._client.request = mock_request from copilot.generated.session_events import ( - Data, - ElicitationRequestedSchema, - RequestedSchemaType, + ElicitationRequestedData, + RequestedSchema, SessionEvent, SessionEventType, ) event = SessionEvent( - data=Data( + data=ElicitationRequestedData( request_id="req-schema-1", message="Fill in your details", - requested_schema=ElicitationRequestedSchema( - type=RequestedSchemaType.OBJECT, + requested_schema=RequestedSchema( + type="object", properties={ "name": {"type": "string"}, "age": {"type": "number"}, @@ -638,14 +637,14 @@ async def test_capabilities_changed_event_updates_session(self): session._set_capabilities({}) from copilot.generated.session_events import ( - CapabilitiesChangedUI, - Data, + UI, + CapabilitiesChangedData, SessionEvent, SessionEventType, ) event = SessionEvent( - data=Data(ui=CapabilitiesChangedUI(elicitation=True)), + data=CapabilitiesChangedData(ui=UI(elicitation=True)), id="evt-cap-1", timestamp="2025-01-01T00:00:00Z", type=SessionEventType.CAPABILITIES_CHANGED, diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 017cff2e8..c8ceecb26 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -12,7 +12,21 @@ import pytest -from copilot.generated.session_events import SessionEventType, session_event_from_dict +from copilot.generated.session_events import ( + Action, + AgentMode, + ContentElement, + Data, + Mode, + ReferenceType, + RequestedSchema, + RequestedSchemaType, + Resource, + Result, + ResultKind, + SessionEventType, + session_event_from_dict, +) class TestEventForwardCompatibility: @@ -62,3 +76,55 @@ def test_malformed_timestamp_raises_error(self): # This should raise an error and NOT be silently suppressed with pytest.raises((ValueError, TypeError)): session_event_from_dict(malformed_event) + + def test_legacy_top_level_generated_symbols_remain_available(self): + """Previously top-level generated helper symbols should remain importable.""" + assert Action.ACCEPT.value == "accept" + assert AgentMode.INTERACTIVE.value == "interactive" + assert Mode.FORM.value == "form" + assert ReferenceType.PR.value == "pr" + + schema = RequestedSchema(properties={"answer": {"type": "string"}}, type=RequestedSchemaType.OBJECT) + assert schema.to_dict()["type"] == "object" + + result = Result( + content="Approved", + kind=ResultKind.APPROVED, + contents=[ + ContentElement( + type=ContentElement.from_dict({"type": "text", "text": "hello"}).type, + text="hello", + resource=Resource(uri="file://artifact.txt", text="artifact"), + ) + ], + ) + assert result.to_dict() == { + "content": "Approved", + "kind": "approved", + "contents": [ + { + "type": "text", + "text": "hello", + "resource": { + "uri": "file://artifact.txt", + "text": "artifact", + }, + } + ], + } + + def test_data_shim_preserves_raw_mapping_values(self): + """Compatibility Data should keep arbitrary nested mappings as plain dicts.""" + parsed = Data.from_dict( + { + "arguments": {"toolCallId": "call-1"}, + "input": {"step_name": "build"}, + } + ) + assert parsed.arguments == {"toolCallId": "call-1"} + assert isinstance(parsed.arguments, dict) + assert parsed.input == {"step_name": "build"} + assert isinstance(parsed.input, dict) + + constructed = Data(arguments={"tool_call_id": "call-1"}) + assert constructed.to_dict() == {"arguments": {"tool_call_id": "call-1"}} diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index f22a83ff9..5b3825d56 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -18,9 +18,9 @@ import { isObjectSchema, isVoidSchema, isRpcMethod, + isNodeFullyExperimental, postProcessSchema, writeGeneratedFile, - isNodeFullyExperimental, type ApiSchema, type RpcMethod, } from "./utils.js"; @@ -210,66 +210,1144 @@ function pythonParamsTypeName(method: RpcMethod): string { } // ── Session Events ────────────────────────────────────────────────────────── +// ── Session Events (custom codegen — dedicated per-event payload types) ───── -async function generateSessionEvents(schemaPath?: string): Promise { - console.log("Python: generating session-events..."); +interface PyEventVariant { + typeName: string; + dataClassName: string; + dataSchema: JSONSchema7; + dataDescription?: string; +} - const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); - const schema = cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7); - const resolvedSchema = (schema.definitions?.SessionEvent as JSONSchema7) || schema; - const processed = postProcessSchema(resolvedSchema); +interface PyResolvedType { + annotation: string; + fromExpr: (expr: string) => string; + toExpr: (expr: string) => string; +} - // Hoist titled inline schemas (enums etc.) to definitions so quicktype - // uses the schema-defined names instead of its own structural heuristics. - const { rootDefinitions: hoistedRoots, sharedDefinitions } = hoistTitledSchemas({ SessionEvent: processed }); - const hoisted = hoistedRoots.SessionEvent; - if (Object.keys(sharedDefinitions).length > 0) { - hoisted.definitions = { ...hoisted.definitions, ...sharedDefinitions }; +interface PyCodegenCtx { + classes: string[]; + enums: string[]; + enumsByValues: Map; + generatedNames: Set; +} + +function toEnumMemberName(value: string): string { + const cleaned = value + .replace(/([a-z])([A-Z])/g, "$1_$2") + .replace(/[^A-Za-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") + .toUpperCase(); + if (!cleaned) { + return "VALUE"; } + return /^[0-9]/.test(cleaned) ? `VALUE_${cleaned}` : cleaned; +} - const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); - await schemaInput.addSource({ name: "SessionEvent", schema: JSON.stringify(hoisted) }); +function wrapParser(resolved: PyResolvedType, arg = "x"): string { + return `lambda ${arg}: ${resolved.fromExpr(arg)}`; +} - const inputData = new InputData(); - inputData.addInput(schemaInput); +function wrapSerializer(resolved: PyResolvedType, arg = "x"): string { + return `lambda ${arg}: ${resolved.toExpr(arg)}`; +} - const result = await quicktype({ - inputData, - lang: "python", - rendererOptions: { "python-version": "3.7" }, +function pyPrimitiveResolvedType(annotation: string, fromFn: string, toFn = fromFn): PyResolvedType { + return { + annotation, + fromExpr: (expr) => `${fromFn}(${expr})`, + toExpr: (expr) => `${toFn}(${expr})`, + }; +} + +function pyOptionalResolvedType(inner: PyResolvedType): PyResolvedType { + return { + annotation: `${inner.annotation} | None`, + fromExpr: (expr) => `from_union([from_none, ${wrapParser(inner)}], ${expr})`, + toExpr: (expr) => `from_union([from_none, ${wrapSerializer(inner)}], ${expr})`, + }; +} + +function pyAnyResolvedType(): PyResolvedType { + return { + annotation: "Any", + fromExpr: (expr) => expr, + toExpr: (expr) => expr, + }; +} + +function extractPyEventVariants(schema: JSONSchema7): PyEventVariant[] { + const sessionEvent = schema.definitions?.SessionEvent as JSONSchema7; + if (!sessionEvent?.anyOf) { + throw new Error("Schema must have SessionEvent definition with anyOf"); + } + + return (sessionEvent.anyOf as JSONSchema7[]) + .map((variant) => { + if (typeof variant !== "object" || !variant.properties) { + throw new Error("Invalid event variant"); + } + + const typeSchema = variant.properties.type as JSONSchema7; + const typeName = typeSchema?.const as string; + if (!typeName) { + throw new Error("Event variant must define type.const"); + } + + const dataSchema = (variant.properties.data as JSONSchema7) || {}; + return { + typeName, + dataClassName: `${toPascalCase(typeName)}Data`, + dataSchema, + dataDescription: dataSchema.description, + }; + }); +} + +function findPyDiscriminator( + variants: JSONSchema7[] +): { property: string; mapping: Map } | null { + if (variants.length === 0) { + return null; + } + + const firstVariant = variants[0]; + if (!firstVariant.properties) { + return null; + } + + for (const [propName, propSchema] of Object.entries(firstVariant.properties)) { + if (typeof propSchema !== "object") { + continue; + } + if ((propSchema as JSONSchema7).const === undefined) { + continue; + } + + const mapping = new Map(); + let valid = true; + for (const variant of variants) { + if (!variant.properties) { + valid = false; + break; + } + + const variantProp = variant.properties[propName]; + if (typeof variantProp !== "object" || (variantProp as JSONSchema7).const === undefined) { + valid = false; + break; + } + + mapping.set(String((variantProp as JSONSchema7).const), variant); + } + + if (valid && mapping.size === variants.length) { + return { property: propName, mapping }; + } + } + + return null; +} + +function getOrCreatePyEnum( + enumName: string, + values: string[], + ctx: PyCodegenCtx, + description?: string +): string { + const valuesKey = [...values].sort().join("|"); + const existing = ctx.enumsByValues.get(valuesKey); + if (existing) { + return existing; + } + + const lines: string[] = []; + if (description) { + lines.push(`class ${enumName}(Enum):`); + lines.push(` """${description.replace(/"/g, '\\"')}"""`); + } else { + lines.push(`class ${enumName}(Enum):`); + } + for (const value of values) { + lines.push(` ${toEnumMemberName(value)} = ${JSON.stringify(value)}`); + } + ctx.enumsByValues.set(valuesKey, enumName); + ctx.enums.push(lines.join("\n")); + return enumName; +} + +function resolvePyPropertyType( + propSchema: JSONSchema7, + parentTypeName: string, + jsonPropName: string, + isRequired: boolean, + ctx: PyCodegenCtx +): PyResolvedType { + const nestedName = parentTypeName + toPascalCase(jsonPropName); + + if (propSchema.allOf && propSchema.allOf.length === 1 && typeof propSchema.allOf[0] === "object") { + return resolvePyPropertyType( + propSchema.allOf[0] as JSONSchema7, + parentTypeName, + jsonPropName, + isRequired, + ctx + ); + } + + if (propSchema.anyOf) { + const variants = (propSchema.anyOf as JSONSchema7[]).filter((item) => typeof item === "object"); + const nonNull = variants.filter((item) => item.type !== "null"); + const hasNull = variants.length !== nonNull.length; + + if (nonNull.length === 1) { + const inner = resolvePyPropertyType(nonNull[0], parentTypeName, jsonPropName, true, ctx); + return hasNull || !isRequired ? pyOptionalResolvedType(inner) : inner; + } + + if (nonNull.length > 1) { + const discriminator = findPyDiscriminator(nonNull); + if (discriminator) { + emitPyFlatDiscriminatedUnion( + nestedName, + discriminator.property, + discriminator.mapping, + ctx, + propSchema.description + ); + const resolved: PyResolvedType = { + annotation: nestedName, + fromExpr: (expr) => `${nestedName}.from_dict(${expr})`, + toExpr: (expr) => `to_class(${nestedName}, ${expr})`, + }; + return hasNull || !isRequired ? pyOptionalResolvedType(resolved) : resolved; + } + + return pyAnyResolvedType(); + } + } + + if (propSchema.enum && Array.isArray(propSchema.enum) && propSchema.enum.every((value) => typeof value === "string")) { + const enumType = getOrCreatePyEnum( + nestedName, + propSchema.enum as string[], + ctx, + propSchema.description + ); + const resolved: PyResolvedType = { + annotation: enumType, + fromExpr: (expr) => `parse_enum(${enumType}, ${expr})`, + toExpr: (expr) => `to_enum(${enumType}, ${expr})`, + }; + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + if (propSchema.const !== undefined) { + if (typeof propSchema.const === "string") { + const resolved = pyPrimitiveResolvedType("str", "from_str"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + if (typeof propSchema.const === "boolean") { + const resolved = pyPrimitiveResolvedType("bool", "from_bool"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + if (typeof propSchema.const === "number") { + const resolved = Number.isInteger(propSchema.const) + ? pyPrimitiveResolvedType("int", "from_int", "to_int") + : pyPrimitiveResolvedType("float", "from_float", "to_float"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + } + + const type = propSchema.type; + const format = propSchema.format; + + if (Array.isArray(type)) { + const nonNullTypes = type.filter((value) => value !== "null"); + if (nonNullTypes.length === 1) { + const inner = resolvePyPropertyType( + { ...propSchema, type: nonNullTypes[0] as JSONSchema7["type"] }, + parentTypeName, + jsonPropName, + true, + ctx + ); + return pyOptionalResolvedType(inner); + } + } + + if (type === "string") { + if (format === "date-time") { + const resolved = pyPrimitiveResolvedType("datetime", "from_datetime", "to_datetime"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + if (format === "uuid") { + const resolved = pyPrimitiveResolvedType("UUID", "from_uuid", "to_uuid"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + const resolved = pyPrimitiveResolvedType("str", "from_str"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + if (type === "integer") { + const resolved = pyPrimitiveResolvedType("int", "from_int", "to_int"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + if (type === "number") { + const resolved = pyPrimitiveResolvedType("float", "from_float", "to_float"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + if (type === "boolean") { + const resolved = pyPrimitiveResolvedType("bool", "from_bool"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + if (type === "array") { + const items = propSchema.items as JSONSchema7 | undefined; + if (!items) { + const resolved: PyResolvedType = { + annotation: "list[Any]", + fromExpr: (expr) => `from_list(lambda x: x, ${expr})`, + toExpr: (expr) => `from_list(lambda x: x, ${expr})`, + }; + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + if (items.allOf && items.allOf.length === 1 && typeof items.allOf[0] === "object") { + return resolvePyPropertyType( + { ...propSchema, items: items.allOf[0] as JSONSchema7 }, + parentTypeName, + jsonPropName, + isRequired, + ctx + ); + } + + if (items.anyOf) { + const itemVariants = (items.anyOf as JSONSchema7[]) + .filter((variant) => typeof variant === "object") + .filter((variant) => variant.type !== "null"); + const discriminator = findPyDiscriminator(itemVariants); + if (discriminator) { + const itemTypeName = nestedName + "Item"; + emitPyFlatDiscriminatedUnion( + itemTypeName, + discriminator.property, + discriminator.mapping, + ctx, + items.description + ); + const resolved: PyResolvedType = { + annotation: `list[${itemTypeName}]`, + fromExpr: (expr) => `from_list(${itemTypeName}.from_dict, ${expr})`, + toExpr: (expr) => `from_list(lambda x: to_class(${itemTypeName}, x), ${expr})`, + }; + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + } + + const itemType = resolvePyPropertyType(items, parentTypeName, jsonPropName + "Item", true, ctx); + const resolved: PyResolvedType = { + annotation: `list[${itemType.annotation}]`, + fromExpr: (expr) => `from_list(${wrapParser(itemType)}, ${expr})`, + toExpr: (expr) => `from_list(${wrapSerializer(itemType)}, ${expr})`, + }; + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + if (type === "object" || (propSchema.properties && !type)) { + if (propSchema.properties) { + emitPyClass(nestedName, propSchema, ctx, propSchema.description); + const resolved: PyResolvedType = { + annotation: nestedName, + fromExpr: (expr) => `${nestedName}.from_dict(${expr})`, + toExpr: (expr) => `to_class(${nestedName}, ${expr})`, + }; + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + if (propSchema.additionalProperties) { + if ( + typeof propSchema.additionalProperties === "object" && + Object.keys(propSchema.additionalProperties as Record).length > 0 + ) { + const valueType = resolvePyPropertyType( + propSchema.additionalProperties as JSONSchema7, + parentTypeName, + jsonPropName + "Value", + true, + ctx + ); + const resolved: PyResolvedType = { + annotation: `dict[str, ${valueType.annotation}]`, + fromExpr: (expr) => `from_dict(${wrapParser(valueType)}, ${expr})`, + toExpr: (expr) => `from_dict(${wrapSerializer(valueType)}, ${expr})`, + }; + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + const resolved: PyResolvedType = { + annotation: "dict[str, Any]", + fromExpr: (expr) => `from_dict(lambda x: x, ${expr})`, + toExpr: (expr) => `from_dict(lambda x: x, ${expr})`, + }; + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } + + return pyAnyResolvedType(); + } + + return pyAnyResolvedType(); +} + +function emitPyClass( + typeName: string, + schema: JSONSchema7, + ctx: PyCodegenCtx, + description?: string +): void { + if (ctx.generatedNames.has(typeName)) { + return; + } + ctx.generatedNames.add(typeName); + + const required = new Set(schema.required || []); + const fieldEntries = Object.entries(schema.properties || {}).filter( + ([, value]) => typeof value === "object" + ) as Array<[string, JSONSchema7]>; + const orderedFieldEntries = [ + ...fieldEntries.filter(([name]) => required.has(name)), + ...fieldEntries.filter(([name]) => !required.has(name)), + ]; + + const fieldInfos = orderedFieldEntries.map(([propName, propSchema]) => { + const isRequired = required.has(propName); + const resolved = resolvePyPropertyType(propSchema, typeName, propName, isRequired, ctx); + return { + jsonName: propName, + fieldName: toSnakeCase(propName), + isRequired, + resolved, + }; }); - let code = result.lines.join("\n"); + const lines: string[] = []; + lines.push(`@dataclass`); + lines.push(`class ${typeName}:`); + if (description || schema.description) { + lines.push(` """${(description || schema.description || "").replace(/"/g, '\\"')}"""`); + } + + if (fieldInfos.length === 0) { + lines.push(` @staticmethod`); + lines.push(` def from_dict(obj: Any) -> "${typeName}":`); + lines.push(` assert isinstance(obj, dict)`); + lines.push(` return ${typeName}()`); + lines.push(``); + lines.push(` def to_dict(self) -> dict:`); + lines.push(` return {}`); + ctx.classes.push(lines.join("\n")); + return; + } - // Fix dataclass field ordering (Any fields need defaults) - code = code.replace(/: Any$/gm, ": Any = None"); - // Fix bare except: to use Exception (required by ruff/pylint) - code = code.replace(/except:/g, "except Exception:"); - // Modernize to Python 3.11+ syntax - code = modernizePython(code); + for (const field of fieldInfos) { + const suffix = field.isRequired ? "" : " = None"; + lines.push(` ${field.fieldName}: ${field.resolved.annotation}${suffix}`); + } - // Add UNKNOWN enum value for forward compatibility - code = code.replace( - /^(class SessionEventType\(Enum\):.*?)(^\s*\n@dataclass)/ms, - `$1 # UNKNOWN is used for forward compatibility - UNKNOWN = "unknown" + lines.push(``); + lines.push(` @staticmethod`); + lines.push(` def from_dict(obj: Any) -> "${typeName}":`); + lines.push(` assert isinstance(obj, dict)`); + for (const field of fieldInfos) { + lines.push( + ` ${field.fieldName} = ${field.resolved.fromExpr(`obj.get(${JSON.stringify(field.jsonName)})`)}` + ); + } + lines.push(` return ${typeName}(`); + for (const field of fieldInfos) { + lines.push(` ${field.fieldName}=${field.fieldName},`); + } + lines.push(` )`); + lines.push(``); + lines.push(` def to_dict(self) -> dict:`); + lines.push(` result: dict = {}`); + for (const field of fieldInfos) { + const valueExpr = field.resolved.toExpr(`self.${field.fieldName}`); + if (field.isRequired) { + lines.push(` result[${JSON.stringify(field.jsonName)}] = ${valueExpr}`); + } else { + lines.push(` if self.${field.fieldName} is not None:`); + lines.push(` result[${JSON.stringify(field.jsonName)}] = ${valueExpr}`); + } + } + lines.push(` return result`); + + ctx.classes.push(lines.join("\n")); +} + +function emitPyFlatDiscriminatedUnion( + typeName: string, + discriminatorProp: string, + mapping: Map, + ctx: PyCodegenCtx, + description?: string +): void { + if (ctx.generatedNames.has(typeName)) { + return; + } + ctx.generatedNames.add(typeName); + + const allProps = new Map(); + for (const [, variant] of mapping) { + const required = new Set(variant.required || []); + for (const [propName, propSchema] of Object.entries(variant.properties || {})) { + if (typeof propSchema !== "object") { + continue; + } + if (!allProps.has(propName)) { + allProps.set(propName, { + schema: propSchema as JSONSchema7, + requiredInAll: required.has(propName), + }); + } else if (!required.has(propName)) { + allProps.get(propName)!.requiredInAll = false; + } + } + } - @classmethod - def _missing_(cls, value: object) -> "SessionEventType": - """Handle unknown event types gracefully for forward compatibility.""" - return cls.UNKNOWN + const variantCount = mapping.size; + for (const [propName, info] of allProps) { + let presentCount = 0; + for (const [, variant] of mapping) { + if (variant.properties && propName in variant.properties) { + presentCount++; + } + } + if (presentCount < variantCount) { + info.requiredInAll = false; + } + } -$2` + const discriminatorEnumName = getOrCreatePyEnum( + typeName + toPascalCase(discriminatorProp), + [...mapping.keys()], + ctx, + description ? `${description} discriminator` : `${typeName} discriminator` ); - const banner = `""" -AUTO-GENERATED FILE - DO NOT EDIT -Generated from: session-events.schema.json -""" + const fieldEntries: Array<[string, JSONSchema7, boolean]> = [ + [ + discriminatorProp, + { + type: "string", + enum: [...mapping.keys()], + }, + true, + ], + ...[...allProps.entries()] + .filter(([propName]) => propName !== discriminatorProp) + .map(([propName, info]) => [propName, info.schema, info.requiredInAll] as [string, JSONSchema7, boolean]), + ]; + + const orderedFieldEntries = [ + ...fieldEntries.filter(([, , requiredInAll]) => requiredInAll), + ...fieldEntries.filter(([, , requiredInAll]) => !requiredInAll), + ]; + + const fieldInfos = orderedFieldEntries.map(([propName, propSchema, requiredInAll]) => { + let resolved: PyResolvedType; + if (propName === discriminatorProp) { + resolved = { + annotation: discriminatorEnumName, + fromExpr: (expr) => `parse_enum(${discriminatorEnumName}, ${expr})`, + toExpr: (expr) => `to_enum(${discriminatorEnumName}, ${expr})`, + }; + } else { + resolved = resolvePyPropertyType(propSchema, typeName, propName, requiredInAll, ctx); + } + + return { + jsonName: propName, + fieldName: toSnakeCase(propName), + isRequired: requiredInAll, + resolved, + }; + }); + + const lines: string[] = []; + lines.push(`@dataclass`); + lines.push(`class ${typeName}:`); + if (description) { + lines.push(` """${description.replace(/"/g, '\\"')}"""`); + } + for (const field of fieldInfos) { + const suffix = field.isRequired ? "" : " = None"; + lines.push(` ${field.fieldName}: ${field.resolved.annotation}${suffix}`); + } + lines.push(``); + lines.push(` @staticmethod`); + lines.push(` def from_dict(obj: Any) -> "${typeName}":`); + lines.push(` assert isinstance(obj, dict)`); + for (const field of fieldInfos) { + lines.push( + ` ${field.fieldName} = ${field.resolved.fromExpr(`obj.get(${JSON.stringify(field.jsonName)})`)}` + ); + } + lines.push(` return ${typeName}(`); + for (const field of fieldInfos) { + lines.push(` ${field.fieldName}=${field.fieldName},`); + } + lines.push(` )`); + lines.push(``); + lines.push(` def to_dict(self) -> dict:`); + lines.push(` result: dict = {}`); + for (const field of fieldInfos) { + const valueExpr = field.resolved.toExpr(`self.${field.fieldName}`); + if (field.isRequired) { + lines.push(` result[${JSON.stringify(field.jsonName)}] = ${valueExpr}`); + } else { + lines.push(` if self.${field.fieldName} is not None:`); + lines.push(` result[${JSON.stringify(field.jsonName)}] = ${valueExpr}`); + } + } + lines.push(` return result`); + + ctx.classes.push(lines.join("\n")); +} -`; +function generatePythonSessionEventsCode(schema: JSONSchema7): string { + const variants = extractPyEventVariants(schema); + const ctx: PyCodegenCtx = { + classes: [], + enums: [], + enumsByValues: new Map(), + generatedNames: new Set(), + }; + + for (const variant of variants) { + emitPyClass(variant.dataClassName, variant.dataSchema, ctx, variant.dataDescription); + } + + const eventTypeLines: string[] = []; + eventTypeLines.push(`class SessionEventType(Enum):`); + for (const variant of variants) { + eventTypeLines.push(` ${toEnumMemberName(variant.typeName)} = ${JSON.stringify(variant.typeName)}`); + } + eventTypeLines.push(` UNKNOWN = "unknown"`); + eventTypeLines.push(``); + eventTypeLines.push(` @classmethod`); + eventTypeLines.push(` def _missing_(cls, value: object) -> "SessionEventType":`); + eventTypeLines.push(` return cls.UNKNOWN`); + + const out: string[] = []; + out.push(`"""`); + out.push(`AUTO-GENERATED FILE - DO NOT EDIT`); + out.push(`Generated from: session-events.schema.json`); + out.push(`"""`); + out.push(``); + out.push(`from __future__ import annotations`); + out.push(``); + out.push(`from collections.abc import Callable`); + out.push(`from dataclasses import dataclass`); + out.push(`from datetime import datetime`); + out.push(`from enum import Enum`); + out.push(`from typing import Any, TypeVar, cast`); + out.push(`from uuid import UUID`); + out.push(``); + out.push(`import dateutil.parser`); + out.push(``); + out.push(`T = TypeVar("T")`); + out.push(`EnumT = TypeVar("EnumT", bound=Enum)`); + out.push(``); + out.push(``); + out.push(`def from_str(x: Any) -> str:`); + out.push(` assert isinstance(x, str)`); + out.push(` return x`); + out.push(``); + out.push(``); + out.push(`def from_int(x: Any) -> int:`); + out.push(` assert isinstance(x, int) and not isinstance(x, bool)`); + out.push(` return x`); + out.push(``); + out.push(``); + out.push(`def to_int(x: Any) -> int:`); + out.push(` assert isinstance(x, int) and not isinstance(x, bool)`); + out.push(` return x`); + out.push(``); + out.push(``); + out.push(`def from_float(x: Any) -> float:`); + out.push(` assert isinstance(x, (float, int)) and not isinstance(x, bool)`); + out.push(` return float(x)`); + out.push(``); + out.push(``); + out.push(`def to_float(x: Any) -> float:`); + out.push(` assert isinstance(x, (float, int)) and not isinstance(x, bool)`); + out.push(` return float(x)`); + out.push(``); + out.push(``); + out.push(`def from_bool(x: Any) -> bool:`); + out.push(` assert isinstance(x, bool)`); + out.push(` return x`); + out.push(``); + out.push(``); + out.push(`def from_none(x: Any) -> Any:`); + out.push(` assert x is None`); + out.push(` return x`); + out.push(``); + out.push(``); + out.push(`def from_union(fs: list[Callable[[Any], T]], x: Any) -> T:`); + out.push(` for f in fs:`); + out.push(` try:`); + out.push(` return f(x)`); + out.push(` except Exception:`); + out.push(` pass`); + out.push(` assert False`); + out.push(``); + out.push(``); + out.push(`def from_list(f: Callable[[Any], T], x: Any) -> list[T]:`); + out.push(` assert isinstance(x, list)`); + out.push(` return [f(item) for item in x]`); + out.push(``); + out.push(``); + out.push(`def from_dict(f: Callable[[Any], T], x: Any) -> dict[str, T]:`); + out.push(` assert isinstance(x, dict)`); + out.push(` return {key: f(value) for key, value in x.items()}`); + out.push(``); + out.push(``); + out.push(`def from_datetime(x: Any) -> datetime:`); + out.push(` return dateutil.parser.parse(from_str(x))`); + out.push(``); + out.push(``); + out.push(`def to_datetime(x: datetime) -> str:`); + out.push(` return x.isoformat()`); + out.push(``); + out.push(``); + out.push(`def from_uuid(x: Any) -> UUID:`); + out.push(` return UUID(from_str(x))`); + out.push(``); + out.push(``); + out.push(`def to_uuid(x: UUID) -> str:`); + out.push(` return str(x)`); + out.push(``); + out.push(``); + out.push(`def parse_enum(c: type[EnumT], x: Any) -> EnumT:`); + out.push(` assert isinstance(x, str)`); + out.push(` return c(x)`); + out.push(``); + out.push(``); + out.push(`def to_class(c: type[T], x: Any) -> dict:`); + out.push(` assert isinstance(x, c)`); + out.push(` return cast(Any, x).to_dict()`); + out.push(``); + out.push(``); + out.push(`def to_enum(c: type[EnumT], x: Any) -> str:`); + out.push(` assert isinstance(x, c)`); + out.push(` return cast(str, x.value)`); + out.push(``); + out.push(``); + out.push(eventTypeLines.join("\n")); + out.push(``); + out.push(``); + out.push(`@dataclass`); + out.push(`class RawSessionEventData:`); + out.push(` raw: Any`); + out.push(``); + out.push(` @staticmethod`); + out.push(` def from_dict(obj: Any) -> "RawSessionEventData":`); + out.push(` return RawSessionEventData(obj)`); + out.push(``); + out.push(` def to_dict(self) -> Any:`); + out.push(` return self.raw`); + out.push(``); + out.push(``); + out.push(`def _compat_to_python_key(name: str) -> str:`); + out.push(` result: list[str] = []`); + out.push(` for index, char in enumerate(name.replace(".", "_")):`); + out.push( + ` if char.isupper() and index > 0 and (not name[index - 1].isupper() or (index + 1 < len(name) and name[index + 1].islower())):` + ); + out.push(` result.append("_")`); + out.push(` result.append(char.lower())`); + out.push(` return "".join(result)`); + out.push(``); + out.push(``); + out.push(`def _compat_to_json_key(name: str) -> str:`); + out.push(` parts = name.split("_")`); + out.push(` if not parts:`); + out.push(` return name`); + out.push(` return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:])`); + out.push(``); + out.push(``); + out.push(`def _compat_to_json_value(value: Any) -> Any:`); + out.push(` if hasattr(value, "to_dict"):`); + out.push(` return cast(Any, value).to_dict()`); + out.push(` if isinstance(value, Enum):`); + out.push(` return value.value`); + out.push(` if isinstance(value, datetime):`); + out.push(` return value.isoformat()`); + out.push(` if isinstance(value, UUID):`); + out.push(` return str(value)`); + out.push(` if isinstance(value, list):`); + out.push(` return [_compat_to_json_value(item) for item in value]`); + out.push(` if isinstance(value, dict):`); + out.push(` return {key: _compat_to_json_value(item) for key, item in value.items()}`); + out.push(` return value`); + out.push(``); + out.push(``); + out.push(`def _compat_from_json_value(value: Any) -> Any:`); + out.push(` return value`); + out.push(``); + out.push(``); + out.push(`class Data:`); + out.push(` """Backward-compatible shim for manually constructed event payloads."""`); + out.push(``); + out.push(` def __init__(self, **kwargs: Any):`); + out.push(` self._values = {key: _compat_from_json_value(value) for key, value in kwargs.items()}`); + out.push(` for key, value in self._values.items():`); + out.push(` setattr(self, key, value)`); + out.push(``); + out.push(` @staticmethod`); + out.push(` def from_dict(obj: Any) -> "Data":`); + out.push(` assert isinstance(obj, dict)`); + out.push( + ` return Data(**{_compat_to_python_key(key): _compat_from_json_value(value) for key, value in obj.items()})` + ); + out.push(``); + out.push(` def to_dict(self) -> dict:`); + out.push( + ` return {_compat_to_json_key(key): _compat_to_json_value(value) for key, value in self._values.items() if value is not None}` + ); + out.push(``); + out.push(``); + for (const classDef of ctx.classes) { + out.push(classDef); + out.push(``); + out.push(``); + } + for (const enumDef of ctx.enums) { + out.push(enumDef); + out.push(``); + out.push(``); + } + + const sessionEventDataTypes = [ + ...variants.map((variant) => variant.dataClassName), + "RawSessionEventData", + "Data", + ]; + out.push(`SessionEventData = ${sessionEventDataTypes.join(" | ")}`); + out.push(``); + out.push(``); + out.push(`@dataclass`); + out.push(`class SessionEvent:`); + out.push(` data: SessionEventData`); + out.push(` id: UUID`); + out.push(` timestamp: datetime`); + out.push(` type: SessionEventType`); + out.push(` ephemeral: bool | None = None`); + out.push(` parent_id: UUID | None = None`); + out.push(` raw_type: str | None = None`); + out.push(``); + out.push(` @staticmethod`); + out.push(` def from_dict(obj: Any) -> "SessionEvent":`); + out.push(` assert isinstance(obj, dict)`); + out.push(` raw_type = from_str(obj.get("type"))`); + out.push(` event_type = SessionEventType(raw_type)`); + out.push(` event_id = from_uuid(obj.get("id"))`); + out.push(` timestamp = from_datetime(obj.get("timestamp"))`); + out.push(` ephemeral = from_union([from_bool, from_none], obj.get("ephemeral"))`); + out.push(` parent_id = from_union([from_none, from_uuid], obj.get("parentId"))`); + out.push(` data_obj = obj.get("data")`); + out.push(` match event_type:`); + for (const variant of variants) { + out.push( + ` case SessionEventType.${toEnumMemberName(variant.typeName)}: data = ${variant.dataClassName}.from_dict(data_obj)` + ); + } + out.push(` case _: data = RawSessionEventData.from_dict(data_obj)`); + out.push(` return SessionEvent(`); + out.push(` data=data,`); + out.push(` id=event_id,`); + out.push(` timestamp=timestamp,`); + out.push(` type=event_type,`); + out.push(` ephemeral=ephemeral,`); + out.push(` parent_id=parent_id,`); + out.push(` raw_type=raw_type if event_type == SessionEventType.UNKNOWN else None,`); + out.push(` )`); + out.push(``); + out.push(` def to_dict(self) -> dict:`); + out.push(` result: dict = {}`); + out.push(` result["data"] = self.data.to_dict()`); + out.push(` result["id"] = to_uuid(self.id)`); + out.push(` result["timestamp"] = to_datetime(self.timestamp)`); + out.push( + ` result["type"] = self.raw_type if self.type == SessionEventType.UNKNOWN and self.raw_type is not None else to_enum(SessionEventType, self.type)` + ); + out.push(` if self.ephemeral is not None:`); + out.push(` result["ephemeral"] = from_bool(self.ephemeral)`); + out.push(` result["parentId"] = from_union([from_none, to_uuid], self.parent_id)`); + out.push(` return result`); + out.push(``); + out.push(``); + out.push(`def session_event_from_dict(s: Any) -> SessionEvent:`); + out.push(` return SessionEvent.from_dict(s)`); + out.push(``); + out.push(``); + out.push(`def session_event_to_dict(x: SessionEvent) -> Any:`); + out.push(` return x.to_dict()`); + out.push(``); + out.push(``); + out.push(`# Compatibility shims for pre-refactor top-level generated types.`); + out.push(`class RequestedSchemaType(str, Enum):`); + out.push(` OBJECT = "object"`); + out.push(``); + out.push(``); + out.push(`@dataclass`); + out.push(`class ErrorClass:`); + out.push(` """Backward-compatible shim for generic error payloads."""`); + out.push(` message: str`); + out.push(` code: str | None = None`); + out.push(` stack: str | None = None`); + out.push(``); + out.push(` @staticmethod`); + out.push(` def from_dict(obj: Any) -> "ErrorClass":`); + out.push(` assert isinstance(obj, dict)`); + out.push(` message = from_str(obj.get("message"))`); + out.push(` code = from_union([from_none, lambda x: from_str(x)], obj.get("code"))`); + out.push(` stack = from_union([from_none, lambda x: from_str(x)], obj.get("stack"))`); + out.push(` return ErrorClass(`); + out.push(` message=message,`); + out.push(` code=code,`); + out.push(` stack=stack,`); + out.push(` )`); + out.push(``); + out.push(` def to_dict(self) -> dict:`); + out.push(` result: dict = {}`); + out.push(` result["message"] = from_str(self.message)`); + out.push(` if self.code is not None:`); + out.push(` result["code"] = from_union([from_none, lambda x: from_str(x)], self.code)`); + out.push(` if self.stack is not None:`); + out.push(` result["stack"] = from_union([from_none, lambda x: from_str(x)], self.stack)`); + out.push(` return result`); + out.push(``); + out.push(``); + out.push(`@dataclass`); + out.push(`class Resource:`); + out.push(` """Backward-compatible shim for embedded tool result resources."""`); + out.push(` uri: str`); + out.push(` mime_type: str | None = None`); + out.push(` text: str | None = None`); + out.push(` blob: str | None = None`); + out.push(``); + out.push(` @staticmethod`); + out.push(` def from_dict(obj: Any) -> "Resource":`); + out.push(` assert isinstance(obj, dict)`); + out.push(` uri = from_str(obj.get("uri"))`); + out.push(` mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType"))`); + out.push(` text = from_union([from_none, lambda x: from_str(x)], obj.get("text"))`); + out.push(` blob = from_union([from_none, lambda x: from_str(x)], obj.get("blob"))`); + out.push(` return Resource(`); + out.push(` uri=uri,`); + out.push(` mime_type=mime_type,`); + out.push(` text=text,`); + out.push(` blob=blob,`); + out.push(` )`); + out.push(``); + out.push(` def to_dict(self) -> dict:`); + out.push(` result: dict = {}`); + out.push(` result["uri"] = from_str(self.uri)`); + out.push(` if self.mime_type is not None:`); + out.push(` result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type)`); + out.push(` if self.text is not None:`); + out.push(` result["text"] = from_union([from_none, lambda x: from_str(x)], self.text)`); + out.push(` if self.blob is not None:`); + out.push(` result["blob"] = from_union([from_none, lambda x: from_str(x)], self.blob)`); + out.push(` return result`); + out.push(``); + out.push(``); + out.push(`ContentType = ToolExecutionCompleteDataResultContentsItemType`); + out.push(`Theme = ToolExecutionCompleteDataResultContentsItemIconsItemTheme`); + out.push(`Icon = ToolExecutionCompleteDataResultContentsItemIconsItem`); + out.push(`ResultKind = PermissionCompletedDataResultKind`); + out.push(``); + out.push(``); + out.push(`@dataclass`); + out.push(`class ContentElement:`); + out.push(` """Backward-compatible shim for tool result content blocks."""`); + out.push(` type: ContentType`); + out.push(` text: str | None = None`); + out.push(` cwd: str | None = None`); + out.push(` exit_code: float | None = None`); + out.push(` data: str | None = None`); + out.push(` mime_type: str | None = None`); + out.push(` description: str | None = None`); + out.push(` icons: list[Icon] | None = None`); + out.push(` name: str | None = None`); + out.push(` size: float | None = None`); + out.push(` title: str | None = None`); + out.push(` uri: str | None = None`); + out.push(` resource: Resource | None = None`); + out.push(``); + out.push(` @staticmethod`); + out.push(` def from_dict(obj: Any) -> "ContentElement":`); + out.push(` assert isinstance(obj, dict)`); + out.push(` type = parse_enum(ContentType, obj.get("type"))`); + out.push(` text = from_union([from_none, lambda x: from_str(x)], obj.get("text"))`); + out.push(` cwd = from_union([from_none, lambda x: from_str(x)], obj.get("cwd"))`); + out.push(` exit_code = from_union([from_none, lambda x: from_float(x)], obj.get("exitCode"))`); + out.push(` data = from_union([from_none, lambda x: from_str(x)], obj.get("data"))`); + out.push(` mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType"))`); + out.push(` description = from_union([from_none, lambda x: from_str(x)], obj.get("description"))`); + out.push(` icons = from_union([from_none, lambda x: from_list(Icon.from_dict, x)], obj.get("icons"))`); + out.push(` name = from_union([from_none, lambda x: from_str(x)], obj.get("name"))`); + out.push(` size = from_union([from_none, lambda x: from_float(x)], obj.get("size"))`); + out.push(` title = from_union([from_none, lambda x: from_str(x)], obj.get("title"))`); + out.push(` uri = from_union([from_none, lambda x: from_str(x)], obj.get("uri"))`); + out.push(` resource = from_union([from_none, lambda x: Resource.from_dict(x)], obj.get("resource"))`); + out.push(` return ContentElement(`); + out.push(` type=type,`); + out.push(` text=text,`); + out.push(` cwd=cwd,`); + out.push(` exit_code=exit_code,`); + out.push(` data=data,`); + out.push(` mime_type=mime_type,`); + out.push(` description=description,`); + out.push(` icons=icons,`); + out.push(` name=name,`); + out.push(` size=size,`); + out.push(` title=title,`); + out.push(` uri=uri,`); + out.push(` resource=resource,`); + out.push(` )`); + out.push(``); + out.push(` def to_dict(self) -> dict:`); + out.push(` result: dict = {}`); + out.push(` result["type"] = to_enum(ContentType, self.type)`); + out.push(` if self.text is not None:`); + out.push(` result["text"] = from_union([from_none, lambda x: from_str(x)], self.text)`); + out.push(` if self.cwd is not None:`); + out.push(` result["cwd"] = from_union([from_none, lambda x: from_str(x)], self.cwd)`); + out.push(` if self.exit_code is not None:`); + out.push(` result["exitCode"] = from_union([from_none, lambda x: to_float(x)], self.exit_code)`); + out.push(` if self.data is not None:`); + out.push(` result["data"] = from_union([from_none, lambda x: from_str(x)], self.data)`); + out.push(` if self.mime_type is not None:`); + out.push(` result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type)`); + out.push(` if self.description is not None:`); + out.push(` result["description"] = from_union([from_none, lambda x: from_str(x)], self.description)`); + out.push(` if self.icons is not None:`); + out.push(` result["icons"] = from_union([from_none, lambda x: from_list(lambda x: to_class(Icon, x), x)], self.icons)`); + out.push(` if self.name is not None:`); + out.push(` result["name"] = from_union([from_none, lambda x: from_str(x)], self.name)`); + out.push(` if self.size is not None:`); + out.push(` result["size"] = from_union([from_none, lambda x: to_float(x)], self.size)`); + out.push(` if self.title is not None:`); + out.push(` result["title"] = from_union([from_none, lambda x: from_str(x)], self.title)`); + out.push(` if self.uri is not None:`); + out.push(` result["uri"] = from_union([from_none, lambda x: from_str(x)], self.uri)`); + out.push(` if self.resource is not None:`); + out.push(` result["resource"] = from_union([from_none, lambda x: to_class(Resource, x)], self.resource)`); + out.push(` return result`); + out.push(``); + out.push(``); + out.push(`@dataclass`); + out.push(`class Result:`); + out.push(` """Backward-compatible shim for generic result payloads."""`); + out.push(` content: str | None = None`); + out.push(` contents: list[ContentElement] | None = None`); + out.push(` detailed_content: str | None = None`); + out.push(` kind: ResultKind | None = None`); + out.push(``); + out.push(` @staticmethod`); + out.push(` def from_dict(obj: Any) -> "Result":`); + out.push(` assert isinstance(obj, dict)`); + out.push(` content = from_union([from_none, lambda x: from_str(x)], obj.get("content"))`); + out.push(` contents = from_union([from_none, lambda x: from_list(ContentElement.from_dict, x)], obj.get("contents"))`); + out.push(` detailed_content = from_union([from_none, lambda x: from_str(x)], obj.get("detailedContent"))`); + out.push(` kind = from_union([from_none, lambda x: parse_enum(ResultKind, x)], obj.get("kind"))`); + out.push(` return Result(`); + out.push(` content=content,`); + out.push(` contents=contents,`); + out.push(` detailed_content=detailed_content,`); + out.push(` kind=kind,`); + out.push(` )`); + out.push(``); + out.push(` def to_dict(self) -> dict:`); + out.push(` result: dict = {}`); + out.push(` if self.content is not None:`); + out.push(` result["content"] = from_union([from_none, lambda x: from_str(x)], self.content)`); + out.push(` if self.contents is not None:`); + out.push(` result["contents"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ContentElement, x), x)], self.contents)`); + out.push(` if self.detailed_content is not None:`); + out.push(` result["detailedContent"] = from_union([from_none, lambda x: from_str(x)], self.detailed_content)`); + out.push(` if self.kind is not None:`); + out.push(` result["kind"] = from_union([from_none, lambda x: to_enum(ResultKind, x)], self.kind)`); + out.push(` return result`); + out.push(``); + out.push(``); + out.push(`# Convenience aliases for commonly used nested event types.`); + out.push(`Action = ElicitationCompletedDataAction`); + out.push(`Agent = SessionCustomAgentsUpdatedDataAgentsItem`); + out.push(`AgentMode = UserMessageDataAgentMode`); + out.push(`Attachment = UserMessageDataAttachmentsItem`); + out.push(`AttachmentType = UserMessageDataAttachmentsItemType`); + out.push(`CodeChanges = SessionShutdownDataCodeChanges`); + out.push(`CompactionTokensUsed = SessionCompactionCompleteDataCompactionTokensUsed`); + out.push(`ContextClass = SessionStartDataContext`); + out.push(`CopilotUsage = AssistantUsageDataCopilotUsage`); + out.push(`DataCommand = CommandsChangedDataCommandsItem`); + out.push(`End = UserMessageDataAttachmentsItemSelectionEnd`); + out.push(`Extension = SessionExtensionsLoadedDataExtensionsItem`); + out.push(`ExtensionStatus = SessionExtensionsLoadedDataExtensionsItemStatus`); + out.push(`HostType = SessionStartDataContextHostType`); + out.push(`KindClass = SystemNotificationDataKind`); + out.push(`KindStatus = SystemNotificationDataKindStatus`); + out.push(`KindType = SystemNotificationDataKindType`); + out.push(`LineRange = UserMessageDataAttachmentsItemLineRange`); + out.push(`Metadata = SystemMessageDataMetadata`); + out.push(`Mode = ElicitationRequestedDataMode`); + out.push(`ModelMetric = SessionShutdownDataModelMetricsValue`); + out.push(`Operation = SessionPlanChangedDataOperation`); + out.push(`PermissionRequest = PermissionRequestedDataPermissionRequest`); + out.push(`PermissionRequestKind = PermissionRequestedDataPermissionRequestKind`); + out.push(`PermissionRequestCommand = PermissionRequestedDataPermissionRequestCommandsItem`); + out.push(`PossibleURL = PermissionRequestedDataPermissionRequestPossibleUrlsItem`); + out.push(`QuotaSnapshot = AssistantUsageDataQuotaSnapshotsValue`); + out.push(`ReferenceType = UserMessageDataAttachmentsItemReferenceType`); + out.push(`RepositoryClass = SessionHandoffDataRepository`); + out.push(`RequestedSchema = ElicitationRequestedDataRequestedSchema`); + out.push(`Requests = SessionShutdownDataModelMetricsValueRequests`); + out.push(`Role = SystemMessageDataRole`); + out.push(`Selection = UserMessageDataAttachmentsItemSelection`); + out.push(`Server = SessionMcpServersLoadedDataServersItem`); + out.push(`ServerStatus = SessionMcpServersLoadedDataServersItemStatus`); + out.push(`ShutdownType = SessionShutdownDataShutdownType`); + out.push(`Skill = SessionSkillsLoadedDataSkillsItem`); + out.push(`Source = SessionExtensionsLoadedDataExtensionsItemSource`); + out.push(`SourceType = SessionHandoffDataSourceType`); + out.push(`Start = UserMessageDataAttachmentsItemSelectionStart`); + out.push(`StaticClientConfig = McpOauthRequiredDataStaticClientConfig`); + out.push(`TokenDetail = AssistantUsageDataCopilotUsageTokenDetailsItem`); + out.push(`ToolRequest = AssistantMessageDataToolRequestsItem`); + out.push(`ToolRequestType = AssistantMessageDataToolRequestsItemType`); + out.push(`UI = CapabilitiesChangedDataUi`); + out.push(`Usage = SessionShutdownDataModelMetricsValueUsage`); + + return out.join("\n"); +} + +async function generateSessionEvents(schemaPath?: string): Promise { + console.log("Python: generating session-events..."); + + const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); + const schema = JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7; + const processed = postProcessSchema(schema); + const code = generatePythonSessionEventsCode(processed); - const outPath = await writeGeneratedFile("python/copilot/generated/session_events.py", banner + code); + const outPath = await writeGeneratedFile("python/copilot/generated/session_events.py", code); console.log(` ✓ ${outPath}`); } From 46b83ed9036472e6b9424e18d0e4f6cad890f012 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 12 Apr 2026 10:09:11 -0400 Subject: [PATCH 02/14] Address review and CI follow-ups Fix the generated Python docstring escaping that CodeQL flagged, correct dotted-key normalization in the Data compatibility shim, update the stale Go local-cli docs snippet for the newer typed event API, and apply the Python formatter change required by CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/setup/local-cli.md | 12 +- python/copilot/generated/session_events.py | 237 +++++++++++---------- python/test_event_forward_compatibility.py | 4 +- scripts/codegen/python.ts | 15 +- 4 files changed, 142 insertions(+), 126 deletions(-) diff --git a/docs/setup/local-cli.md b/docs/setup/local-cli.md index 845a20af5..b7f8de04c 100644 --- a/docs/setup/local-cli.md +++ b/docs/setup/local-cli.md @@ -99,8 +99,10 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) - if d, ok := response.Data.(*copilot.AssistantMessageData); ok { - fmt.Println(d.Content) + if response != nil { + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } } } ``` @@ -117,8 +119,10 @@ defer client.Stop() session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { - fmt.Println(d.Content) +if response != nil { + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } } ``` diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 4957d2df7..6682889ad 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -199,9 +199,10 @@ def to_dict(self) -> Any: def _compat_to_python_key(name: str) -> str: + normalized = name.replace(".", "_") result: list[str] = [] - for index, char in enumerate(name.replace(".", "_")): - if char.isupper() and index > 0 and (not name[index - 1].isupper() or (index + 1 < len(name) and name[index + 1].islower())): + for index, char in enumerate(normalized): + if char.isupper() and index > 0 and (not normalized[index - 1].isupper() or (index + 1 < len(normalized) and normalized[index + 1].islower())): result.append("_") result.append(char.lower()) return "".join(result) @@ -253,7 +254,7 @@ def to_dict(self) -> dict: @dataclass class SessionStartDataContext: - """Working directory and git context at session start""" + "Working directory and git context at session start" cwd: str git_root: str | None = None repository: str | None = None @@ -302,7 +303,7 @@ def to_dict(self) -> dict: @dataclass class SessionStartData: - """Session initialization metadata including context and configuration""" + "Session initialization metadata including context and configuration" session_id: str version: float producer: str @@ -362,7 +363,7 @@ def to_dict(self) -> dict: @dataclass class SessionResumeDataContext: - """Updated working directory and git context at resume time""" + "Updated working directory and git context at resume time" cwd: str git_root: str | None = None repository: str | None = None @@ -411,7 +412,7 @@ def to_dict(self) -> dict: @dataclass class SessionResumeData: - """Session resume metadata including current context and event count""" + "Session resume metadata including current context and event count" resume_time: datetime event_count: float selected_model: str | None = None @@ -459,7 +460,7 @@ def to_dict(self) -> dict: @dataclass class SessionRemoteSteerableChangedData: - """Notifies Mission Control that the session's remote steering capability has changed""" + "Notifies Mission Control that the session's remote steering capability has changed" remote_steerable: bool @staticmethod @@ -478,7 +479,7 @@ def to_dict(self) -> dict: @dataclass class SessionErrorData: - """Error details for timeline display including message and optional diagnostic information""" + "Error details for timeline display including message and optional diagnostic information" error_type: str message: str stack: str | None = None @@ -521,7 +522,7 @@ def to_dict(self) -> dict: @dataclass class SessionIdleData: - """Payload indicating the session is idle with no background agents in flight""" + "Payload indicating the session is idle with no background agents in flight" aborted: bool | None = None @staticmethod @@ -541,7 +542,7 @@ def to_dict(self) -> dict: @dataclass class SessionTitleChangedData: - """Session title change payload containing the new display title""" + "Session title change payload containing the new display title" title: str @staticmethod @@ -560,7 +561,7 @@ def to_dict(self) -> dict: @dataclass class SessionInfoData: - """Informational message for timeline display with categorization""" + "Informational message for timeline display with categorization" info_type: str message: str url: str | None = None @@ -588,7 +589,7 @@ def to_dict(self) -> dict: @dataclass class SessionWarningData: - """Warning message for timeline display with categorization""" + "Warning message for timeline display with categorization" warning_type: str message: str url: str | None = None @@ -616,7 +617,7 @@ def to_dict(self) -> dict: @dataclass class SessionModelChangeData: - """Model change details including previous and new model identifiers""" + "Model change details including previous and new model identifiers" new_model: str previous_model: str | None = None previous_reasoning_effort: str | None = None @@ -650,7 +651,7 @@ def to_dict(self) -> dict: @dataclass class SessionModeChangedData: - """Agent mode change details including previous and new modes""" + "Agent mode change details including previous and new modes" previous_mode: str new_mode: str @@ -673,7 +674,7 @@ def to_dict(self) -> dict: @dataclass class SessionPlanChangedData: - """Plan file operation details indicating what changed""" + "Plan file operation details indicating what changed" operation: SessionPlanChangedDataOperation @staticmethod @@ -692,7 +693,7 @@ def to_dict(self) -> dict: @dataclass class SessionWorkspaceFileChangedData: - """Workspace file change details including path and operation type""" + "Workspace file change details including path and operation type" path: str operation: SessionWorkspaceFileChangedDataOperation @@ -946,7 +947,7 @@ def to_dict(self) -> dict: @dataclass class SessionHandoffDataRepository: - """Repository context for the handed-off session""" + "Repository context for the handed-off session" owner: str name: str branch: str | None = None @@ -974,7 +975,7 @@ def to_dict(self) -> dict: @dataclass class SessionHandoffData: - """Session handoff metadata including source, context, and repository information""" + "Session handoff metadata including source, context, and repository information" handoff_time: datetime source_type: SessionHandoffDataSourceType repository: SessionHandoffDataRepository | None = None @@ -1022,7 +1023,7 @@ def to_dict(self) -> dict: @dataclass class SessionTruncationData: - """Conversation truncation statistics including token counts and removed content metrics""" + "Conversation truncation statistics including token counts and removed content metrics" token_limit: float pre_truncation_tokens_in_messages: float pre_truncation_messages_length: float @@ -1069,7 +1070,7 @@ def to_dict(self) -> dict: @dataclass class SessionSnapshotRewindData: - """Session rewind details including target event and count of removed events""" + "Session rewind details including target event and count of removed events" up_to_event_id: str events_removed: float @@ -1092,7 +1093,7 @@ def to_dict(self) -> dict: @dataclass class SessionShutdownDataCodeChanges: - """Aggregate code change metrics for the session""" + "Aggregate code change metrics for the session" lines_added: float lines_removed: float files_modified: list[str] @@ -1119,7 +1120,7 @@ def to_dict(self) -> dict: @dataclass class SessionShutdownDataModelMetricsValueRequests: - """Request count and cost metrics""" + "Request count and cost metrics" count: float cost: float @@ -1142,7 +1143,7 @@ def to_dict(self) -> dict: @dataclass class SessionShutdownDataModelMetricsValueUsage: - """Token usage breakdown""" + "Token usage breakdown" input_tokens: float output_tokens: float cache_read_tokens: float @@ -1195,7 +1196,7 @@ def to_dict(self) -> dict: @dataclass class SessionShutdownData: - """Session termination metrics including usage statistics, code changes, and shutdown reason""" + "Session termination metrics including usage statistics, code changes, and shutdown reason" shutdown_type: SessionShutdownDataShutdownType total_premium_requests: float total_api_duration_ms: float @@ -1264,7 +1265,7 @@ def to_dict(self) -> dict: @dataclass class SessionContextChangedData: - """Updated working directory and git context after the change""" + "Updated working directory and git context after the change" cwd: str git_root: str | None = None repository: str | None = None @@ -1313,7 +1314,7 @@ def to_dict(self) -> dict: @dataclass class SessionUsageInfoData: - """Current context window usage statistics including token and message counts""" + "Current context window usage statistics including token and message counts" token_limit: float current_tokens: float messages_length: float @@ -1360,7 +1361,7 @@ def to_dict(self) -> dict: @dataclass class SessionCompactionStartData: - """Context window breakdown at the start of LLM-powered conversation compaction""" + "Context window breakdown at the start of LLM-powered conversation compaction" system_tokens: float | None = None conversation_tokens: float | None = None tool_definitions_tokens: float | None = None @@ -1390,7 +1391,7 @@ def to_dict(self) -> dict: @dataclass class SessionCompactionCompleteDataCompactionTokensUsed: - """Token usage breakdown for the compaction LLM call""" + "Token usage breakdown for the compaction LLM call" input: float output: float cached_input: float @@ -1417,7 +1418,7 @@ def to_dict(self) -> dict: @dataclass class SessionCompactionCompleteData: - """Conversation compaction results including success status, metrics, and optional error details""" + "Conversation compaction results including success status, metrics, and optional error details" success: bool error: str | None = None pre_compaction_tokens: float | None = None @@ -1506,7 +1507,7 @@ def to_dict(self) -> dict: @dataclass class SessionTaskCompleteData: - """Task completion notification with summary from the agent""" + "Task completion notification with summary from the agent" summary: str | None = None success: bool | None = None @@ -1531,7 +1532,7 @@ def to_dict(self) -> dict: @dataclass class UserMessageDataAttachmentsItemLineRange: - """Optional line range to scope the attachment to a specific section of the file""" + "Optional line range to scope the attachment to a specific section of the file" start: float end: float @@ -1554,7 +1555,7 @@ def to_dict(self) -> dict: @dataclass class UserMessageDataAttachmentsItemSelectionStart: - """Start position of the selection""" + "Start position of the selection" line: float character: float @@ -1577,7 +1578,7 @@ def to_dict(self) -> dict: @dataclass class UserMessageDataAttachmentsItemSelectionEnd: - """End position of the selection""" + "End position of the selection" line: float character: float @@ -1600,7 +1601,7 @@ def to_dict(self) -> dict: @dataclass class UserMessageDataAttachmentsItemSelection: - """Position range of the selection within the file""" + "Position range of the selection within the file" start: UserMessageDataAttachmentsItemSelectionStart end: UserMessageDataAttachmentsItemSelectionEnd @@ -1623,7 +1624,7 @@ def to_dict(self) -> dict: @dataclass class UserMessageDataAttachmentsItem: - """A user message attachment — a file, directory, code selection, blob, or GitHub reference""" + "A user message attachment — a file, directory, code selection, blob, or GitHub reference" type: UserMessageDataAttachmentsItemType path: str | None = None display_name: str | None = None @@ -1750,7 +1751,7 @@ def to_dict(self) -> dict: @dataclass class PendingMessagesModifiedData: - """Empty payload; the event signals that the pending message queue has changed""" + "Empty payload; the event signals that the pending message queue has changed" @staticmethod def from_dict(obj: Any) -> "PendingMessagesModifiedData": assert isinstance(obj, dict) @@ -1762,7 +1763,7 @@ def to_dict(self) -> dict: @dataclass class AssistantTurnStartData: - """Turn initialization metadata including identifier and interaction tracking""" + "Turn initialization metadata including identifier and interaction tracking" turn_id: str interaction_id: str | None = None @@ -1786,7 +1787,7 @@ def to_dict(self) -> dict: @dataclass class AssistantIntentData: - """Agent intent description for current activity or plan""" + "Agent intent description for current activity or plan" intent: str @staticmethod @@ -1805,7 +1806,7 @@ def to_dict(self) -> dict: @dataclass class AssistantReasoningData: - """Assistant reasoning content for timeline display with complete thinking text""" + "Assistant reasoning content for timeline display with complete thinking text" reasoning_id: str content: str @@ -1828,7 +1829,7 @@ def to_dict(self) -> dict: @dataclass class AssistantReasoningDeltaData: - """Streaming reasoning delta for incremental extended thinking updates""" + "Streaming reasoning delta for incremental extended thinking updates" reasoning_id: str delta_content: str @@ -1851,7 +1852,7 @@ def to_dict(self) -> dict: @dataclass class AssistantStreamingDeltaData: - """Streaming response progress with cumulative byte count""" + "Streaming response progress with cumulative byte count" total_response_size_bytes: float @staticmethod @@ -1870,7 +1871,7 @@ def to_dict(self) -> dict: @dataclass class AssistantMessageDataToolRequestsItem: - """A tool invocation request from the assistant""" + "A tool invocation request from the assistant" tool_call_id: str name: str arguments: Any = None @@ -1918,7 +1919,7 @@ def to_dict(self) -> dict: @dataclass class AssistantMessageData: - """Assistant response containing text content, optional tool requests, and interaction metadata""" + "Assistant response containing text content, optional tool requests, and interaction metadata" message_id: str content: str tool_requests: list[AssistantMessageDataToolRequestsItem] | None = None @@ -1986,7 +1987,7 @@ def to_dict(self) -> dict: @dataclass class AssistantMessageDeltaData: - """Streaming assistant message delta for incremental response updates""" + "Streaming assistant message delta for incremental response updates" message_id: str delta_content: str parent_tool_call_id: str | None = None @@ -2014,7 +2015,7 @@ def to_dict(self) -> dict: @dataclass class AssistantTurnEndData: - """Turn completion metadata including the turn identifier""" + "Turn completion metadata including the turn identifier" turn_id: str @staticmethod @@ -2080,7 +2081,7 @@ def to_dict(self) -> dict: @dataclass class AssistantUsageDataCopilotUsageTokenDetailsItem: - """Token usage detail for a single billing category""" + "Token usage detail for a single billing category" batch_size: float cost_per_batch: float token_count: float @@ -2111,7 +2112,7 @@ def to_dict(self) -> dict: @dataclass class AssistantUsageDataCopilotUsage: - """Per-request cost and usage data from the CAPI copilot_usage response field""" + "Per-request cost and usage data from the CAPI copilot_usage response field" token_details: list[AssistantUsageDataCopilotUsageTokenDetailsItem] total_nano_aiu: float @@ -2134,7 +2135,7 @@ def to_dict(self) -> dict: @dataclass class AssistantUsageData: - """LLM API call usage metrics including tokens, costs, quotas, and billing information""" + "LLM API call usage metrics including tokens, costs, quotas, and billing information" model: str input_tokens: float | None = None output_tokens: float | None = None @@ -2228,7 +2229,7 @@ def to_dict(self) -> dict: @dataclass class AbortData: - """Turn abort information including the reason for termination""" + "Turn abort information including the reason for termination" reason: str @staticmethod @@ -2247,7 +2248,7 @@ def to_dict(self) -> dict: @dataclass class ToolUserRequestedData: - """User-initiated tool invocation request with tool name and arguments""" + "User-initiated tool invocation request with tool name and arguments" tool_call_id: str tool_name: str arguments: Any = None @@ -2275,7 +2276,7 @@ def to_dict(self) -> dict: @dataclass class ToolExecutionStartData: - """Tool execution startup details including MCP server information when applicable""" + "Tool execution startup details including MCP server information when applicable" tool_call_id: str tool_name: str arguments: Any = None @@ -2318,7 +2319,7 @@ def to_dict(self) -> dict: @dataclass class ToolExecutionPartialResultData: - """Streaming tool execution output for incremental result display""" + "Streaming tool execution output for incremental result display" tool_call_id: str partial_output: str @@ -2341,7 +2342,7 @@ def to_dict(self) -> dict: @dataclass class ToolExecutionProgressData: - """Tool execution progress notification with status message""" + "Tool execution progress notification with status message" tool_call_id: str progress_message: str @@ -2364,7 +2365,7 @@ def to_dict(self) -> dict: @dataclass class ToolExecutionCompleteDataResultContentsItemIconsItem: - """Icon image for a resource""" + "Icon image for a resource" src: str mime_type: str | None = None sizes: list[str] | None = None @@ -2398,7 +2399,7 @@ def to_dict(self) -> dict: @dataclass class ToolExecutionCompleteDataResultContentsItem: - """A content block within a tool result, which may be text, terminal output, image, audio, or a resource""" + "A content block within a tool result, which may be text, terminal output, image, audio, or a resource" type: ToolExecutionCompleteDataResultContentsItemType text: str | None = None exit_code: float | None = None @@ -2477,7 +2478,7 @@ def to_dict(self) -> dict: @dataclass class ToolExecutionCompleteDataResult: - """Tool execution result on success""" + "Tool execution result on success" content: str detailed_content: str | None = None contents: list[ToolExecutionCompleteDataResultContentsItem] | None = None @@ -2506,7 +2507,7 @@ def to_dict(self) -> dict: @dataclass class ToolExecutionCompleteDataError: - """Error details when the tool execution failed""" + "Error details when the tool execution failed" message: str code: str | None = None @@ -2530,7 +2531,7 @@ def to_dict(self) -> dict: @dataclass class ToolExecutionCompleteData: - """Tool execution completion results including success status, detailed output, and error information""" + "Tool execution completion results including success status, detailed output, and error information" tool_call_id: str success: bool model: str | None = None @@ -2588,7 +2589,7 @@ def to_dict(self) -> dict: @dataclass class SkillInvokedData: - """Skill invocation details including content, allowed tools, and plugin metadata""" + "Skill invocation details including content, allowed tools, and plugin metadata" name: str path: str content: str @@ -2635,7 +2636,7 @@ def to_dict(self) -> dict: @dataclass class SubagentStartedData: - """Sub-agent startup details including parent tool call and agent information""" + "Sub-agent startup details including parent tool call and agent information" tool_call_id: str agent_name: str agent_display_name: str @@ -2666,7 +2667,7 @@ def to_dict(self) -> dict: @dataclass class SubagentCompletedData: - """Sub-agent completion details for successful execution""" + "Sub-agent completion details for successful execution" tool_call_id: str agent_name: str agent_display_name: str @@ -2713,7 +2714,7 @@ def to_dict(self) -> dict: @dataclass class SubagentFailedData: - """Sub-agent failure details including error message and agent information""" + "Sub-agent failure details including error message and agent information" tool_call_id: str agent_name: str agent_display_name: str @@ -2764,7 +2765,7 @@ def to_dict(self) -> dict: @dataclass class SubagentSelectedData: - """Custom agent selection details including name and available tools""" + "Custom agent selection details including name and available tools" agent_name: str agent_display_name: str tools: list[str] | None @@ -2791,7 +2792,7 @@ def to_dict(self) -> dict: @dataclass class SubagentDeselectedData: - """Empty payload; the event signals that the custom agent was deselected, returning to the default agent""" + "Empty payload; the event signals that the custom agent was deselected, returning to the default agent" @staticmethod def from_dict(obj: Any) -> "SubagentDeselectedData": assert isinstance(obj, dict) @@ -2803,7 +2804,7 @@ def to_dict(self) -> dict: @dataclass class HookStartData: - """Hook invocation start details including type and input data""" + "Hook invocation start details including type and input data" hook_invocation_id: str hook_type: str input: Any = None @@ -2831,7 +2832,7 @@ def to_dict(self) -> dict: @dataclass class HookEndDataError: - """Error details when the hook failed""" + "Error details when the hook failed" message: str stack: str | None = None @@ -2855,7 +2856,7 @@ def to_dict(self) -> dict: @dataclass class HookEndData: - """Hook invocation completion details including output, success status, and error information""" + "Hook invocation completion details including output, success status, and error information" hook_invocation_id: str hook_type: str success: bool @@ -2892,7 +2893,7 @@ def to_dict(self) -> dict: @dataclass class SystemMessageDataMetadata: - """Metadata about the prompt template and its construction""" + "Metadata about the prompt template and its construction" prompt_version: str | None = None variables: dict[str, Any] | None = None @@ -2917,7 +2918,7 @@ def to_dict(self) -> dict: @dataclass class SystemMessageData: - """System or developer message content with role and optional template metadata""" + "System or developer message content with role and optional template metadata" content: str role: SystemMessageDataRole name: str | None = None @@ -2950,7 +2951,7 @@ def to_dict(self) -> dict: @dataclass class SystemNotificationDataKind: - """Structured metadata identifying what triggered this notification""" + "Structured metadata identifying what triggered this notification" type: SystemNotificationDataKindType agent_id: str | None = None agent_type: str | None = None @@ -3004,7 +3005,7 @@ def to_dict(self) -> dict: @dataclass class SystemNotificationData: - """System-generated notification for runtime events like background task completion""" + "System-generated notification for runtime events like background task completion" content: str kind: SystemNotificationDataKind @@ -3067,7 +3068,7 @@ def to_dict(self) -> dict: @dataclass class PermissionRequestedDataPermissionRequest: - """Details of the permission being requested""" + "Details of the permission being requested" kind: PermissionRequestedDataPermissionRequestKind tool_call_id: str | None = None full_command_text: str | None = None @@ -3226,7 +3227,7 @@ def to_dict(self) -> dict: @dataclass class PermissionRequestedData: - """Permission request notification requiring client approval with request details""" + "Permission request notification requiring client approval with request details" request_id: str permission_request: PermissionRequestedDataPermissionRequest resolved_by_hook: bool | None = None @@ -3254,7 +3255,7 @@ def to_dict(self) -> dict: @dataclass class PermissionCompletedDataResult: - """The result of the permission request""" + "The result of the permission request" kind: PermissionCompletedDataResultKind @staticmethod @@ -3273,7 +3274,7 @@ def to_dict(self) -> dict: @dataclass class PermissionCompletedData: - """Permission request completion notification signaling UI dismissal""" + "Permission request completion notification signaling UI dismissal" request_id: str result: PermissionCompletedDataResult @@ -3296,7 +3297,7 @@ def to_dict(self) -> dict: @dataclass class UserInputRequestedData: - """User input request notification with question and optional predefined choices""" + "User input request notification with question and optional predefined choices" request_id: str question: str choices: list[str] | None = None @@ -3334,7 +3335,7 @@ def to_dict(self) -> dict: @dataclass class UserInputCompletedData: - """User input request completion with the user's response""" + "User input request completion with the user's response" request_id: str answer: str | None = None was_freeform: bool | None = None @@ -3363,7 +3364,7 @@ def to_dict(self) -> dict: @dataclass class ElicitationRequestedDataRequestedSchema: - """JSON Schema describing the form fields to present to the user (form mode only)""" + "JSON Schema describing the form fields to present to the user (form mode only)" type: str properties: dict[str, Any] required: list[str] | None = None @@ -3391,7 +3392,7 @@ def to_dict(self) -> dict: @dataclass class ElicitationRequestedData: - """Elicitation request; may be form-based (structured input) or URL-based (browser redirect)""" + "Elicitation request; may be form-based (structured input) or URL-based (browser redirect)" request_id: str message: str tool_call_id: str | None = None @@ -3439,7 +3440,7 @@ def to_dict(self) -> dict: @dataclass class ElicitationCompletedData: - """Elicitation request completion with the user's response""" + "Elicitation request completion with the user's response" request_id: str action: ElicitationCompletedDataAction | None = None content: dict[str, Any] | None = None @@ -3468,7 +3469,7 @@ def to_dict(self) -> dict: @dataclass class SamplingRequestedData: - """Sampling request from an MCP server; contains the server name and a requestId for correlation""" + "Sampling request from an MCP server; contains the server name and a requestId for correlation" request_id: str server_name: str mcp_request_id: Any @@ -3495,7 +3496,7 @@ def to_dict(self) -> dict: @dataclass class SamplingCompletedData: - """Sampling request completion notification signaling UI dismissal""" + "Sampling request completion notification signaling UI dismissal" request_id: str @staticmethod @@ -3514,7 +3515,7 @@ def to_dict(self) -> dict: @dataclass class McpOauthRequiredDataStaticClientConfig: - """Static OAuth client configuration, if the server specifies one""" + "Static OAuth client configuration, if the server specifies one" client_id: str public_client: bool | None = None @@ -3538,7 +3539,7 @@ def to_dict(self) -> dict: @dataclass class McpOauthRequiredData: - """OAuth authentication request for an MCP server""" + "OAuth authentication request for an MCP server" request_id: str server_name: str server_url: str @@ -3570,7 +3571,7 @@ def to_dict(self) -> dict: @dataclass class McpOauthCompletedData: - """MCP OAuth request completion notification""" + "MCP OAuth request completion notification" request_id: str @staticmethod @@ -3589,7 +3590,7 @@ def to_dict(self) -> dict: @dataclass class ExternalToolRequestedData: - """External tool invocation request for client-side tool execution""" + "External tool invocation request for client-side tool execution" request_id: str session_id: str tool_call_id: str @@ -3635,7 +3636,7 @@ def to_dict(self) -> dict: @dataclass class ExternalToolCompletedData: - """External tool completion notification signaling UI dismissal""" + "External tool completion notification signaling UI dismissal" request_id: str @staticmethod @@ -3654,7 +3655,7 @@ def to_dict(self) -> dict: @dataclass class CommandQueuedData: - """Queued slash command dispatch request for client execution""" + "Queued slash command dispatch request for client execution" request_id: str command: str @@ -3677,7 +3678,7 @@ def to_dict(self) -> dict: @dataclass class CommandExecuteData: - """Registered command dispatch request routed to the owning client""" + "Registered command dispatch request routed to the owning client" request_id: str command: str command_name: str @@ -3708,7 +3709,7 @@ def to_dict(self) -> dict: @dataclass class CommandCompletedData: - """Queued command completion notification signaling UI dismissal""" + "Queued command completion notification signaling UI dismissal" request_id: str @staticmethod @@ -3750,7 +3751,7 @@ def to_dict(self) -> dict: @dataclass class CommandsChangedData: - """SDK command registration change notification""" + "SDK command registration change notification" commands: list[CommandsChangedDataCommandsItem] @staticmethod @@ -3769,7 +3770,7 @@ def to_dict(self) -> dict: @dataclass class CapabilitiesChangedDataUi: - """UI capability changes""" + "UI capability changes" elicitation: bool | None = None @staticmethod @@ -3789,7 +3790,7 @@ def to_dict(self) -> dict: @dataclass class CapabilitiesChangedData: - """Session capability change notification""" + "Session capability change notification" ui: CapabilitiesChangedDataUi | None = None @staticmethod @@ -3809,7 +3810,7 @@ def to_dict(self) -> dict: @dataclass class ExitPlanModeRequestedData: - """Plan approval request with plan content and available user actions""" + "Plan approval request with plan content and available user actions" request_id: str summary: str plan_content: str @@ -3844,7 +3845,7 @@ def to_dict(self) -> dict: @dataclass class ExitPlanModeCompletedData: - """Plan mode exit completion with the user's approval decision and optional feedback""" + "Plan mode exit completion with the user's approval decision and optional feedback" request_id: str approved: bool | None = None selected_action: str | None = None @@ -4161,20 +4162,20 @@ def to_dict(self) -> dict: class SessionStartDataContextHostType(Enum): - """Hosting platform type of the repository (github or ado)""" + "Hosting platform type of the repository (github or ado)" GITHUB = "github" ADO = "ado" class SessionPlanChangedDataOperation(Enum): - """The type of operation performed on the plan file""" + "The type of operation performed on the plan file" CREATE = "create" UPDATE = "update" DELETE = "delete" class SessionWorkspaceFileChangedDataOperation(Enum): - """Whether the file was newly created or updated""" + "Whether the file was newly created or updated" CREATE = "create" UPDATE = "update" @@ -4216,19 +4217,19 @@ class SessionImportLegacyDataLegacySessionSelectedModel(Enum): class SessionHandoffDataSourceType(Enum): - """Origin type of the session being handed off""" + "Origin type of the session being handed off" REMOTE = "remote" LOCAL = "local" class SessionShutdownDataShutdownType(Enum): - """Whether the session ended normally (\"routine\") or due to a crash/fatal error (\"error\")""" + "Whether the session ended normally (\"routine\") or due to a crash/fatal error (\"error\")" ROUTINE = "routine" ERROR = "error" class UserMessageDataAttachmentsItemType(Enum): - """A user message attachment — a file, directory, code selection, blob, or GitHub reference discriminator""" + "A user message attachment — a file, directory, code selection, blob, or GitHub reference discriminator" FILE = "file" DIRECTORY = "directory" SELECTION = "selection" @@ -4237,22 +4238,26 @@ class UserMessageDataAttachmentsItemType(Enum): class UserMessageDataAttachmentsItemReferenceType(Enum): - """Type of GitHub reference""" + "Type of GitHub reference" ISSUE = "issue" PR = "pr" DISCUSSION = "discussion" class UserMessageDataAgentMode(Enum): - """The agent mode that was active when this message was sent""" + "The agent mode that was active when this message was sent" INTERACTIVE = "interactive" PLAN = "plan" AUTOPILOT = "autopilot" SHELL = "shell" +class AssistantMessageDataToolRequestsItemType(Enum): + "Tool call type: \"function\" for standard tool calls, \"custom\" for grammar-based tool calls. Defaults to \"function\" when absent." + FUNCTION = "function" + CUSTOM = "custom" class ToolExecutionCompleteDataResultContentsItemType(Enum): - """A content block within a tool result, which may be text, terminal output, image, audio, or a resource discriminator""" + "A content block within a tool result, which may be text, terminal output, image, audio, or a resource discriminator" TEXT = "text" TERMINAL = "terminal" IMAGE = "image" @@ -4262,19 +4267,19 @@ class ToolExecutionCompleteDataResultContentsItemType(Enum): class ToolExecutionCompleteDataResultContentsItemIconsItemTheme(Enum): - """Theme variant this icon is intended for""" + "Theme variant this icon is intended for" LIGHT = "light" DARK = "dark" class SystemMessageDataRole(Enum): - """Message role: \"system\" for system prompts, \"developer\" for developer-injected instructions""" + "Message role: \"system\" for system prompts, \"developer\" for developer-injected instructions" SYSTEM = "system" DEVELOPER = "developer" class SystemNotificationDataKindType(Enum): - """Structured metadata identifying what triggered this notification discriminator""" + "Structured metadata identifying what triggered this notification discriminator" AGENT_COMPLETED = "agent_completed" AGENT_IDLE = "agent_idle" SHELL_COMPLETED = "shell_completed" @@ -4282,13 +4287,13 @@ class SystemNotificationDataKindType(Enum): class SystemNotificationDataKindStatus(Enum): - """Whether the agent completed successfully or failed""" + "Whether the agent completed successfully or failed" COMPLETED = "completed" FAILED = "failed" class PermissionRequestedDataPermissionRequestKind(Enum): - """Details of the permission being requested discriminator""" + "Details of the permission being requested discriminator" SHELL = "shell" WRITE = "write" READ = "read" @@ -4312,7 +4317,7 @@ class PermissionRequestedDataPermissionRequestDirection(Enum): class PermissionCompletedDataResultKind(Enum): - """The outcome of the permission request""" + "The outcome of the permission request" APPROVED = "approved" DENIED_BY_RULES = "denied-by-rules" DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user" @@ -4322,20 +4327,20 @@ class PermissionCompletedDataResultKind(Enum): class ElicitationRequestedDataMode(Enum): - """Elicitation mode; \"form\" for structured input, \"url\" for browser-based. Defaults to \"form\" when absent.""" + "Elicitation mode; \"form\" for structured input, \"url\" for browser-based. Defaults to \"form\" when absent." FORM = "form" URL = "url" class ElicitationCompletedDataAction(Enum): - """The user action: \"accept\" (submitted form), \"decline\" (explicitly refused), or \"cancel\" (dismissed)""" + "The user action: \"accept\" (submitted form), \"decline\" (explicitly refused), or \"cancel\" (dismissed)" ACCEPT = "accept" DECLINE = "decline" CANCEL = "cancel" class SessionMcpServersLoadedDataServersItemStatus(Enum): - """Connection status: connected, failed, needs-auth, pending, disabled, or not_configured""" + "Connection status: connected, failed, needs-auth, pending, disabled, or not_configured" CONNECTED = "connected" FAILED = "failed" NEEDS_AUTH = "needs-auth" @@ -4345,13 +4350,13 @@ class SessionMcpServersLoadedDataServersItemStatus(Enum): class SessionExtensionsLoadedDataExtensionsItemSource(Enum): - """Discovery source""" + "Discovery source" PROJECT = "project" USER = "user" class SessionExtensionsLoadedDataExtensionsItemStatus(Enum): - """Current status: running, disabled, failed, or starting""" + "Current status: running, disabled, failed, or starting" RUNNING = "running" DISABLED = "disabled" FAILED = "failed" @@ -4722,4 +4727,4 @@ def to_dict(self) -> dict: ToolRequest = AssistantMessageDataToolRequestsItem ToolRequestType = AssistantMessageDataToolRequestsItemType UI = CapabilitiesChangedDataUi -Usage = SessionShutdownDataModelMetricsValueUsage \ No newline at end of file +Usage = SessionShutdownDataModelMetricsValueUsage diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index c8ceecb26..f0a4bedef 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -84,7 +84,9 @@ def test_legacy_top_level_generated_symbols_remain_available(self): assert Mode.FORM.value == "form" assert ReferenceType.PR.value == "pr" - schema = RequestedSchema(properties={"answer": {"type": "string"}}, type=RequestedSchemaType.OBJECT) + schema = RequestedSchema( + properties={"answer": {"type": "string"}}, type=RequestedSchemaType.OBJECT + ) assert schema.to_dict()["type"] == "object" result = Result( diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 5b3825d56..99ba86852 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -78,6 +78,10 @@ function splitTopLevelCommas(s: string): string[] { return parts; } +function pyDocstringLiteral(text: string): string { + return JSON.stringify(text); +} + function modernizePython(code: string): string { // Replace Optional[X] with X | None (handles arbitrarily nested brackets) code = replaceBalancedBrackets(code, "Optional", (inner) => `${inner} | None`); @@ -364,7 +368,7 @@ function getOrCreatePyEnum( const lines: string[] = []; if (description) { lines.push(`class ${enumName}(Enum):`); - lines.push(` """${description.replace(/"/g, '\\"')}"""`); + lines.push(` ${pyDocstringLiteral(description)}`); } else { lines.push(`class ${enumName}(Enum):`); } @@ -637,7 +641,7 @@ function emitPyClass( lines.push(`@dataclass`); lines.push(`class ${typeName}:`); if (description || schema.description) { - lines.push(` """${(description || schema.description || "").replace(/"/g, '\\"')}"""`); + lines.push(` ${pyDocstringLiteral(description || schema.description || "")}`); } if (fieldInfos.length === 0) { @@ -781,7 +785,7 @@ function emitPyFlatDiscriminatedUnion( lines.push(`@dataclass`); lines.push(`class ${typeName}:`); if (description) { - lines.push(` """${description.replace(/"/g, '\\"')}"""`); + lines.push(` ${pyDocstringLiteral(description)}`); } for (const field of fieldInfos) { const suffix = field.isRequired ? "" : " = None"; @@ -964,10 +968,11 @@ function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(``); out.push(``); out.push(`def _compat_to_python_key(name: str) -> str:`); + out.push(` normalized = name.replace(".", "_")`); out.push(` result: list[str] = []`); - out.push(` for index, char in enumerate(name.replace(".", "_")):`); + out.push(` for index, char in enumerate(normalized):`); out.push( - ` if char.isupper() and index > 0 and (not name[index - 1].isupper() or (index + 1 < len(name) and name[index + 1].islower())):` + ` if char.isupper() and index > 0 and (not normalized[index - 1].isupper() or (index + 1 < len(normalized) and normalized[index + 1].islower())):` ); out.push(` result.append("_")`); out.push(` result.append(char.lower())`); From 96de885b6c3fbbb969a3596d0b70c59f59c6305b Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 12 Apr 2026 13:43:06 -0400 Subject: [PATCH 03/14] Clarify typed Python event examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/README.md | 27 ++++++++++++++++++++------- python/copilot/session.py | 20 +++++++++++++------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/python/README.md b/python/README.md index a023c6102..b303ab9bc 100644 --- a/python/README.md +++ b/python/README.md @@ -25,8 +25,10 @@ python chat.py ```python import asyncio +from typing import cast + from copilot import CopilotClient -from copilot.session import PermissionHandler +from copilot.generated.session_events import AssistantMessageData async def main(): # Client automatically starts on enter and cleans up on exit @@ -38,7 +40,7 @@ async def main(): def on_event(event): if event.type.value == "assistant.message": - print(event.data.content) + print(cast(AssistantMessageData, event.data).content) elif event.type.value == "session.idle": done.set() @@ -57,7 +59,10 @@ If you need more control over the lifecycle, you can call `start()`, `stop()`, a ```python import asyncio +from typing import cast + from copilot import CopilotClient +from copilot.generated.session_events import AssistantMessageData from copilot.session import PermissionHandler async def main(): @@ -74,7 +79,7 @@ async def main(): def on_event(event): if event.type.value == "assistant.message": - print(event.data.content) + print(cast(AssistantMessageData, event.data).content) elif event.type.value == "session.idle": done.set() @@ -333,7 +338,15 @@ Enable streaming to receive assistant response chunks as they're generated: ```python import asyncio +from typing import cast + from copilot import CopilotClient +from copilot.generated.session_events import ( + AssistantMessageData, + AssistantMessageDeltaData, + AssistantReasoningData, + AssistantReasoningDeltaData, +) from copilot.session import PermissionHandler async def main(): @@ -350,20 +363,20 @@ async def main(): match event.type.value: case "assistant.message_delta": # Streaming message chunk - print incrementally - delta = event.data.delta_content or "" + delta = cast(AssistantMessageDeltaData, event.data).delta_content or "" print(delta, end="", flush=True) case "assistant.reasoning_delta": # Streaming reasoning chunk (if model supports reasoning) - delta = event.data.delta_content or "" + delta = cast(AssistantReasoningDeltaData, event.data).delta_content or "" print(delta, end="", flush=True) case "assistant.message": # Final message - complete content print("\n--- Final message ---") - print(event.data.content) + print(cast(AssistantMessageData, event.data).content) case "assistant.reasoning": # Final reasoning content (if model supports reasoning) print("--- Reasoning ---") - print(event.data.content) + print(cast(AssistantReasoningData, event.data).content) case "session.idle": # Session finished processing done.set() diff --git a/python/copilot/session.py b/python/copilot/session.py index 6b0a7e03e..6371c3409 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1132,9 +1132,11 @@ async def send_and_wait( Exception: If the session has been disconnected or the connection fails. Example: + >>> from typing import cast + >>> from copilot.generated.session_events import AssistantMessageData >>> response = await session.send_and_wait("What is 2+2?") >>> if response: - ... print(response.data.content) + ... print(cast(AssistantMessageData, response.data).content) """ idle_event = asyncio.Event() error_event: Exception | None = None @@ -1180,11 +1182,13 @@ def on(self, handler: Callable[[SessionEvent], None]) -> Callable[[], None]: A function that, when called, unsubscribes the handler. Example: + >>> from typing import cast + >>> from copilot.generated.session_events import AssistantMessageData, SessionErrorData >>> def handle_event(event): - ... if event.type == "assistant.message": - ... print(f"Assistant: {event.data.content}") - ... elif event.type == "session.error": - ... print(f"Error: {event.data.message}") + ... if event.type.value == "assistant.message": + ... print(f"Assistant: {cast(AssistantMessageData, event.data).content}") + ... elif event.type.value == "session.error": + ... print(f"Error: {cast(SessionErrorData, event.data).message}") >>> unsubscribe = session.on(handle_event) >>> # Later, to stop receiving events: >>> unsubscribe() @@ -1805,10 +1809,12 @@ async def get_messages(self) -> list[SessionEvent]: Exception: If the session has been disconnected or the connection fails. Example: + >>> from typing import cast + >>> from copilot.generated.session_events import AssistantMessageData >>> events = await session.get_messages() >>> for event in events: - ... if event.type == "assistant.message": - ... print(f"Assistant: {event.data.content}") + ... if event.type.value == "assistant.message": + ... print(f"Assistant: {cast(AssistantMessageData, event.data).content}") """ response = await self._client.request("session.getMessages", {"sessionId": self.session_id}) # Convert dict events to SessionEvent objects From 789b98ead80b3b5b6e6ee94b4be2c670e1203a27 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 12 Apr 2026 22:46:05 -0400 Subject: [PATCH 04/14] Align Python generator formats Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/test/python-codegen.test.ts | 79 +++++++++++++++ python/copilot/generated/session_events.py | 8 +- python/test_event_forward_compatibility.py | 11 +++ scripts/codegen/python.ts | 109 ++++++++++++++++++--- 4 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 nodejs/test/python-codegen.test.ts diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/python-codegen.test.ts new file mode 100644 index 000000000..449fd740c --- /dev/null +++ b/nodejs/test/python-codegen.test.ts @@ -0,0 +1,79 @@ +import type { JSONSchema7 } from "json-schema"; +import { describe, expect, it } from "vitest"; + +import { generatePythonSessionEventsCode } from "../../scripts/codegen/python.ts"; + +describe("python session event codegen", () => { + it("maps special schema formats to the expected Python types", () => { + const schema: JSONSchema7 = { + definitions: { + SessionEvent: { + anyOf: [ + { + type: "object", + required: ["type", "data"], + properties: { + type: { const: "session.synthetic" }, + data: { + type: "object", + required: [ + "at", + "identifier", + "duration", + "integerDuration", + "uri", + "pattern", + "payload", + "encoded", + "count", + ], + properties: { + at: { type: "string", format: "date-time" }, + identifier: { type: "string", format: "uuid" }, + duration: { type: "number", format: "duration" }, + integerDuration: { type: "integer", format: "duration" }, + optionalDuration: { + type: ["number", "null"], + format: "duration", + }, + action: { + type: "string", + enum: ["store", "vote"], + default: "store", + }, + summary: { type: "string", default: "" }, + uri: { type: "string", format: "uri" }, + pattern: { type: "string", format: "regex" }, + payload: { type: "string", format: "byte" }, + encoded: { type: "string", contentEncoding: "base64" }, + count: { type: "integer" }, + }, + }, + }, + }, + ], + }, + }, + }; + + const code = generatePythonSessionEventsCode(schema); + + expect(code).toContain("from datetime import datetime, timedelta"); + expect(code).toContain("at: datetime"); + expect(code).toContain("identifier: UUID"); + expect(code).toContain("duration: timedelta"); + expect(code).toContain("integer_duration: timedelta"); + expect(code).toContain("optional_duration: timedelta | None = None"); + expect(code).toContain('duration = from_timedelta(obj.get("duration"))'); + expect(code).toContain('result["duration"] = to_timedelta(self.duration)'); + expect(code).toContain('result["integerDuration"] = to_timedelta_int(self.integer_duration)'); + expect(code).toContain("def to_timedelta_int(x: timedelta) -> int:"); + expect(code).toContain('action = from_union([from_none, lambda x: parse_enum(SessionSyntheticDataAction, x)], obj.get("action", "store"))'); + expect(code).toContain('summary = from_union([from_none, lambda x: from_str(x)], obj.get("summary", ""))'); + expect(code).toContain("uri: str"); + expect(code).toContain("pattern: str"); + expect(code).toContain("payload: str"); + expect(code).toContain("encoded: str"); + expect(code).toContain("count: int"); + }); +}); diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 6682889ad..9418b6bde 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -1514,7 +1514,7 @@ class SessionTaskCompleteData: @staticmethod def from_dict(obj: Any) -> "SessionTaskCompleteData": assert isinstance(obj, dict) - summary = from_union([from_none, lambda x: from_str(x)], obj.get("summary")) + summary = from_union([from_none, lambda x: from_str(x)], obj.get("summary", "")) success = from_union([from_none, lambda x: from_bool(x)], obj.get("success")) return SessionTaskCompleteData( summary=summary, @@ -3122,7 +3122,7 @@ def from_dict(obj: Any) -> "PermissionRequestedDataPermissionRequest": args = obj.get("args") read_only = from_union([from_none, lambda x: from_bool(x)], obj.get("readOnly")) url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) - action = from_union([from_none, lambda x: parse_enum(PermissionRequestedDataPermissionRequestAction, x)], obj.get("action")) + action = from_union([from_none, lambda x: parse_enum(PermissionRequestedDataPermissionRequestAction, x)], obj.get("action", "store")) subject = from_union([from_none, lambda x: from_str(x)], obj.get("subject")) fact = from_union([from_none, lambda x: from_str(x)], obj.get("fact")) citations = from_union([from_none, lambda x: from_str(x)], obj.get("citations")) @@ -4305,13 +4305,13 @@ class PermissionRequestedDataPermissionRequestKind(Enum): class PermissionRequestedDataPermissionRequestAction(Enum): - """Whether this is a store or vote memory operation""" + "Whether this is a store or vote memory operation" STORE = "store" VOTE = "vote" class PermissionRequestedDataPermissionRequestDirection(Enum): - """Vote direction (vote only)""" + "Vote direction (vote only)" UPVOTE = "upvote" DOWNVOTE = "downvote" diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index f0a4bedef..7bd65039c 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -18,12 +18,15 @@ ContentElement, Data, Mode, + PermissionRequestedDataPermissionRequest, + PermissionRequestedDataPermissionRequestAction, ReferenceType, RequestedSchema, RequestedSchemaType, Resource, Result, ResultKind, + SessionTaskCompleteData, SessionEventType, session_event_from_dict, ) @@ -130,3 +133,11 @@ def test_data_shim_preserves_raw_mapping_values(self): constructed = Data(arguments={"tool_call_id": "call-1"}) assert constructed.to_dict() == {"arguments": {"tool_call_id": "call-1"}} + + def test_schema_defaults_are_applied_for_missing_optional_fields(self): + """Generated event models should honor primitive schema defaults during parsing.""" + request = PermissionRequestedDataPermissionRequest.from_dict({"kind": "memory", "fact": "remember this"}) + assert request.action == PermissionRequestedDataPermissionRequestAction.STORE + + task_complete = SessionTaskCompleteData.from_dict({"success": True}) + assert task_complete.summary == "" diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 99ba86852..e75279186 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -7,8 +7,9 @@ */ import fs from "fs/promises"; +import path from "path"; import type { JSONSchema7 } from "json-schema"; -import { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } from "quicktype-core"; +import { fileURLToPath } from "url"; import { cloneSchemaForCodegen, getApiSchemaPath, @@ -234,6 +235,8 @@ interface PyCodegenCtx { enums: string[]; enumsByValues: Map; generatedNames: Set; + usesTimedelta: boolean; + usesIntegerTimedelta: boolean; } function toEnumMemberName(value: string): string { @@ -280,6 +283,38 @@ function pyAnyResolvedType(): PyResolvedType { }; } +function pyDurationResolvedType(ctx: PyCodegenCtx, isInteger: boolean): PyResolvedType { + ctx.usesTimedelta = true; + if (isInteger) { + ctx.usesIntegerTimedelta = true; + } + return { + annotation: "timedelta", + fromExpr: (expr) => `from_timedelta(${expr})`, + toExpr: (expr) => (isInteger ? `to_timedelta_int(${expr})` : `to_timedelta(${expr})`), + }; +} + +function isPyBase64StringSchema(schema: JSONSchema7): boolean { + return schema.format === "byte" || (schema as Record).contentEncoding === "base64"; +} + +function toPythonLiteral(value: unknown): string | undefined { + if (typeof value === "string") { + return JSON.stringify(value); + } + if (typeof value === "number") { + return Number.isFinite(value) ? String(value) : undefined; + } + if (typeof value === "boolean") { + return value ? "True" : "False"; + } + if (value === null) { + return "None"; + } + return undefined; +} + function extractPyEventVariants(schema: JSONSchema7): PyEventVariant[] { const sessionEvent = schema.definitions?.SessionEvent as JSONSchema7; if (!sessionEvent?.anyOf) { @@ -489,16 +524,28 @@ function resolvePyPropertyType( const resolved = pyPrimitiveResolvedType("UUID", "from_uuid", "to_uuid"); return isRequired ? resolved : pyOptionalResolvedType(resolved); } + if (format === "uri" || format === "regex" || isPyBase64StringSchema(propSchema)) { + const resolved = pyPrimitiveResolvedType("str", "from_str"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } const resolved = pyPrimitiveResolvedType("str", "from_str"); return isRequired ? resolved : pyOptionalResolvedType(resolved); } if (type === "integer") { + if (format === "duration") { + const resolved = pyDurationResolvedType(ctx, true); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } const resolved = pyPrimitiveResolvedType("int", "from_int", "to_int"); return isRequired ? resolved : pyOptionalResolvedType(resolved); } if (type === "number") { + if (format === "duration") { + const resolved = pyDurationResolvedType(ctx, false); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } const resolved = pyPrimitiveResolvedType("float", "from_float", "to_float"); return isRequired ? resolved : pyOptionalResolvedType(resolved); } @@ -634,6 +681,7 @@ function emitPyClass( fieldName: toSnakeCase(propName), isRequired, resolved, + defaultLiteral: isRequired ? undefined : toPythonLiteral(propSchema.default), }; }); @@ -666,8 +714,11 @@ function emitPyClass( lines.push(` def from_dict(obj: Any) -> "${typeName}":`); lines.push(` assert isinstance(obj, dict)`); for (const field of fieldInfos) { + const sourceExpr = field.defaultLiteral + ? `obj.get(${JSON.stringify(field.jsonName)}, ${field.defaultLiteral})` + : `obj.get(${JSON.stringify(field.jsonName)})`; lines.push( - ` ${field.fieldName} = ${field.resolved.fromExpr(`obj.get(${JSON.stringify(field.jsonName)})`)}` + ` ${field.fieldName} = ${field.resolved.fromExpr(sourceExpr)}` ); } lines.push(` return ${typeName}(`); @@ -778,6 +829,7 @@ function emitPyFlatDiscriminatedUnion( fieldName: toSnakeCase(propName), isRequired: requiredInAll, resolved, + defaultLiteral: requiredInAll ? undefined : toPythonLiteral(propSchema.default), }; }); @@ -796,8 +848,11 @@ function emitPyFlatDiscriminatedUnion( lines.push(` def from_dict(obj: Any) -> "${typeName}":`); lines.push(` assert isinstance(obj, dict)`); for (const field of fieldInfos) { + const sourceExpr = field.defaultLiteral + ? `obj.get(${JSON.stringify(field.jsonName)}, ${field.defaultLiteral})` + : `obj.get(${JSON.stringify(field.jsonName)})`; lines.push( - ` ${field.fieldName} = ${field.resolved.fromExpr(`obj.get(${JSON.stringify(field.jsonName)})`)}` + ` ${field.fieldName} = ${field.resolved.fromExpr(sourceExpr)}` ); } lines.push(` return ${typeName}(`); @@ -822,13 +877,15 @@ function emitPyFlatDiscriminatedUnion( ctx.classes.push(lines.join("\n")); } -function generatePythonSessionEventsCode(schema: JSONSchema7): string { +export function generatePythonSessionEventsCode(schema: JSONSchema7): string { const variants = extractPyEventVariants(schema); const ctx: PyCodegenCtx = { classes: [], enums: [], enumsByValues: new Map(), generatedNames: new Set(), + usesTimedelta: false, + usesIntegerTimedelta: false, }; for (const variant of variants) { @@ -856,7 +913,7 @@ function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(``); out.push(`from collections.abc import Callable`); out.push(`from dataclasses import dataclass`); - out.push(`from datetime import datetime`); + out.push(ctx.usesTimedelta ? `from datetime import datetime, timedelta` : `from datetime import datetime`); out.push(`from enum import Enum`); out.push(`from typing import Any, TypeVar, cast`); out.push(`from uuid import UUID`); @@ -892,6 +949,27 @@ function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(` return float(x)`); out.push(``); out.push(``); + if (ctx.usesTimedelta) { + out.push(`def from_timedelta(x: Any) -> timedelta:`); + out.push(` assert isinstance(x, (float, int)) and not isinstance(x, bool)`); + out.push(` return timedelta(milliseconds=float(x))`); + out.push(``); + out.push(``); + if (ctx.usesIntegerTimedelta) { + out.push(`def to_timedelta_int(x: timedelta) -> int:`); + out.push(` assert isinstance(x, timedelta)`); + out.push(` milliseconds = x.total_seconds() * 1000.0`); + out.push(` assert milliseconds.is_integer()`); + out.push(` return int(milliseconds)`); + out.push(``); + out.push(``); + } + out.push(`def to_timedelta(x: timedelta) -> float:`); + out.push(` assert isinstance(x, timedelta)`); + out.push(` return x.total_seconds() * 1000.0`); + out.push(``); + out.push(``); + } out.push(`def from_bool(x: Any) -> bool:`); out.push(` assert isinstance(x, bool)`); out.push(` return x`); @@ -993,6 +1071,10 @@ function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(` return value.value`); out.push(` if isinstance(value, datetime):`); out.push(` return value.isoformat()`); + if (ctx.usesTimedelta) { + out.push(` if isinstance(value, timedelta):`); + out.push(` return value.total_seconds() * 1000.0`); + } out.push(` if isinstance(value, UUID):`); out.push(` return str(value)`); out.push(` if isinstance(value, list):`); @@ -1360,6 +1442,7 @@ async function generateSessionEvents(schemaPath?: string): Promise { async function generateRpc(schemaPath?: string): Promise { console.log("Python: generating RPC types..."); + const { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } = await import("quicktype-core"); const resolvedPath = schemaPath ?? (await getApiSchemaPath()); const schema = cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as ApiSchema); @@ -1778,9 +1861,13 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro } } -const sessionArg = process.argv[2] || undefined; -const apiArg = process.argv[3] || undefined; -generate(sessionArg, apiArg).catch((err) => { - console.error("Python generation failed:", err); - process.exit(1); -}); +const __filename = fileURLToPath(import.meta.url); + +if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { + const sessionArg = process.argv[2] || undefined; + const apiArg = process.argv[3] || undefined; + generate(sessionArg, apiArg).catch((err) => { + console.error("Python generation failed:", err); + process.exit(1); + }); +} From 9fee14e9aee453ce75021cc0995ada96987a5c19 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 12 Apr 2026 23:36:53 -0400 Subject: [PATCH 05/14] Fix SDK test formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/test/python-codegen.test.ts | 12 +++++++++--- python/test_event_forward_compatibility.py | 6 ++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/python-codegen.test.ts index 449fd740c..c47284e17 100644 --- a/nodejs/test/python-codegen.test.ts +++ b/nodejs/test/python-codegen.test.ts @@ -66,10 +66,16 @@ describe("python session event codegen", () => { expect(code).toContain("optional_duration: timedelta | None = None"); expect(code).toContain('duration = from_timedelta(obj.get("duration"))'); expect(code).toContain('result["duration"] = to_timedelta(self.duration)'); - expect(code).toContain('result["integerDuration"] = to_timedelta_int(self.integer_duration)'); + expect(code).toContain( + 'result["integerDuration"] = to_timedelta_int(self.integer_duration)' + ); expect(code).toContain("def to_timedelta_int(x: timedelta) -> int:"); - expect(code).toContain('action = from_union([from_none, lambda x: parse_enum(SessionSyntheticDataAction, x)], obj.get("action", "store"))'); - expect(code).toContain('summary = from_union([from_none, lambda x: from_str(x)], obj.get("summary", ""))'); + expect(code).toContain( + 'action = from_union([from_none, lambda x: parse_enum(SessionSyntheticDataAction, x)], obj.get("action", "store"))' + ); + expect(code).toContain( + 'summary = from_union([from_none, lambda x: from_str(x)], obj.get("summary", ""))' + ); expect(code).toContain("uri: str"); expect(code).toContain("pattern: str"); expect(code).toContain("payload: str"); diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 7bd65039c..4825be3f5 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -26,8 +26,8 @@ Resource, Result, ResultKind, - SessionTaskCompleteData, SessionEventType, + SessionTaskCompleteData, session_event_from_dict, ) @@ -136,7 +136,9 @@ def test_data_shim_preserves_raw_mapping_values(self): def test_schema_defaults_are_applied_for_missing_optional_fields(self): """Generated event models should honor primitive schema defaults during parsing.""" - request = PermissionRequestedDataPermissionRequest.from_dict({"kind": "memory", "fact": "remember this"}) + request = PermissionRequestedDataPermissionRequest.from_dict( + {"kind": "memory", "fact": "remember this"} + ) assert request.action == PermissionRequestedDataPermissionRequestAction.STORE task_complete = SessionTaskCompleteData.from_dict({"success": True}) From 9020a5831c4e899152565b40a760019cf67ce0b2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 13 Apr 2026 09:59:41 -0400 Subject: [PATCH 06/14] Use runtime checks in Python README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/README.md | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/python/README.md b/python/README.md index b303ab9bc..14dc57947 100644 --- a/python/README.md +++ b/python/README.md @@ -25,7 +25,6 @@ python chat.py ```python import asyncio -from typing import cast from copilot import CopilotClient from copilot.generated.session_events import AssistantMessageData @@ -39,8 +38,8 @@ async def main(): done = asyncio.Event() def on_event(event): - if event.type.value == "assistant.message": - print(cast(AssistantMessageData, event.data).content) + if event.type.value == "assistant.message" and isinstance(event.data, AssistantMessageData): + print(event.data.content) elif event.type.value == "session.idle": done.set() @@ -59,7 +58,6 @@ If you need more control over the lifecycle, you can call `start()`, `stop()`, a ```python import asyncio -from typing import cast from copilot import CopilotClient from copilot.generated.session_events import AssistantMessageData @@ -78,8 +76,8 @@ async def main(): done = asyncio.Event() def on_event(event): - if event.type.value == "assistant.message": - print(cast(AssistantMessageData, event.data).content) + if event.type.value == "assistant.message" and isinstance(event.data, AssistantMessageData): + print(event.data.content) elif event.type.value == "session.idle": done.set() @@ -338,7 +336,6 @@ Enable streaming to receive assistant response chunks as they're generated: ```python import asyncio -from typing import cast from copilot import CopilotClient from copilot.generated.session_events import ( @@ -362,21 +359,25 @@ async def main(): def on_event(event): match event.type.value: case "assistant.message_delta": - # Streaming message chunk - print incrementally - delta = cast(AssistantMessageDeltaData, event.data).delta_content or "" - print(delta, end="", flush=True) + if isinstance(event.data, AssistantMessageDeltaData): + # Streaming message chunk - print incrementally + delta = event.data.delta_content or "" + print(delta, end="", flush=True) case "assistant.reasoning_delta": - # Streaming reasoning chunk (if model supports reasoning) - delta = cast(AssistantReasoningDeltaData, event.data).delta_content or "" - print(delta, end="", flush=True) + if isinstance(event.data, AssistantReasoningDeltaData): + # Streaming reasoning chunk (if model supports reasoning) + delta = event.data.delta_content or "" + print(delta, end="", flush=True) case "assistant.message": - # Final message - complete content - print("\n--- Final message ---") - print(cast(AssistantMessageData, event.data).content) + if isinstance(event.data, AssistantMessageData): + # Final message - complete content + print("\n--- Final message ---") + print(event.data.content) case "assistant.reasoning": - # Final reasoning content (if model supports reasoning) - print("--- Reasoning ---") - print(cast(AssistantReasoningData, event.data).content) + if isinstance(event.data, AssistantReasoningData): + # Final reasoning content (if model supports reasoning) + print("--- Reasoning ---") + print(event.data.content) case "session.idle": # Session finished processing done.set() From 591ac03b3eb94ee1814049502015a5cf2226047b Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 13 Apr 2026 16:52:58 -0400 Subject: [PATCH 07/14] Use isinstance in Python event examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/copilot/session.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/python/copilot/session.py b/python/copilot/session.py index 6371c3409..204339a62 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1132,11 +1132,10 @@ async def send_and_wait( Exception: If the session has been disconnected or the connection fails. Example: - >>> from typing import cast >>> from copilot.generated.session_events import AssistantMessageData >>> response = await session.send_and_wait("What is 2+2?") - >>> if response: - ... print(cast(AssistantMessageData, response.data).content) + >>> if response and isinstance(response.data, AssistantMessageData): + ... print(response.data.content) """ idle_event = asyncio.Event() error_event: Exception | None = None @@ -1182,13 +1181,12 @@ def on(self, handler: Callable[[SessionEvent], None]) -> Callable[[], None]: A function that, when called, unsubscribes the handler. Example: - >>> from typing import cast >>> from copilot.generated.session_events import AssistantMessageData, SessionErrorData >>> def handle_event(event): - ... if event.type.value == "assistant.message": - ... print(f"Assistant: {cast(AssistantMessageData, event.data).content}") - ... elif event.type.value == "session.error": - ... print(f"Error: {cast(SessionErrorData, event.data).message}") + ... if isinstance(event.data, AssistantMessageData): + ... print(f"Assistant: {event.data.content}") + ... elif isinstance(event.data, SessionErrorData): + ... print(f"Error: {event.data.message}") >>> unsubscribe = session.on(handle_event) >>> # Later, to stop receiving events: >>> unsubscribe() @@ -1809,12 +1807,11 @@ async def get_messages(self) -> list[SessionEvent]: Exception: If the session has been disconnected or the connection fails. Example: - >>> from typing import cast >>> from copilot.generated.session_events import AssistantMessageData >>> events = await session.get_messages() >>> for event in events: - ... if event.type.value == "assistant.message": - ... print(f"Assistant: {cast(AssistantMessageData, event.data).content}") + ... if isinstance(event.data, AssistantMessageData): + ... print(f"Assistant: {event.data.content}") """ response = await self._client.request("session.getMessages", {"sessionId": self.session_id}) # Convert dict events to SessionEvent objects From adc2d9bda53ec45f14cf050c04d799fba5523961 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 13 Apr 2026 17:41:35 -0400 Subject: [PATCH 08/14] Use isinstance for broadcast events Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/copilot/session.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/python/copilot/session.py b/python/copilot/session.py index 204339a62..58bc6f3f7 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1232,8 +1232,9 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: Implements the protocol v3 broadcast model where tool calls and permission requests are broadcast as session events to all clients. """ - if event.type == SessionEventType.EXTERNAL_TOOL_REQUESTED: - data = cast(ExternalToolRequestedData, event.data) + data = event.data + + if isinstance(data, ExternalToolRequestedData): request_id = data.request_id tool_name = data.tool_name if not request_id or not tool_name: @@ -1253,8 +1254,7 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: ) ) - elif event.type == SessionEventType.PERMISSION_REQUESTED: - data = cast(PermissionRequestedData, event.data) + elif isinstance(data, PermissionRequestedData): request_id = data.request_id permission_request = data.permission_request if not request_id or not permission_request: @@ -1273,8 +1273,7 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: self._execute_permission_and_respond(request_id, permission_request, perm_handler) ) - elif event.type == SessionEventType.COMMAND_EXECUTE: - data = cast(CommandExecuteData, event.data) + elif isinstance(data, CommandExecuteData): request_id = data.request_id command_name = data.command_name command = data.command @@ -1287,8 +1286,7 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: ) ) - elif event.type == SessionEventType.ELICITATION_REQUESTED: - data = cast(ElicitationRequestedData, event.data) + elif isinstance(data, ElicitationRequestedData): with self._elicitation_handler_lock: handler = self._elicitation_handler if not handler: @@ -1310,8 +1308,7 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: context["url"] = data.url asyncio.ensure_future(self._handle_elicitation_request(context, request_id)) - elif event.type == SessionEventType.CAPABILITIES_CHANGED: - data = cast(CapabilitiesChangedData, event.data) + elif isinstance(data, CapabilitiesChangedData): cap: SessionCapabilities = {} if data.ui is not None: ui_cap: SessionUiCapabilities = {} From 9a0a8f220cc31a84df9e059c242ba653cf0c0e3a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 13 Apr 2026 22:20:22 -0400 Subject: [PATCH 09/14] Use match for Python event data Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/README.md | 63 +++--- python/copilot/session.py | 191 +++++++++--------- python/e2e/test_permissions.py | 47 +++-- python/e2e/test_session.py | 15 +- python/e2e/test_session_fs.py | 8 +- .../e2e/test_ui_elicitation_multi_client.py | 30 +-- python/e2e/testharness/helper.py | 37 ++-- python/samples/chat.py | 21 +- 8 files changed, 225 insertions(+), 187 deletions(-) diff --git a/python/README.md b/python/README.md index 14dc57947..e54322d7d 100644 --- a/python/README.md +++ b/python/README.md @@ -27,7 +27,7 @@ python chat.py import asyncio from copilot import CopilotClient -from copilot.generated.session_events import AssistantMessageData +from copilot.generated.session_events import AssistantMessageData, SessionIdleData async def main(): # Client automatically starts on enter and cleans up on exit @@ -38,10 +38,11 @@ async def main(): done = asyncio.Event() def on_event(event): - if event.type.value == "assistant.message" and isinstance(event.data, AssistantMessageData): - print(event.data.content) - elif event.type.value == "session.idle": - done.set() + match event.data: + case AssistantMessageData() as data: + print(data.content) + case SessionIdleData(): + done.set() session.on(on_event) @@ -60,7 +61,7 @@ If you need more control over the lifecycle, you can call `start()`, `stop()`, a import asyncio from copilot import CopilotClient -from copilot.generated.session_events import AssistantMessageData +from copilot.generated.session_events import AssistantMessageData, SessionIdleData from copilot.session import PermissionHandler async def main(): @@ -76,10 +77,11 @@ async def main(): done = asyncio.Event() def on_event(event): - if event.type.value == "assistant.message" and isinstance(event.data, AssistantMessageData): - print(event.data.content) - elif event.type.value == "session.idle": - done.set() + match event.data: + case AssistantMessageData() as data: + print(data.content) + case SessionIdleData(): + done.set() session.on(on_event) await session.send("What is 2+2?") @@ -343,6 +345,7 @@ from copilot.generated.session_events import ( AssistantMessageDeltaData, AssistantReasoningData, AssistantReasoningDeltaData, + SessionIdleData, ) from copilot.session import PermissionHandler @@ -357,28 +360,24 @@ async def main(): done = asyncio.Event() def on_event(event): - match event.type.value: - case "assistant.message_delta": - if isinstance(event.data, AssistantMessageDeltaData): - # Streaming message chunk - print incrementally - delta = event.data.delta_content or "" - print(delta, end="", flush=True) - case "assistant.reasoning_delta": - if isinstance(event.data, AssistantReasoningDeltaData): - # Streaming reasoning chunk (if model supports reasoning) - delta = event.data.delta_content or "" - print(delta, end="", flush=True) - case "assistant.message": - if isinstance(event.data, AssistantMessageData): - # Final message - complete content - print("\n--- Final message ---") - print(event.data.content) - case "assistant.reasoning": - if isinstance(event.data, AssistantReasoningData): - # Final reasoning content (if model supports reasoning) - print("--- Reasoning ---") - print(event.data.content) - case "session.idle": + match event.data: + case AssistantMessageDeltaData() as data: + # Streaming message chunk - print incrementally + delta = data.delta_content or "" + print(delta, end="", flush=True) + case AssistantReasoningDeltaData() as data: + # Streaming reasoning chunk (if model supports reasoning) + delta = data.delta_content or "" + print(delta, end="", flush=True) + case AssistantMessageData() as data: + # Final message - complete content + print("\n--- Final message ---") + print(data.content) + case AssistantReasoningData() as data: + # Final reasoning content (if model supports reasoning) + print("--- Reasoning ---") + print(data.content) + case SessionIdleData(): # Session finished processing done.set() diff --git a/python/copilot/session.py b/python/copilot/session.py index 58bc6f3f7..b6d8df8e5 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -45,6 +45,7 @@ ) from .generated.rpc import ModelCapabilitiesOverride as _RpcModelCapabilitiesOverride from .generated.session_events import ( + AssistantMessageData, CapabilitiesChangedData, CommandExecuteData, ElicitationRequestedData, @@ -52,7 +53,9 @@ PermissionRequest, PermissionRequestedData, SessionEvent, + SessionErrorData, SessionEventType, + SessionIdleData, session_event_from_dict, ) from .tools import Tool, ToolHandler, ToolInvocation, ToolResult @@ -1134,8 +1137,10 @@ async def send_and_wait( Example: >>> from copilot.generated.session_events import AssistantMessageData >>> response = await session.send_and_wait("What is 2+2?") - >>> if response and isinstance(response.data, AssistantMessageData): - ... print(response.data.content) + >>> if response: + ... match response.data: + ... case AssistantMessageData() as data: + ... print(data.content) """ idle_event = asyncio.Event() error_event: Exception | None = None @@ -1143,15 +1148,14 @@ async def send_and_wait( def handler(event: SessionEventTypeAlias) -> None: nonlocal last_assistant_message, error_event - if event.type == SessionEventType.ASSISTANT_MESSAGE: - last_assistant_message = event - elif event.type == SessionEventType.SESSION_IDLE: - idle_event.set() - elif event.type == SessionEventType.SESSION_ERROR: - error_event = Exception( - f"Session error: {getattr(event.data, 'message', str(event.data))}" - ) - idle_event.set() + match event.data: + case AssistantMessageData(): + last_assistant_message = event + case SessionIdleData(): + idle_event.set() + case SessionErrorData() as data: + error_event = Exception(f"Session error: {data.message or str(data)}") + idle_event.set() unsubscribe = self.on(handler) try: @@ -1183,10 +1187,11 @@ def on(self, handler: Callable[[SessionEvent], None]) -> Callable[[], None]: Example: >>> from copilot.generated.session_events import AssistantMessageData, SessionErrorData >>> def handle_event(event): - ... if isinstance(event.data, AssistantMessageData): - ... print(f"Assistant: {event.data.content}") - ... elif isinstance(event.data, SessionErrorData): - ... print(f"Error: {event.data.message}") + ... match event.data: + ... case AssistantMessageData() as data: + ... print(f"Assistant: {data.content}") + ... case SessionErrorData() as data: + ... print(f"Error: {data.message}") >>> unsubscribe = session.on(handle_event) >>> # Later, to stop receiving events: >>> unsubscribe() @@ -1232,90 +1237,89 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: Implements the protocol v3 broadcast model where tool calls and permission requests are broadcast as session events to all clients. """ - data = event.data - - if isinstance(data, ExternalToolRequestedData): - request_id = data.request_id - tool_name = data.tool_name - if not request_id or not tool_name: - return - - handler = self._get_tool_handler(tool_name) - if not handler: - return # This client doesn't handle this tool; another client will. - - tool_call_id = data.tool_call_id or "" - arguments = data.arguments - tp = getattr(data, "traceparent", None) - ts = getattr(data, "tracestate", None) - asyncio.ensure_future( - self._execute_tool_and_respond( - request_id, tool_name, tool_call_id, arguments, handler, tp, ts + match event.data: + case ExternalToolRequestedData() as data: + request_id = data.request_id + tool_name = data.tool_name + if not request_id or not tool_name: + return + + handler = self._get_tool_handler(tool_name) + if not handler: + return # This client doesn't handle this tool; another client will. + + tool_call_id = data.tool_call_id or "" + arguments = data.arguments + tp = getattr(data, "traceparent", None) + ts = getattr(data, "tracestate", None) + asyncio.ensure_future( + self._execute_tool_and_respond( + request_id, tool_name, tool_call_id, arguments, handler, tp, ts + ) ) - ) - elif isinstance(data, PermissionRequestedData): - request_id = data.request_id - permission_request = data.permission_request - if not request_id or not permission_request: - return + case PermissionRequestedData() as data: + request_id = data.request_id + permission_request = data.permission_request + if not request_id or not permission_request: + return - resolved_by_hook = getattr(data, "resolved_by_hook", None) - if resolved_by_hook: - return # Already resolved by a permissionRequest hook; no client action needed. + resolved_by_hook = getattr(data, "resolved_by_hook", None) + if resolved_by_hook: + return # Already resolved by a permissionRequest hook; no client action needed. - with self._permission_handler_lock: - perm_handler = self._permission_handler - if not perm_handler: - return # This client doesn't handle permissions; another client will. + with self._permission_handler_lock: + perm_handler = self._permission_handler + if not perm_handler: + return # This client doesn't handle permissions; another client will. - asyncio.ensure_future( - self._execute_permission_and_respond(request_id, permission_request, perm_handler) - ) + asyncio.ensure_future( + self._execute_permission_and_respond(request_id, permission_request, perm_handler) + ) - elif isinstance(data, CommandExecuteData): - request_id = data.request_id - command_name = data.command_name - command = data.command - args = data.args - if not request_id or not command_name: - return - asyncio.ensure_future( - self._execute_command_and_respond( - request_id, command_name, command or "", args or "" + case CommandExecuteData() as data: + request_id = data.request_id + command_name = data.command_name + command = data.command + args = data.args + if not request_id or not command_name: + return + asyncio.ensure_future( + self._execute_command_and_respond( + request_id, command_name, command or "", args or "" + ) ) - ) - elif isinstance(data, ElicitationRequestedData): - with self._elicitation_handler_lock: - handler = self._elicitation_handler - if not handler: - return - request_id = data.request_id - if not request_id: - return - context: ElicitationContext = { - "session_id": self.session_id, - "message": data.message or "", - } - if data.requested_schema is not None: - context["requestedSchema"] = data.requested_schema.to_dict() - if data.mode is not None: - context["mode"] = data.mode.value - if data.elicitation_source is not None: - context["elicitationSource"] = data.elicitation_source - if data.url is not None: - context["url"] = data.url - asyncio.ensure_future(self._handle_elicitation_request(context, request_id)) - - elif isinstance(data, CapabilitiesChangedData): - cap: SessionCapabilities = {} - if data.ui is not None: - ui_cap: SessionUiCapabilities = {} - if data.ui.elicitation is not None: - ui_cap["elicitation"] = data.ui.elicitation - cap["ui"] = ui_cap - self._capabilities = {**self._capabilities, **cap} + case ElicitationRequestedData() as data: + with self._elicitation_handler_lock: + handler = self._elicitation_handler + if not handler: + return + request_id = data.request_id + if not request_id: + return + context: ElicitationContext = { + "session_id": self.session_id, + "message": data.message or "", + } + if data.requested_schema is not None: + context["requestedSchema"] = data.requested_schema.to_dict() + if data.mode is not None: + context["mode"] = data.mode.value + if data.elicitation_source is not None: + context["elicitationSource"] = data.elicitation_source + if data.url is not None: + context["url"] = data.url + asyncio.ensure_future(self._handle_elicitation_request(context, request_id)) + + case CapabilitiesChangedData() as data: + cap: SessionCapabilities = {} + if data.ui is not None: + ui_cap: SessionUiCapabilities = {} + if data.ui.elicitation is not None: + ui_cap["elicitation"] = data.ui.elicitation + cap["ui"] = ui_cap + self._capabilities = {**self._capabilities, **cap} async def _execute_tool_and_respond( self, @@ -1807,8 +1811,9 @@ async def get_messages(self) -> list[SessionEvent]: >>> from copilot.generated.session_events import AssistantMessageData >>> events = await session.get_messages() >>> for event in events: - ... if isinstance(event.data, AssistantMessageData): - ... print(f"Assistant: {event.data.content}") + ... match event.data: + ... case AssistantMessageData() as data: + ... print(f"Assistant: {data.content}") """ response = await self._client.request("session.getMessages", {"sessionId": self.session_id}) # Convert dict events to SessionEvent objects diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index 692c600e0..31f2230fe 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -6,6 +6,7 @@ import pytest +from copilot.generated.session_events import SessionIdleData, ToolExecutionCompleteData from copilot.session import PermissionHandler, PermissionRequest, PermissionRequestResult from .testharness import E2ETestContext @@ -76,17 +77,18 @@ def deny_all(request, invocation): done_event = asyncio.Event() def on_event(event): - if event.type.value == "tool.execution_complete" and event.data.success is False: - error = event.data.error - msg = ( - error - if isinstance(error, str) - else (getattr(error, "message", None) if error is not None else None) - ) - if msg and "Permission denied" in msg: - denied_events.append(event) - elif event.type.value == "session.idle": - done_event.set() + match event.data: + case ToolExecutionCompleteData(success=False) as data: + error = data.error + msg = ( + error + if isinstance(error, str) + else (getattr(error, "message", None) if error is not None else None) + ) + if msg and "Permission denied" in msg: + denied_events.append(event) + case SessionIdleData(): + done_event.set() session.on(on_event) @@ -116,17 +118,18 @@ def deny_all(request, invocation): done_event = asyncio.Event() def on_event(event): - if event.type.value == "tool.execution_complete" and event.data.success is False: - error = event.data.error - msg = ( - error - if isinstance(error, str) - else (getattr(error, "message", None) if error is not None else None) - ) - if msg and "Permission denied" in msg: - denied_events.append(event) - elif event.type.value == "session.idle": - done_event.set() + match event.data: + case ToolExecutionCompleteData(success=False) as data: + error = data.error + msg = ( + error + if isinstance(error, str) + else (getattr(error, "message", None) if error is not None else None) + ) + if msg and "Permission denied" in msg: + denied_events.append(event) + case SessionIdleData(): + done_event.set() session2.on(on_event) diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 1a249b516..621062e4e 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -7,6 +7,7 @@ from copilot import CopilotClient from copilot.client import SubprocessConfig +from copilot.generated.session_events import SessionModelChangeData from copilot.session import PermissionHandler from copilot.tools import Tool, ToolResult @@ -600,16 +601,20 @@ async def test_should_set_model_with_reasoning_effort(self, ctx: E2ETestContext) model_change_event = asyncio.get_event_loop().create_future() def on_event(event): - if not model_change_event.done() and event.type.value == "session.model_change": - model_change_event.set_result(event) + if model_change_event.done(): + return + + match event.data: + case SessionModelChangeData() as data: + model_change_event.set_result(data) session.on(on_event) await session.set_model("gpt-4.1", reasoning_effort="high") - event = await asyncio.wait_for(model_change_event, timeout=30) - assert event.data.new_model == "gpt-4.1" - assert event.data.reasoning_effort == "high" + data = await asyncio.wait_for(model_change_event, timeout=30) + assert data.new_model == "gpt-4.1" + assert data.reasoning_effort == "high" async def test_should_accept_blob_attachments(self, ctx: E2ETestContext): # Write the image to disk so the model can view it diff --git a/python/e2e/test_session_fs.py b/python/e2e/test_session_fs.py index d9bfabb55..f0637b253 100644 --- a/python/e2e/test_session_fs.py +++ b/python/e2e/test_session_fs.py @@ -13,6 +13,7 @@ from copilot import CopilotClient, SessionFsConfig, define_tool from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot.generated.session_events import SessionCompactionCompleteData from copilot.generated.rpc import ( SessionFSExistsResult, SessionFSReaddirResult, @@ -192,9 +193,10 @@ async def test_should_succeed_with_compaction_while_using_sessionfs( def on_event(event: SessionEvent): nonlocal compaction_success - if event.type.value == "session.compaction_complete": - compaction_success = event.data.success - compaction_event.set() + match event.data: + case SessionCompactionCompleteData() as data: + compaction_success = data.success + compaction_event.set() session.on(on_event) diff --git a/python/e2e/test_ui_elicitation_multi_client.py b/python/e2e/test_ui_elicitation_multi_client.py index 45280f6b2..4c63fb6b2 100644 --- a/python/e2e/test_ui_elicitation_multi_client.py +++ b/python/e2e/test_ui_elicitation_multi_client.py @@ -17,6 +17,7 @@ from copilot import CopilotClient from copilot.client import ExternalServerConfig, SubprocessConfig +from copilot.generated.session_events import CapabilitiesChangedData from copilot.session import ( ElicitationContext, ElicitationResult, @@ -194,11 +195,12 @@ async def test_capabilities_changed_when_second_client_joins_with_elicitation( cap_event_data: dict = {} def on_event(event): - if event.type.value == "capabilities.changed": - ui = getattr(event.data, "ui", None) - if ui: - cap_event_data["elicitation"] = getattr(ui, "elicitation", None) - cap_changed.set() + match event.data: + case CapabilitiesChangedData() as data: + ui = data.ui + if ui: + cap_event_data["elicitation"] = ui.elicitation + cap_changed.set() unsubscribe = session1.on(on_event) @@ -239,10 +241,11 @@ async def test_capabilities_changed_when_elicitation_provider_disconnects( cap_enabled = asyncio.Event() def on_enabled(event): - if event.type.value == "capabilities.changed": - ui = getattr(event.data, "ui", None) - if ui and getattr(ui, "elicitation", None) is True: - cap_enabled.set() + match event.data: + case CapabilitiesChangedData() as data: + ui = data.ui + if ui and ui.elicitation is True: + cap_enabled.set() unsub_enabled = session1.on(on_enabled) @@ -269,10 +272,11 @@ async def handler( cap_disabled = asyncio.Event() def on_disabled(event): - if event.type.value == "capabilities.changed": - ui = getattr(event.data, "ui", None) - if ui and getattr(ui, "elicitation", None) is False: - cap_disabled.set() + match event.data: + case CapabilitiesChangedData() as data: + ui = data.ui + if ui and ui.elicitation is False: + cap_disabled.set() unsub_disabled = session1.on(on_disabled) diff --git a/python/e2e/testharness/helper.py b/python/e2e/testharness/helper.py index e0e3d267c..c603a8ec5 100644 --- a/python/e2e/testharness/helper.py +++ b/python/e2e/testharness/helper.py @@ -6,6 +6,11 @@ import os from copilot import CopilotSession +from copilot.generated.session_events import ( + AssistantMessageData, + SessionErrorData, + SessionIdleData, +) async def get_final_assistant_message( @@ -34,14 +39,15 @@ def on_event(event): if result_future.done(): return - if event.type.value == "assistant.message": - final_assistant_message = event - elif event.type.value == "session.idle": - if final_assistant_message is not None: - result_future.set_result(final_assistant_message) - elif event.type.value == "session.error": - msg = event.data.message if event.data.message else "session error" - result_future.set_exception(RuntimeError(msg)) + match event.data: + case AssistantMessageData(): + final_assistant_message = event + case SessionIdleData(): + if final_assistant_message is not None: + result_future.set_result(final_assistant_message) + case SessionErrorData() as data: + msg = data.message if data.message else "session error" + result_future.set_exception(RuntimeError(msg)) # Subscribe to future events unsubscribe = session.on(on_event) @@ -75,9 +81,10 @@ async def _get_existing_final_response(session: CopilotSession, already_idle: bo # Check for errors for msg in current_turn_messages: - if msg.type.value == "session.error": - err_msg = msg.data.message if msg.data.message else "session error" - raise RuntimeError(err_msg) + match msg.data: + case SessionErrorData() as data: + err_msg = data.message if data.message else "session error" + raise RuntimeError(err_msg) # Find session.idle and get last assistant message before it if already_idle: @@ -156,9 +163,11 @@ def on_event(event): if event.type.value == event_type: result_future.set_result(event) - elif event.type.value == "session.error": - msg = event.data.message if event.data.message else "session error" - result_future.set_exception(RuntimeError(msg)) + else: + match event.data: + case SessionErrorData() as data: + msg = data.message if data.message else "session error" + result_future.set_exception(RuntimeError(msg)) unsubscribe = session.on(on_event) diff --git a/python/samples/chat.py b/python/samples/chat.py index 890191b19..2e48c7ed5 100644 --- a/python/samples/chat.py +++ b/python/samples/chat.py @@ -1,6 +1,11 @@ import asyncio from copilot import CopilotClient +from copilot.generated.session_events import ( + AssistantMessageData, + AssistantReasoningData, + ToolExecutionStartData, +) from copilot.session import PermissionHandler BLUE = "\033[34m" @@ -14,10 +19,11 @@ async def main(): def on_event(event): output = None - if event.type.value == "assistant.reasoning": - output = f"[reasoning: {event.data.content}]" - elif event.type.value == "tool.execution_start": - output = f"[tool: {event.data.tool_name}]" + match event.data: + case AssistantReasoningData() as data: + output = f"[reasoning: {data.content}]" + case ToolExecutionStartData() as data: + output = f"[tool: {data.tool_name}]" if output: print(f"{BLUE}{output}{RESET}") @@ -32,7 +38,12 @@ def on_event(event): print() reply = await session.send_and_wait(user_input) - print(f"\nAssistant: {reply.data.content if reply else None}\n") + assistant_output = None + if reply: + match reply.data: + case AssistantMessageData() as data: + assistant_output = data.content + print(f"\nAssistant: {assistant_output}\n") if __name__ == "__main__": From e6d65d95fe3f9202c9613d3f2a33b28d89016a44 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 13 Apr 2026 22:26:51 -0400 Subject: [PATCH 10/14] Fix Python SDK Ruff failures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/copilot/session.py | 7 ++++--- python/e2e/test_session_fs.py | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/python/copilot/session.py b/python/copilot/session.py index b6d8df8e5..443cfc969 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -52,9 +52,8 @@ ExternalToolRequestedData, PermissionRequest, PermissionRequestedData, - SessionEvent, SessionErrorData, - SessionEventType, + SessionEvent, SessionIdleData, session_event_from_dict, ) @@ -1274,7 +1273,9 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: return # This client doesn't handle permissions; another client will. asyncio.ensure_future( - self._execute_permission_and_respond(request_id, permission_request, perm_handler) + self._execute_permission_and_respond( + request_id, permission_request, perm_handler + ) ) case CommandExecuteData() as data: diff --git a/python/e2e/test_session_fs.py b/python/e2e/test_session_fs.py index f0637b253..bc228707b 100644 --- a/python/e2e/test_session_fs.py +++ b/python/e2e/test_session_fs.py @@ -13,7 +13,6 @@ from copilot import CopilotClient, SessionFsConfig, define_tool from copilot.client import ExternalServerConfig, SubprocessConfig -from copilot.generated.session_events import SessionCompactionCompleteData from copilot.generated.rpc import ( SessionFSExistsResult, SessionFSReaddirResult, @@ -21,7 +20,7 @@ SessionFSReadFileResult, SessionFSStatResult, ) -from copilot.generated.session_events import SessionEvent +from copilot.generated.session_events import SessionCompactionCompleteData, SessionEvent from copilot.session import PermissionHandler from .testharness import E2ETestContext From a98118d8dc051751b1eb93442dbf6c5178191bcf Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 13 Apr 2026 22:53:35 -0400 Subject: [PATCH 11/14] Remove Python session event shims Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/README.md | 10 +- python/copilot/client.py | 8 +- python/copilot/generated/session_events.py | 237 --------------------- python/copilot/session.py | 8 +- python/e2e/test_permissions.py | 20 +- python/test_commands_and_elicitation.py | 8 +- python/test_event_forward_compatibility.py | 57 ++--- scripts/codegen/python.ts | 235 -------------------- 8 files changed, 46 insertions(+), 537 deletions(-) diff --git a/python/README.md b/python/README.md index e54322d7d..c8b3bb00e 100644 --- a/python/README.md +++ b/python/README.md @@ -558,9 +558,11 @@ Provide your own function to inspect each request and apply custom logic (sync o ```python from copilot.session import PermissionRequestResult -from copilot.generated.session_events import PermissionRequest +from copilot.generated.session_events import PermissionRequestedDataPermissionRequest -def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult: +def on_permission_request( + request: PermissionRequestedDataPermissionRequest, invocation: dict +) -> PermissionRequestResult: # request.kind — what type of operation is being requested: # "shell" — executing a shell command # "write" — writing or editing a file @@ -590,7 +592,9 @@ session = await client.create_session( Async handlers are also supported: ```python -async def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult: +async def on_permission_request( + request: PermissionRequestedDataPermissionRequest, invocation: dict +) -> PermissionRequestResult: # Simulate an async approval check (e.g., prompting a user over a network) await asyncio.sleep(0) return PermissionRequestResult(kind="approved") diff --git a/python/copilot/client.py b/python/copilot/client.py index c47acdf14..94fcac23d 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -37,7 +37,11 @@ ServerRpc, register_client_session_api_handlers, ) -from .generated.session_events import PermissionRequest, SessionEvent, session_event_from_dict +from .generated.session_events import ( + PermissionRequestedDataPermissionRequest, + SessionEvent, + session_event_from_dict, +) from .session import ( CommandDefinition, CopilotSession, @@ -2629,7 +2633,7 @@ async def _handle_permission_request_v2(self, params: dict) -> dict: raise ValueError(f"unknown session {session_id}") try: - perm_request = PermissionRequest.from_dict(permission_request) + perm_request = PermissionRequestedDataPermissionRequest.from_dict(permission_request) result = await session._handle_permission_request(perm_request) if result.kind == "no-result": raise ValueError(NO_RESULT_PERMISSION_V2_ERROR) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 9418b6bde..7ff26d0ad 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -4491,240 +4491,3 @@ def session_event_from_dict(s: Any) -> SessionEvent: def session_event_to_dict(x: SessionEvent) -> Any: return x.to_dict() - - -# Compatibility shims for pre-refactor top-level generated types. -class RequestedSchemaType(str, Enum): - OBJECT = "object" - - -@dataclass -class ErrorClass: - """Backward-compatible shim for generic error payloads.""" - message: str - code: str | None = None - stack: str | None = None - - @staticmethod - def from_dict(obj: Any) -> "ErrorClass": - assert isinstance(obj, dict) - message = from_str(obj.get("message")) - code = from_union([from_none, lambda x: from_str(x)], obj.get("code")) - stack = from_union([from_none, lambda x: from_str(x)], obj.get("stack")) - return ErrorClass( - message=message, - code=code, - stack=stack, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["message"] = from_str(self.message) - if self.code is not None: - result["code"] = from_union([from_none, lambda x: from_str(x)], self.code) - if self.stack is not None: - result["stack"] = from_union([from_none, lambda x: from_str(x)], self.stack) - return result - - -@dataclass -class Resource: - """Backward-compatible shim for embedded tool result resources.""" - uri: str - mime_type: str | None = None - text: str | None = None - blob: str | None = None - - @staticmethod - def from_dict(obj: Any) -> "Resource": - assert isinstance(obj, dict) - uri = from_str(obj.get("uri")) - mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType")) - text = from_union([from_none, lambda x: from_str(x)], obj.get("text")) - blob = from_union([from_none, lambda x: from_str(x)], obj.get("blob")) - return Resource( - uri=uri, - mime_type=mime_type, - text=text, - blob=blob, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["uri"] = from_str(self.uri) - if self.mime_type is not None: - result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type) - if self.text is not None: - result["text"] = from_union([from_none, lambda x: from_str(x)], self.text) - if self.blob is not None: - result["blob"] = from_union([from_none, lambda x: from_str(x)], self.blob) - return result - - -ContentType = ToolExecutionCompleteDataResultContentsItemType -Theme = ToolExecutionCompleteDataResultContentsItemIconsItemTheme -Icon = ToolExecutionCompleteDataResultContentsItemIconsItem -ResultKind = PermissionCompletedDataResultKind - - -@dataclass -class ContentElement: - """Backward-compatible shim for tool result content blocks.""" - type: ContentType - text: str | None = None - cwd: str | None = None - exit_code: float | None = None - data: str | None = None - mime_type: str | None = None - description: str | None = None - icons: list[Icon] | None = None - name: str | None = None - size: float | None = None - title: str | None = None - uri: str | None = None - resource: Resource | None = None - - @staticmethod - def from_dict(obj: Any) -> "ContentElement": - assert isinstance(obj, dict) - type = parse_enum(ContentType, obj.get("type")) - text = from_union([from_none, lambda x: from_str(x)], obj.get("text")) - cwd = from_union([from_none, lambda x: from_str(x)], obj.get("cwd")) - exit_code = from_union([from_none, lambda x: from_float(x)], obj.get("exitCode")) - data = from_union([from_none, lambda x: from_str(x)], obj.get("data")) - mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType")) - description = from_union([from_none, lambda x: from_str(x)], obj.get("description")) - icons = from_union([from_none, lambda x: from_list(Icon.from_dict, x)], obj.get("icons")) - name = from_union([from_none, lambda x: from_str(x)], obj.get("name")) - size = from_union([from_none, lambda x: from_float(x)], obj.get("size")) - title = from_union([from_none, lambda x: from_str(x)], obj.get("title")) - uri = from_union([from_none, lambda x: from_str(x)], obj.get("uri")) - resource = from_union([from_none, lambda x: Resource.from_dict(x)], obj.get("resource")) - return ContentElement( - type=type, - text=text, - cwd=cwd, - exit_code=exit_code, - data=data, - mime_type=mime_type, - description=description, - icons=icons, - name=name, - size=size, - title=title, - uri=uri, - resource=resource, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["type"] = to_enum(ContentType, self.type) - if self.text is not None: - result["text"] = from_union([from_none, lambda x: from_str(x)], self.text) - if self.cwd is not None: - result["cwd"] = from_union([from_none, lambda x: from_str(x)], self.cwd) - if self.exit_code is not None: - result["exitCode"] = from_union([from_none, lambda x: to_float(x)], self.exit_code) - if self.data is not None: - result["data"] = from_union([from_none, lambda x: from_str(x)], self.data) - if self.mime_type is not None: - result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type) - if self.description is not None: - result["description"] = from_union([from_none, lambda x: from_str(x)], self.description) - if self.icons is not None: - result["icons"] = from_union([from_none, lambda x: from_list(lambda x: to_class(Icon, x), x)], self.icons) - if self.name is not None: - result["name"] = from_union([from_none, lambda x: from_str(x)], self.name) - if self.size is not None: - result["size"] = from_union([from_none, lambda x: to_float(x)], self.size) - if self.title is not None: - result["title"] = from_union([from_none, lambda x: from_str(x)], self.title) - if self.uri is not None: - result["uri"] = from_union([from_none, lambda x: from_str(x)], self.uri) - if self.resource is not None: - result["resource"] = from_union([from_none, lambda x: to_class(Resource, x)], self.resource) - return result - - -@dataclass -class Result: - """Backward-compatible shim for generic result payloads.""" - content: str | None = None - contents: list[ContentElement] | None = None - detailed_content: str | None = None - kind: ResultKind | None = None - - @staticmethod - def from_dict(obj: Any) -> "Result": - assert isinstance(obj, dict) - content = from_union([from_none, lambda x: from_str(x)], obj.get("content")) - contents = from_union([from_none, lambda x: from_list(ContentElement.from_dict, x)], obj.get("contents")) - detailed_content = from_union([from_none, lambda x: from_str(x)], obj.get("detailedContent")) - kind = from_union([from_none, lambda x: parse_enum(ResultKind, x)], obj.get("kind")) - return Result( - content=content, - contents=contents, - detailed_content=detailed_content, - kind=kind, - ) - - def to_dict(self) -> dict: - result: dict = {} - if self.content is not None: - result["content"] = from_union([from_none, lambda x: from_str(x)], self.content) - if self.contents is not None: - result["contents"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ContentElement, x), x)], self.contents) - if self.detailed_content is not None: - result["detailedContent"] = from_union([from_none, lambda x: from_str(x)], self.detailed_content) - if self.kind is not None: - result["kind"] = from_union([from_none, lambda x: to_enum(ResultKind, x)], self.kind) - return result - - -# Convenience aliases for commonly used nested event types. -Action = ElicitationCompletedDataAction -Agent = SessionCustomAgentsUpdatedDataAgentsItem -AgentMode = UserMessageDataAgentMode -Attachment = UserMessageDataAttachmentsItem -AttachmentType = UserMessageDataAttachmentsItemType -CodeChanges = SessionShutdownDataCodeChanges -CompactionTokensUsed = SessionCompactionCompleteDataCompactionTokensUsed -ContextClass = SessionStartDataContext -CopilotUsage = AssistantUsageDataCopilotUsage -DataCommand = CommandsChangedDataCommandsItem -End = UserMessageDataAttachmentsItemSelectionEnd -Extension = SessionExtensionsLoadedDataExtensionsItem -ExtensionStatus = SessionExtensionsLoadedDataExtensionsItemStatus -HostType = SessionStartDataContextHostType -KindClass = SystemNotificationDataKind -KindStatus = SystemNotificationDataKindStatus -KindType = SystemNotificationDataKindType -LineRange = UserMessageDataAttachmentsItemLineRange -Metadata = SystemMessageDataMetadata -Mode = ElicitationRequestedDataMode -ModelMetric = SessionShutdownDataModelMetricsValue -Operation = SessionPlanChangedDataOperation -PermissionRequest = PermissionRequestedDataPermissionRequest -PermissionRequestKind = PermissionRequestedDataPermissionRequestKind -PermissionRequestCommand = PermissionRequestedDataPermissionRequestCommandsItem -PossibleURL = PermissionRequestedDataPermissionRequestPossibleUrlsItem -QuotaSnapshot = AssistantUsageDataQuotaSnapshotsValue -ReferenceType = UserMessageDataAttachmentsItemReferenceType -RepositoryClass = SessionHandoffDataRepository -RequestedSchema = ElicitationRequestedDataRequestedSchema -Requests = SessionShutdownDataModelMetricsValueRequests -Role = SystemMessageDataRole -Selection = UserMessageDataAttachmentsItemSelection -Server = SessionMcpServersLoadedDataServersItem -ServerStatus = SessionMcpServersLoadedDataServersItemStatus -ShutdownType = SessionShutdownDataShutdownType -Skill = SessionSkillsLoadedDataSkillsItem -Source = SessionExtensionsLoadedDataExtensionsItemSource -SourceType = SessionHandoffDataSourceType -Start = UserMessageDataAttachmentsItemSelectionStart -StaticClientConfig = McpOauthRequiredDataStaticClientConfig -TokenDetail = AssistantUsageDataCopilotUsageTokenDetailsItem -ToolRequest = AssistantMessageDataToolRequestsItem -ToolRequestType = AssistantMessageDataToolRequestsItemType -UI = CapabilitiesChangedDataUi -Usage = SessionShutdownDataModelMetricsValueUsage diff --git a/python/copilot/session.py b/python/copilot/session.py index 443cfc969..5f664b9ce 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -50,8 +50,8 @@ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, - PermissionRequest, PermissionRequestedData, + PermissionRequestedDataPermissionRequest, SessionErrorData, SessionEvent, SessionIdleData, @@ -242,7 +242,7 @@ class PermissionRequestResult: _PermissionHandlerFn = Callable[ - [PermissionRequest, dict[str, str]], + [PermissionRequestedDataPermissionRequest, dict[str, str]], PermissionRequestResult | Awaitable[PermissionRequestResult], ] @@ -250,7 +250,7 @@ class PermissionRequestResult: class PermissionHandler: @staticmethod def approve_all( - request: PermissionRequest, invocation: dict[str, str] + request: PermissionRequestedDataPermissionRequest, invocation: dict[str, str] ) -> PermissionRequestResult: return PermissionRequestResult(kind="approved") @@ -1625,7 +1625,7 @@ def _register_permission_handler(self, handler: _PermissionHandlerFn | None) -> self._permission_handler = handler async def _handle_permission_request( - self, request: PermissionRequest + self, request: PermissionRequestedDataPermissionRequest ) -> PermissionRequestResult: """ Handle a permission request from the Copilot CLI. diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index 31f2230fe..5612161c6 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -6,8 +6,12 @@ import pytest -from copilot.generated.session_events import SessionIdleData, ToolExecutionCompleteData -from copilot.session import PermissionHandler, PermissionRequest, PermissionRequestResult +from copilot.generated.session_events import ( + PermissionRequestedDataPermissionRequest, + SessionIdleData, + ToolExecutionCompleteData, +) +from copilot.session import PermissionHandler, PermissionRequestResult from .testharness import E2ETestContext from .testharness.helper import read_file, write_file @@ -21,7 +25,7 @@ async def test_should_invoke_permission_handler_for_write_operations(self, ctx: permission_requests = [] def on_permission_request( - request: PermissionRequest, invocation: dict + request: PermissionRequestedDataPermissionRequest, invocation: dict ) -> PermissionRequestResult: permission_requests.append(request) assert invocation["session_id"] == session.session_id @@ -46,7 +50,7 @@ async def test_should_deny_permission_when_handler_returns_denied(self, ctx: E2E """Test denying permissions""" def on_permission_request( - request: PermissionRequest, invocation: dict + request: PermissionRequestedDataPermissionRequest, invocation: dict ) -> PermissionRequestResult: return PermissionRequestResult(kind="denied-interactively-by-user") @@ -158,7 +162,7 @@ async def test_should_handle_async_permission_handler(self, ctx: E2ETestContext) permission_requests = [] async def on_permission_request( - request: PermissionRequest, invocation: dict + request: PermissionRequestedDataPermissionRequest, invocation: dict ) -> PermissionRequestResult: permission_requests.append(request) # Simulate async permission check (e.g., user prompt) @@ -186,7 +190,7 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC # Resume with permission handler def on_permission_request( - request: PermissionRequest, invocation: dict + request: PermissionRequestedDataPermissionRequest, invocation: dict ) -> PermissionRequestResult: permission_requests.append(request) return PermissionRequestResult(kind="approved") @@ -206,7 +210,7 @@ async def test_should_handle_permission_handler_errors_gracefully(self, ctx: E2E """Test that permission handler errors are handled gracefully""" def on_permission_request( - request: PermissionRequest, invocation: dict + request: PermissionRequestedDataPermissionRequest, invocation: dict ) -> PermissionRequestResult: raise RuntimeError("Handler error") @@ -226,7 +230,7 @@ async def test_should_receive_toolcallid_in_permission_requests(self, ctx: E2ETe received_tool_call_id = False def on_permission_request( - request: PermissionRequest, invocation: dict + request: PermissionRequestedDataPermissionRequest, invocation: dict ) -> PermissionRequestResult: nonlocal received_tool_call_id if request.tool_call_id: diff --git a/python/test_commands_and_elicitation.py b/python/test_commands_and_elicitation.py index bab9d0b98..5c5edc763 100644 --- a/python/test_commands_and_elicitation.py +++ b/python/test_commands_and_elicitation.py @@ -579,7 +579,7 @@ async def mock_request(method, params): from copilot.generated.session_events import ( ElicitationRequestedData, - RequestedSchema, + ElicitationRequestedDataRequestedSchema, SessionEvent, SessionEventType, ) @@ -588,7 +588,7 @@ async def mock_request(method, params): data=ElicitationRequestedData( request_id="req-schema-1", message="Fill in your details", - requested_schema=RequestedSchema( + requested_schema=ElicitationRequestedDataRequestedSchema( type="object", properties={ "name": {"type": "string"}, @@ -637,14 +637,14 @@ async def test_capabilities_changed_event_updates_session(self): session._set_capabilities({}) from copilot.generated.session_events import ( - UI, CapabilitiesChangedData, + CapabilitiesChangedDataUi, SessionEvent, SessionEventType, ) event = SessionEvent( - data=CapabilitiesChangedData(ui=UI(elicitation=True)), + data=CapabilitiesChangedData(ui=CapabilitiesChangedDataUi(elicitation=True)), id="evt-cap-1", timestamp="2025-01-01T00:00:00Z", type=SessionEventType.CAPABILITIES_CHANGED, diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 4825be3f5..9447d3fbd 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -13,21 +13,16 @@ import pytest from copilot.generated.session_events import ( - Action, - AgentMode, - ContentElement, Data, - Mode, + ElicitationCompletedDataAction, + ElicitationRequestedDataMode, + ElicitationRequestedDataRequestedSchema, PermissionRequestedDataPermissionRequest, PermissionRequestedDataPermissionRequestAction, - ReferenceType, - RequestedSchema, - RequestedSchemaType, - Resource, - Result, - ResultKind, SessionEventType, SessionTaskCompleteData, + UserMessageDataAgentMode, + UserMessageDataAttachmentsItemReferenceType, session_event_from_dict, ) @@ -80,44 +75,18 @@ def test_malformed_timestamp_raises_error(self): with pytest.raises((ValueError, TypeError)): session_event_from_dict(malformed_event) - def test_legacy_top_level_generated_symbols_remain_available(self): - """Previously top-level generated helper symbols should remain importable.""" - assert Action.ACCEPT.value == "accept" - assert AgentMode.INTERACTIVE.value == "interactive" - assert Mode.FORM.value == "form" - assert ReferenceType.PR.value == "pr" + def test_explicit_generated_symbols_remain_available(self): + """Explicit generated helper symbols should remain importable.""" + assert ElicitationCompletedDataAction.ACCEPT.value == "accept" + assert UserMessageDataAgentMode.INTERACTIVE.value == "interactive" + assert ElicitationRequestedDataMode.FORM.value == "form" + assert UserMessageDataAttachmentsItemReferenceType.PR.value == "pr" - schema = RequestedSchema( - properties={"answer": {"type": "string"}}, type=RequestedSchemaType.OBJECT + schema = ElicitationRequestedDataRequestedSchema( + properties={"answer": {"type": "string"}}, type="object" ) assert schema.to_dict()["type"] == "object" - result = Result( - content="Approved", - kind=ResultKind.APPROVED, - contents=[ - ContentElement( - type=ContentElement.from_dict({"type": "text", "text": "hello"}).type, - text="hello", - resource=Resource(uri="file://artifact.txt", text="artifact"), - ) - ], - ) - assert result.to_dict() == { - "content": "Approved", - "kind": "approved", - "contents": [ - { - "type": "text", - "text": "hello", - "resource": { - "uri": "file://artifact.txt", - "text": "artifact", - }, - } - ], - } - def test_data_shim_preserves_raw_mapping_values(self): """Compatibility Data should keep arbitrary nested mappings as plain dicts.""" parsed = Data.from_dict( diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index e75279186..b5aba76b6 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -1187,241 +1187,6 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(` return x.to_dict()`); out.push(``); out.push(``); - out.push(`# Compatibility shims for pre-refactor top-level generated types.`); - out.push(`class RequestedSchemaType(str, Enum):`); - out.push(` OBJECT = "object"`); - out.push(``); - out.push(``); - out.push(`@dataclass`); - out.push(`class ErrorClass:`); - out.push(` """Backward-compatible shim for generic error payloads."""`); - out.push(` message: str`); - out.push(` code: str | None = None`); - out.push(` stack: str | None = None`); - out.push(``); - out.push(` @staticmethod`); - out.push(` def from_dict(obj: Any) -> "ErrorClass":`); - out.push(` assert isinstance(obj, dict)`); - out.push(` message = from_str(obj.get("message"))`); - out.push(` code = from_union([from_none, lambda x: from_str(x)], obj.get("code"))`); - out.push(` stack = from_union([from_none, lambda x: from_str(x)], obj.get("stack"))`); - out.push(` return ErrorClass(`); - out.push(` message=message,`); - out.push(` code=code,`); - out.push(` stack=stack,`); - out.push(` )`); - out.push(``); - out.push(` def to_dict(self) -> dict:`); - out.push(` result: dict = {}`); - out.push(` result["message"] = from_str(self.message)`); - out.push(` if self.code is not None:`); - out.push(` result["code"] = from_union([from_none, lambda x: from_str(x)], self.code)`); - out.push(` if self.stack is not None:`); - out.push(` result["stack"] = from_union([from_none, lambda x: from_str(x)], self.stack)`); - out.push(` return result`); - out.push(``); - out.push(``); - out.push(`@dataclass`); - out.push(`class Resource:`); - out.push(` """Backward-compatible shim for embedded tool result resources."""`); - out.push(` uri: str`); - out.push(` mime_type: str | None = None`); - out.push(` text: str | None = None`); - out.push(` blob: str | None = None`); - out.push(``); - out.push(` @staticmethod`); - out.push(` def from_dict(obj: Any) -> "Resource":`); - out.push(` assert isinstance(obj, dict)`); - out.push(` uri = from_str(obj.get("uri"))`); - out.push(` mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType"))`); - out.push(` text = from_union([from_none, lambda x: from_str(x)], obj.get("text"))`); - out.push(` blob = from_union([from_none, lambda x: from_str(x)], obj.get("blob"))`); - out.push(` return Resource(`); - out.push(` uri=uri,`); - out.push(` mime_type=mime_type,`); - out.push(` text=text,`); - out.push(` blob=blob,`); - out.push(` )`); - out.push(``); - out.push(` def to_dict(self) -> dict:`); - out.push(` result: dict = {}`); - out.push(` result["uri"] = from_str(self.uri)`); - out.push(` if self.mime_type is not None:`); - out.push(` result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type)`); - out.push(` if self.text is not None:`); - out.push(` result["text"] = from_union([from_none, lambda x: from_str(x)], self.text)`); - out.push(` if self.blob is not None:`); - out.push(` result["blob"] = from_union([from_none, lambda x: from_str(x)], self.blob)`); - out.push(` return result`); - out.push(``); - out.push(``); - out.push(`ContentType = ToolExecutionCompleteDataResultContentsItemType`); - out.push(`Theme = ToolExecutionCompleteDataResultContentsItemIconsItemTheme`); - out.push(`Icon = ToolExecutionCompleteDataResultContentsItemIconsItem`); - out.push(`ResultKind = PermissionCompletedDataResultKind`); - out.push(``); - out.push(``); - out.push(`@dataclass`); - out.push(`class ContentElement:`); - out.push(` """Backward-compatible shim for tool result content blocks."""`); - out.push(` type: ContentType`); - out.push(` text: str | None = None`); - out.push(` cwd: str | None = None`); - out.push(` exit_code: float | None = None`); - out.push(` data: str | None = None`); - out.push(` mime_type: str | None = None`); - out.push(` description: str | None = None`); - out.push(` icons: list[Icon] | None = None`); - out.push(` name: str | None = None`); - out.push(` size: float | None = None`); - out.push(` title: str | None = None`); - out.push(` uri: str | None = None`); - out.push(` resource: Resource | None = None`); - out.push(``); - out.push(` @staticmethod`); - out.push(` def from_dict(obj: Any) -> "ContentElement":`); - out.push(` assert isinstance(obj, dict)`); - out.push(` type = parse_enum(ContentType, obj.get("type"))`); - out.push(` text = from_union([from_none, lambda x: from_str(x)], obj.get("text"))`); - out.push(` cwd = from_union([from_none, lambda x: from_str(x)], obj.get("cwd"))`); - out.push(` exit_code = from_union([from_none, lambda x: from_float(x)], obj.get("exitCode"))`); - out.push(` data = from_union([from_none, lambda x: from_str(x)], obj.get("data"))`); - out.push(` mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType"))`); - out.push(` description = from_union([from_none, lambda x: from_str(x)], obj.get("description"))`); - out.push(` icons = from_union([from_none, lambda x: from_list(Icon.from_dict, x)], obj.get("icons"))`); - out.push(` name = from_union([from_none, lambda x: from_str(x)], obj.get("name"))`); - out.push(` size = from_union([from_none, lambda x: from_float(x)], obj.get("size"))`); - out.push(` title = from_union([from_none, lambda x: from_str(x)], obj.get("title"))`); - out.push(` uri = from_union([from_none, lambda x: from_str(x)], obj.get("uri"))`); - out.push(` resource = from_union([from_none, lambda x: Resource.from_dict(x)], obj.get("resource"))`); - out.push(` return ContentElement(`); - out.push(` type=type,`); - out.push(` text=text,`); - out.push(` cwd=cwd,`); - out.push(` exit_code=exit_code,`); - out.push(` data=data,`); - out.push(` mime_type=mime_type,`); - out.push(` description=description,`); - out.push(` icons=icons,`); - out.push(` name=name,`); - out.push(` size=size,`); - out.push(` title=title,`); - out.push(` uri=uri,`); - out.push(` resource=resource,`); - out.push(` )`); - out.push(``); - out.push(` def to_dict(self) -> dict:`); - out.push(` result: dict = {}`); - out.push(` result["type"] = to_enum(ContentType, self.type)`); - out.push(` if self.text is not None:`); - out.push(` result["text"] = from_union([from_none, lambda x: from_str(x)], self.text)`); - out.push(` if self.cwd is not None:`); - out.push(` result["cwd"] = from_union([from_none, lambda x: from_str(x)], self.cwd)`); - out.push(` if self.exit_code is not None:`); - out.push(` result["exitCode"] = from_union([from_none, lambda x: to_float(x)], self.exit_code)`); - out.push(` if self.data is not None:`); - out.push(` result["data"] = from_union([from_none, lambda x: from_str(x)], self.data)`); - out.push(` if self.mime_type is not None:`); - out.push(` result["mimeType"] = from_union([from_none, lambda x: from_str(x)], self.mime_type)`); - out.push(` if self.description is not None:`); - out.push(` result["description"] = from_union([from_none, lambda x: from_str(x)], self.description)`); - out.push(` if self.icons is not None:`); - out.push(` result["icons"] = from_union([from_none, lambda x: from_list(lambda x: to_class(Icon, x), x)], self.icons)`); - out.push(` if self.name is not None:`); - out.push(` result["name"] = from_union([from_none, lambda x: from_str(x)], self.name)`); - out.push(` if self.size is not None:`); - out.push(` result["size"] = from_union([from_none, lambda x: to_float(x)], self.size)`); - out.push(` if self.title is not None:`); - out.push(` result["title"] = from_union([from_none, lambda x: from_str(x)], self.title)`); - out.push(` if self.uri is not None:`); - out.push(` result["uri"] = from_union([from_none, lambda x: from_str(x)], self.uri)`); - out.push(` if self.resource is not None:`); - out.push(` result["resource"] = from_union([from_none, lambda x: to_class(Resource, x)], self.resource)`); - out.push(` return result`); - out.push(``); - out.push(``); - out.push(`@dataclass`); - out.push(`class Result:`); - out.push(` """Backward-compatible shim for generic result payloads."""`); - out.push(` content: str | None = None`); - out.push(` contents: list[ContentElement] | None = None`); - out.push(` detailed_content: str | None = None`); - out.push(` kind: ResultKind | None = None`); - out.push(``); - out.push(` @staticmethod`); - out.push(` def from_dict(obj: Any) -> "Result":`); - out.push(` assert isinstance(obj, dict)`); - out.push(` content = from_union([from_none, lambda x: from_str(x)], obj.get("content"))`); - out.push(` contents = from_union([from_none, lambda x: from_list(ContentElement.from_dict, x)], obj.get("contents"))`); - out.push(` detailed_content = from_union([from_none, lambda x: from_str(x)], obj.get("detailedContent"))`); - out.push(` kind = from_union([from_none, lambda x: parse_enum(ResultKind, x)], obj.get("kind"))`); - out.push(` return Result(`); - out.push(` content=content,`); - out.push(` contents=contents,`); - out.push(` detailed_content=detailed_content,`); - out.push(` kind=kind,`); - out.push(` )`); - out.push(``); - out.push(` def to_dict(self) -> dict:`); - out.push(` result: dict = {}`); - out.push(` if self.content is not None:`); - out.push(` result["content"] = from_union([from_none, lambda x: from_str(x)], self.content)`); - out.push(` if self.contents is not None:`); - out.push(` result["contents"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ContentElement, x), x)], self.contents)`); - out.push(` if self.detailed_content is not None:`); - out.push(` result["detailedContent"] = from_union([from_none, lambda x: from_str(x)], self.detailed_content)`); - out.push(` if self.kind is not None:`); - out.push(` result["kind"] = from_union([from_none, lambda x: to_enum(ResultKind, x)], self.kind)`); - out.push(` return result`); - out.push(``); - out.push(``); - out.push(`# Convenience aliases for commonly used nested event types.`); - out.push(`Action = ElicitationCompletedDataAction`); - out.push(`Agent = SessionCustomAgentsUpdatedDataAgentsItem`); - out.push(`AgentMode = UserMessageDataAgentMode`); - out.push(`Attachment = UserMessageDataAttachmentsItem`); - out.push(`AttachmentType = UserMessageDataAttachmentsItemType`); - out.push(`CodeChanges = SessionShutdownDataCodeChanges`); - out.push(`CompactionTokensUsed = SessionCompactionCompleteDataCompactionTokensUsed`); - out.push(`ContextClass = SessionStartDataContext`); - out.push(`CopilotUsage = AssistantUsageDataCopilotUsage`); - out.push(`DataCommand = CommandsChangedDataCommandsItem`); - out.push(`End = UserMessageDataAttachmentsItemSelectionEnd`); - out.push(`Extension = SessionExtensionsLoadedDataExtensionsItem`); - out.push(`ExtensionStatus = SessionExtensionsLoadedDataExtensionsItemStatus`); - out.push(`HostType = SessionStartDataContextHostType`); - out.push(`KindClass = SystemNotificationDataKind`); - out.push(`KindStatus = SystemNotificationDataKindStatus`); - out.push(`KindType = SystemNotificationDataKindType`); - out.push(`LineRange = UserMessageDataAttachmentsItemLineRange`); - out.push(`Metadata = SystemMessageDataMetadata`); - out.push(`Mode = ElicitationRequestedDataMode`); - out.push(`ModelMetric = SessionShutdownDataModelMetricsValue`); - out.push(`Operation = SessionPlanChangedDataOperation`); - out.push(`PermissionRequest = PermissionRequestedDataPermissionRequest`); - out.push(`PermissionRequestKind = PermissionRequestedDataPermissionRequestKind`); - out.push(`PermissionRequestCommand = PermissionRequestedDataPermissionRequestCommandsItem`); - out.push(`PossibleURL = PermissionRequestedDataPermissionRequestPossibleUrlsItem`); - out.push(`QuotaSnapshot = AssistantUsageDataQuotaSnapshotsValue`); - out.push(`ReferenceType = UserMessageDataAttachmentsItemReferenceType`); - out.push(`RepositoryClass = SessionHandoffDataRepository`); - out.push(`RequestedSchema = ElicitationRequestedDataRequestedSchema`); - out.push(`Requests = SessionShutdownDataModelMetricsValueRequests`); - out.push(`Role = SystemMessageDataRole`); - out.push(`Selection = UserMessageDataAttachmentsItemSelection`); - out.push(`Server = SessionMcpServersLoadedDataServersItem`); - out.push(`ServerStatus = SessionMcpServersLoadedDataServersItemStatus`); - out.push(`ShutdownType = SessionShutdownDataShutdownType`); - out.push(`Skill = SessionSkillsLoadedDataSkillsItem`); - out.push(`Source = SessionExtensionsLoadedDataExtensionsItemSource`); - out.push(`SourceType = SessionHandoffDataSourceType`); - out.push(`Start = UserMessageDataAttachmentsItemSelectionStart`); - out.push(`StaticClientConfig = McpOauthRequiredDataStaticClientConfig`); - out.push(`TokenDetail = AssistantUsageDataCopilotUsageTokenDetailsItem`); - out.push(`ToolRequest = AssistantMessageDataToolRequestsItem`); - out.push(`ToolRequestType = AssistantMessageDataToolRequestsItemType`); - out.push(`UI = CapabilitiesChangedDataUi`); - out.push(`Usage = SessionShutdownDataModelMetricsValueUsage`); return out.join("\n"); } From 698dc0dd5b3165bd164d04119329efcf0cb7e191 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 14 Apr 2026 09:23:40 -0400 Subject: [PATCH 12/14] Preserve Python event helper types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/test/python-codegen.test.ts | 235 ++++++++++ python/README.md | 6 +- python/copilot/client.py | 4 +- python/copilot/generated/session_events.py | 473 +++++++++++---------- python/copilot/session.py | 8 +- python/e2e/test_permissions.py | 14 +- python/test_commands_and_elicitation.py | 8 +- python/test_event_forward_compatibility.py | 30 +- scripts/codegen/python.ts | 63 ++- 9 files changed, 575 insertions(+), 266 deletions(-) diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/python-codegen.test.ts index c47284e17..ab4e2ff37 100644 --- a/nodejs/test/python-codegen.test.ts +++ b/nodejs/test/python-codegen.test.ts @@ -82,4 +82,239 @@ describe("python session event codegen", () => { expect(code).toContain("encoded: str"); expect(code).toContain("count: int"); }); + + it("preserves key shortened nested type names", () => { + const schema: JSONSchema7 = { + definitions: { + SessionEvent: { + anyOf: [ + { + type: "object", + required: ["type", "data"], + properties: { + type: { const: "permission.requested" }, + data: { + type: "object", + required: ["requestId", "permissionRequest"], + properties: { + requestId: { type: "string" }, + permissionRequest: { + anyOf: [ + { + type: "object", + required: [ + "kind", + "fullCommandText", + "intention", + "commands", + "possiblePaths", + "possibleUrls", + "hasWriteFileRedirection", + "canOfferSessionApproval", + ], + properties: { + kind: { const: "shell", type: "string" }, + fullCommandText: { type: "string" }, + intention: { type: "string" }, + commands: { + type: "array", + items: { + type: "object", + required: ["identifier", "readOnly"], + properties: { + identifier: { type: "string" }, + readOnly: { type: "boolean" }, + }, + }, + }, + possiblePaths: { + type: "array", + items: { type: "string" }, + }, + possibleUrls: { + type: "array", + items: { + type: "object", + required: ["url"], + properties: { url: { type: "string" } }, + }, + }, + hasWriteFileRedirection: { type: "boolean" }, + canOfferSessionApproval: { type: "boolean" }, + }, + }, + { + type: "object", + required: ["kind", "fact"], + properties: { + kind: { const: "memory", type: "string" }, + fact: { type: "string" }, + action: { + type: "string", + enum: ["store", "vote"], + default: "store", + }, + direction: { + type: "string", + enum: ["upvote", "downvote"], + }, + }, + }, + ], + }, + }, + }, + }, + }, + { + type: "object", + required: ["type", "data"], + properties: { + type: { const: "elicitation.requested" }, + data: { + type: "object", + properties: { + requestedSchema: { + type: "object", + required: ["type", "properties"], + properties: { + type: { const: "object", type: "string" }, + properties: { + type: "object", + additionalProperties: {}, + }, + }, + }, + mode: { + type: "string", + enum: ["form", "url"], + }, + }, + }, + }, + }, + { + type: "object", + required: ["type", "data"], + properties: { + type: { const: "capabilities.changed" }, + data: { + type: "object", + properties: { + ui: { + type: "object", + properties: { + elicitation: { type: "boolean" }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }; + + const code = generatePythonSessionEventsCode(schema); + + expect(code).toContain("class PermissionRequest:"); + expect(code).toContain("class PermissionRequestShellCommand:"); + expect(code).toContain("class PermissionRequestShellPossibleURL:"); + expect(code).toContain("class PermissionRequestMemoryAction(Enum):"); + expect(code).toContain("class PermissionRequestMemoryDirection(Enum):"); + expect(code).toContain("class ElicitationRequestedSchema:"); + expect(code).toContain("class ElicitationRequestedMode(Enum):"); + expect(code).toContain("class CapabilitiesChangedUI:"); + expect(code).not.toContain("class PermissionRequestedDataPermissionRequest:"); + expect(code).not.toContain("class ElicitationRequestedDataRequestedSchema:"); + expect(code).not.toContain("class CapabilitiesChangedDataUi:"); + }); + + it("keeps distinct enum types even when they share the same values", () => { + const schema: JSONSchema7 = { + definitions: { + SessionEvent: { + anyOf: [ + { + type: "object", + required: ["type", "data"], + properties: { + type: { const: "assistant.message" }, + data: { + type: "object", + properties: { + toolRequests: { + type: "array", + items: { + type: "object", + required: ["toolCallId", "name", "type"], + properties: { + toolCallId: { type: "string" }, + name: { type: "string" }, + type: { + type: "string", + enum: ["function", "custom"], + }, + }, + }, + }, + }, + }, + }, + }, + { + type: "object", + required: ["type", "data"], + properties: { + type: { const: "session.import_legacy" }, + data: { + type: "object", + properties: { + legacySession: { + type: "object", + properties: { + chatMessages: { + type: "array", + items: { + type: "object", + properties: { + toolCalls: { + type: "array", + items: { + type: "object", + properties: { + type: { + type: "string", + enum: [ + "function", + "custom", + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }; + + const code = generatePythonSessionEventsCode(schema); + + expect(code).toContain("class AssistantMessageToolRequestType(Enum):"); + expect(code).toContain("type: AssistantMessageToolRequestType"); + expect(code).toContain("parse_enum(AssistantMessageToolRequestType,"); + expect(code).toContain( + "class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType(Enum):" + ); + }); }); diff --git a/python/README.md b/python/README.md index c8b3bb00e..b65b14736 100644 --- a/python/README.md +++ b/python/README.md @@ -558,10 +558,10 @@ Provide your own function to inspect each request and apply custom logic (sync o ```python from copilot.session import PermissionRequestResult -from copilot.generated.session_events import PermissionRequestedDataPermissionRequest +from copilot.generated.session_events import PermissionRequest def on_permission_request( - request: PermissionRequestedDataPermissionRequest, invocation: dict + request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: # request.kind — what type of operation is being requested: # "shell" — executing a shell command @@ -593,7 +593,7 @@ Async handlers are also supported: ```python async def on_permission_request( - request: PermissionRequestedDataPermissionRequest, invocation: dict + request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: # Simulate an async approval check (e.g., prompting a user over a network) await asyncio.sleep(0) diff --git a/python/copilot/client.py b/python/copilot/client.py index 94fcac23d..f59816d6e 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -38,7 +38,7 @@ register_client_session_api_handlers, ) from .generated.session_events import ( - PermissionRequestedDataPermissionRequest, + PermissionRequest, SessionEvent, session_event_from_dict, ) @@ -2633,7 +2633,7 @@ async def _handle_permission_request_v2(self, params: dict) -> dict: raise ValueError(f"unknown session {session_id}") try: - perm_request = PermissionRequestedDataPermissionRequest.from_dict(permission_request) + perm_request = PermissionRequest.from_dict(permission_request) result = await session._handle_permission_request(perm_request) if result.kind == "no-result": raise ValueError(NO_RESULT_PERMISSION_V2_ERROR) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 7ff26d0ad..c95df5c56 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -367,7 +367,7 @@ class SessionResumeDataContext: cwd: str git_root: str | None = None repository: str | None = None - host_type: SessionStartDataContextHostType | None = None + host_type: SessionResumeDataContextHostType | None = None branch: str | None = None head_commit: str | None = None base_commit: str | None = None @@ -378,7 +378,7 @@ def from_dict(obj: Any) -> "SessionResumeDataContext": cwd = from_str(obj.get("cwd")) git_root = from_union([from_none, lambda x: from_str(x)], obj.get("gitRoot")) repository = from_union([from_none, lambda x: from_str(x)], obj.get("repository")) - host_type = from_union([from_none, lambda x: parse_enum(SessionStartDataContextHostType, x)], obj.get("hostType")) + host_type = from_union([from_none, lambda x: parse_enum(SessionResumeDataContextHostType, x)], obj.get("hostType")) branch = from_union([from_none, lambda x: from_str(x)], obj.get("branch")) head_commit = from_union([from_none, lambda x: from_str(x)], obj.get("headCommit")) base_commit = from_union([from_none, lambda x: from_str(x)], obj.get("baseCommit")) @@ -400,7 +400,7 @@ def to_dict(self) -> dict: if self.repository is not None: result["repository"] = from_union([from_none, lambda x: from_str(x)], self.repository) if self.host_type is not None: - result["hostType"] = from_union([from_none, lambda x: to_enum(SessionStartDataContextHostType, x)], self.host_type) + result["hostType"] = from_union([from_none, lambda x: to_enum(SessionResumeDataContextHostType, x)], self.host_type) if self.branch is not None: result["branch"] = from_union([from_none, lambda x: from_str(x)], self.branch) if self.head_commit is not None: @@ -920,7 +920,7 @@ def to_dict(self) -> dict: @dataclass class SessionImportLegacyData: - """Legacy session import data including the complete session JSON, import timestamp, and source file path""" + "Legacy session import data including the complete session JSON, import timestamp, and source file path" legacy_session: SessionImportLegacyDataLegacySession import_time: datetime source_file: str @@ -946,19 +946,19 @@ def to_dict(self) -> dict: @dataclass -class SessionHandoffDataRepository: +class HandoffRepository: "Repository context for the handed-off session" owner: str name: str branch: str | None = None @staticmethod - def from_dict(obj: Any) -> "SessionHandoffDataRepository": + def from_dict(obj: Any) -> "HandoffRepository": assert isinstance(obj, dict) owner = from_str(obj.get("owner")) name = from_str(obj.get("name")) branch = from_union([from_none, lambda x: from_str(x)], obj.get("branch")) - return SessionHandoffDataRepository( + return HandoffRepository( owner=owner, name=name, branch=branch, @@ -977,8 +977,8 @@ def to_dict(self) -> dict: class SessionHandoffData: "Session handoff metadata including source, context, and repository information" handoff_time: datetime - source_type: SessionHandoffDataSourceType - repository: SessionHandoffDataRepository | None = None + source_type: HandoffSourceType + repository: HandoffRepository | None = None context: str | None = None summary: str | None = None remote_session_id: str | None = None @@ -988,8 +988,8 @@ class SessionHandoffData: def from_dict(obj: Any) -> "SessionHandoffData": assert isinstance(obj, dict) handoff_time = from_datetime(obj.get("handoffTime")) - source_type = parse_enum(SessionHandoffDataSourceType, obj.get("sourceType")) - repository = from_union([from_none, lambda x: SessionHandoffDataRepository.from_dict(x)], obj.get("repository")) + source_type = parse_enum(HandoffSourceType, obj.get("sourceType")) + repository = from_union([from_none, lambda x: HandoffRepository.from_dict(x)], obj.get("repository")) context = from_union([from_none, lambda x: from_str(x)], obj.get("context")) summary = from_union([from_none, lambda x: from_str(x)], obj.get("summary")) remote_session_id = from_union([from_none, lambda x: from_str(x)], obj.get("remoteSessionId")) @@ -1007,9 +1007,9 @@ def from_dict(obj: Any) -> "SessionHandoffData": def to_dict(self) -> dict: result: dict = {} result["handoffTime"] = to_datetime(self.handoff_time) - result["sourceType"] = to_enum(SessionHandoffDataSourceType, self.source_type) + result["sourceType"] = to_enum(HandoffSourceType, self.source_type) if self.repository is not None: - result["repository"] = from_union([from_none, lambda x: to_class(SessionHandoffDataRepository, x)], self.repository) + result["repository"] = from_union([from_none, lambda x: to_class(HandoffRepository, x)], self.repository) if self.context is not None: result["context"] = from_union([from_none, lambda x: from_str(x)], self.context) if self.summary is not None: @@ -1092,19 +1092,19 @@ def to_dict(self) -> dict: @dataclass -class SessionShutdownDataCodeChanges: +class ShutdownCodeChanges: "Aggregate code change metrics for the session" lines_added: float lines_removed: float files_modified: list[str] @staticmethod - def from_dict(obj: Any) -> "SessionShutdownDataCodeChanges": + def from_dict(obj: Any) -> "ShutdownCodeChanges": assert isinstance(obj, dict) lines_added = from_float(obj.get("linesAdded")) lines_removed = from_float(obj.get("linesRemoved")) files_modified = from_list(lambda x: from_str(x), obj.get("filesModified")) - return SessionShutdownDataCodeChanges( + return ShutdownCodeChanges( lines_added=lines_added, lines_removed=lines_removed, files_modified=files_modified, @@ -1119,17 +1119,17 @@ def to_dict(self) -> dict: @dataclass -class SessionShutdownDataModelMetricsValueRequests: +class ShutdownModelMetricRequests: "Request count and cost metrics" count: float cost: float @staticmethod - def from_dict(obj: Any) -> "SessionShutdownDataModelMetricsValueRequests": + def from_dict(obj: Any) -> "ShutdownModelMetricRequests": assert isinstance(obj, dict) count = from_float(obj.get("count")) cost = from_float(obj.get("cost")) - return SessionShutdownDataModelMetricsValueRequests( + return ShutdownModelMetricRequests( count=count, cost=cost, ) @@ -1142,7 +1142,7 @@ def to_dict(self) -> dict: @dataclass -class SessionShutdownDataModelMetricsValueUsage: +class ShutdownModelMetricUsage: "Token usage breakdown" input_tokens: float output_tokens: float @@ -1150,13 +1150,13 @@ class SessionShutdownDataModelMetricsValueUsage: cache_write_tokens: float @staticmethod - def from_dict(obj: Any) -> "SessionShutdownDataModelMetricsValueUsage": + def from_dict(obj: Any) -> "ShutdownModelMetricUsage": assert isinstance(obj, dict) input_tokens = from_float(obj.get("inputTokens")) output_tokens = from_float(obj.get("outputTokens")) cache_read_tokens = from_float(obj.get("cacheReadTokens")) cache_write_tokens = from_float(obj.get("cacheWriteTokens")) - return SessionShutdownDataModelMetricsValueUsage( + return ShutdownModelMetricUsage( input_tokens=input_tokens, output_tokens=output_tokens, cache_read_tokens=cache_read_tokens, @@ -1173,36 +1173,36 @@ def to_dict(self) -> dict: @dataclass -class SessionShutdownDataModelMetricsValue: - requests: SessionShutdownDataModelMetricsValueRequests - usage: SessionShutdownDataModelMetricsValueUsage +class ShutdownModelMetric: + requests: ShutdownModelMetricRequests + usage: ShutdownModelMetricUsage @staticmethod - def from_dict(obj: Any) -> "SessionShutdownDataModelMetricsValue": + def from_dict(obj: Any) -> "ShutdownModelMetric": assert isinstance(obj, dict) - requests = SessionShutdownDataModelMetricsValueRequests.from_dict(obj.get("requests")) - usage = SessionShutdownDataModelMetricsValueUsage.from_dict(obj.get("usage")) - return SessionShutdownDataModelMetricsValue( + requests = ShutdownModelMetricRequests.from_dict(obj.get("requests")) + usage = ShutdownModelMetricUsage.from_dict(obj.get("usage")) + return ShutdownModelMetric( requests=requests, usage=usage, ) def to_dict(self) -> dict: result: dict = {} - result["requests"] = to_class(SessionShutdownDataModelMetricsValueRequests, self.requests) - result["usage"] = to_class(SessionShutdownDataModelMetricsValueUsage, self.usage) + result["requests"] = to_class(ShutdownModelMetricRequests, self.requests) + result["usage"] = to_class(ShutdownModelMetricUsage, self.usage) return result @dataclass class SessionShutdownData: "Session termination metrics including usage statistics, code changes, and shutdown reason" - shutdown_type: SessionShutdownDataShutdownType + shutdown_type: ShutdownType total_premium_requests: float total_api_duration_ms: float session_start_time: float - code_changes: SessionShutdownDataCodeChanges - model_metrics: dict[str, SessionShutdownDataModelMetricsValue] + code_changes: ShutdownCodeChanges + model_metrics: dict[str, ShutdownModelMetric] error_reason: str | None = None current_model: str | None = None current_tokens: float | None = None @@ -1213,12 +1213,12 @@ class SessionShutdownData: @staticmethod def from_dict(obj: Any) -> "SessionShutdownData": assert isinstance(obj, dict) - shutdown_type = parse_enum(SessionShutdownDataShutdownType, obj.get("shutdownType")) + shutdown_type = parse_enum(ShutdownType, obj.get("shutdownType")) total_premium_requests = from_float(obj.get("totalPremiumRequests")) total_api_duration_ms = from_float(obj.get("totalApiDurationMs")) session_start_time = from_float(obj.get("sessionStartTime")) - code_changes = SessionShutdownDataCodeChanges.from_dict(obj.get("codeChanges")) - model_metrics = from_dict(lambda x: SessionShutdownDataModelMetricsValue.from_dict(x), obj.get("modelMetrics")) + code_changes = ShutdownCodeChanges.from_dict(obj.get("codeChanges")) + model_metrics = from_dict(lambda x: ShutdownModelMetric.from_dict(x), obj.get("modelMetrics")) error_reason = from_union([from_none, lambda x: from_str(x)], obj.get("errorReason")) current_model = from_union([from_none, lambda x: from_str(x)], obj.get("currentModel")) current_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("currentTokens")) @@ -1242,12 +1242,12 @@ def from_dict(obj: Any) -> "SessionShutdownData": def to_dict(self) -> dict: result: dict = {} - result["shutdownType"] = to_enum(SessionShutdownDataShutdownType, self.shutdown_type) + result["shutdownType"] = to_enum(ShutdownType, self.shutdown_type) result["totalPremiumRequests"] = to_float(self.total_premium_requests) result["totalApiDurationMs"] = to_float(self.total_api_duration_ms) result["sessionStartTime"] = to_float(self.session_start_time) - result["codeChanges"] = to_class(SessionShutdownDataCodeChanges, self.code_changes) - result["modelMetrics"] = from_dict(lambda x: to_class(SessionShutdownDataModelMetricsValue, x), self.model_metrics) + result["codeChanges"] = to_class(ShutdownCodeChanges, self.code_changes) + result["modelMetrics"] = from_dict(lambda x: to_class(ShutdownModelMetric, x), self.model_metrics) if self.error_reason is not None: result["errorReason"] = from_union([from_none, lambda x: from_str(x)], self.error_reason) if self.current_model is not None: @@ -1269,7 +1269,7 @@ class SessionContextChangedData: cwd: str git_root: str | None = None repository: str | None = None - host_type: SessionStartDataContextHostType | None = None + host_type: SessionContextChangedDataHostType | None = None branch: str | None = None head_commit: str | None = None base_commit: str | None = None @@ -1280,7 +1280,7 @@ def from_dict(obj: Any) -> "SessionContextChangedData": cwd = from_str(obj.get("cwd")) git_root = from_union([from_none, lambda x: from_str(x)], obj.get("gitRoot")) repository = from_union([from_none, lambda x: from_str(x)], obj.get("repository")) - host_type = from_union([from_none, lambda x: parse_enum(SessionStartDataContextHostType, x)], obj.get("hostType")) + host_type = from_union([from_none, lambda x: parse_enum(SessionContextChangedDataHostType, x)], obj.get("hostType")) branch = from_union([from_none, lambda x: from_str(x)], obj.get("branch")) head_commit = from_union([from_none, lambda x: from_str(x)], obj.get("headCommit")) base_commit = from_union([from_none, lambda x: from_str(x)], obj.get("baseCommit")) @@ -1302,7 +1302,7 @@ def to_dict(self) -> dict: if self.repository is not None: result["repository"] = from_union([from_none, lambda x: from_str(x)], self.repository) if self.host_type is not None: - result["hostType"] = from_union([from_none, lambda x: to_enum(SessionStartDataContextHostType, x)], self.host_type) + result["hostType"] = from_union([from_none, lambda x: to_enum(SessionContextChangedDataHostType, x)], self.host_type) if self.branch is not None: result["branch"] = from_union([from_none, lambda x: from_str(x)], self.branch) if self.head_commit is not None: @@ -1390,19 +1390,19 @@ def to_dict(self) -> dict: @dataclass -class SessionCompactionCompleteDataCompactionTokensUsed: +class CompactionCompleteCompactionTokensUsed: "Token usage breakdown for the compaction LLM call" input: float output: float cached_input: float @staticmethod - def from_dict(obj: Any) -> "SessionCompactionCompleteDataCompactionTokensUsed": + def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsed": assert isinstance(obj, dict) input = from_float(obj.get("input")) output = from_float(obj.get("output")) cached_input = from_float(obj.get("cachedInput")) - return SessionCompactionCompleteDataCompactionTokensUsed( + return CompactionCompleteCompactionTokensUsed( input=input, output=output, cached_input=cached_input, @@ -1429,7 +1429,7 @@ class SessionCompactionCompleteData: summary_content: str | None = None checkpoint_number: float | None = None checkpoint_path: str | None = None - compaction_tokens_used: SessionCompactionCompleteDataCompactionTokensUsed | None = None + compaction_tokens_used: CompactionCompleteCompactionTokensUsed | None = None request_id: str | None = None system_tokens: float | None = None conversation_tokens: float | None = None @@ -1448,7 +1448,7 @@ def from_dict(obj: Any) -> "SessionCompactionCompleteData": summary_content = from_union([from_none, lambda x: from_str(x)], obj.get("summaryContent")) checkpoint_number = from_union([from_none, lambda x: from_float(x)], obj.get("checkpointNumber")) checkpoint_path = from_union([from_none, lambda x: from_str(x)], obj.get("checkpointPath")) - compaction_tokens_used = from_union([from_none, lambda x: SessionCompactionCompleteDataCompactionTokensUsed.from_dict(x)], obj.get("compactionTokensUsed")) + compaction_tokens_used = from_union([from_none, lambda x: CompactionCompleteCompactionTokensUsed.from_dict(x)], obj.get("compactionTokensUsed")) request_id = from_union([from_none, lambda x: from_str(x)], obj.get("requestId")) system_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("systemTokens")) conversation_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("conversationTokens")) @@ -1493,7 +1493,7 @@ def to_dict(self) -> dict: if self.checkpoint_path is not None: result["checkpointPath"] = from_union([from_none, lambda x: from_str(x)], self.checkpoint_path) if self.compaction_tokens_used is not None: - result["compactionTokensUsed"] = from_union([from_none, lambda x: to_class(SessionCompactionCompleteDataCompactionTokensUsed, x)], self.compaction_tokens_used) + result["compactionTokensUsed"] = from_union([from_none, lambda x: to_class(CompactionCompleteCompactionTokensUsed, x)], self.compaction_tokens_used) if self.request_id is not None: result["requestId"] = from_union([from_none, lambda x: from_str(x)], self.request_id) if self.system_tokens is not None: @@ -1531,17 +1531,17 @@ def to_dict(self) -> dict: @dataclass -class UserMessageDataAttachmentsItemLineRange: +class UserMessageAttachmentFileLineRange: "Optional line range to scope the attachment to a specific section of the file" start: float end: float @staticmethod - def from_dict(obj: Any) -> "UserMessageDataAttachmentsItemLineRange": + def from_dict(obj: Any) -> "UserMessageAttachmentFileLineRange": assert isinstance(obj, dict) start = from_float(obj.get("start")) end = from_float(obj.get("end")) - return UserMessageDataAttachmentsItemLineRange( + return UserMessageAttachmentFileLineRange( start=start, end=end, ) @@ -1554,17 +1554,17 @@ def to_dict(self) -> dict: @dataclass -class UserMessageDataAttachmentsItemSelectionStart: +class UserMessageAttachmentSelectionDetailsStart: "Start position of the selection" line: float character: float @staticmethod - def from_dict(obj: Any) -> "UserMessageDataAttachmentsItemSelectionStart": + def from_dict(obj: Any) -> "UserMessageAttachmentSelectionDetailsStart": assert isinstance(obj, dict) line = from_float(obj.get("line")) character = from_float(obj.get("character")) - return UserMessageDataAttachmentsItemSelectionStart( + return UserMessageAttachmentSelectionDetailsStart( line=line, character=character, ) @@ -1577,17 +1577,17 @@ def to_dict(self) -> dict: @dataclass -class UserMessageDataAttachmentsItemSelectionEnd: +class UserMessageAttachmentSelectionDetailsEnd: "End position of the selection" line: float character: float @staticmethod - def from_dict(obj: Any) -> "UserMessageDataAttachmentsItemSelectionEnd": + def from_dict(obj: Any) -> "UserMessageAttachmentSelectionDetailsEnd": assert isinstance(obj, dict) line = from_float(obj.get("line")) character = from_float(obj.get("character")) - return UserMessageDataAttachmentsItemSelectionEnd( + return UserMessageAttachmentSelectionDetailsEnd( line=line, character=character, ) @@ -1600,64 +1600,64 @@ def to_dict(self) -> dict: @dataclass -class UserMessageDataAttachmentsItemSelection: +class UserMessageAttachmentSelectionDetails: "Position range of the selection within the file" - start: UserMessageDataAttachmentsItemSelectionStart - end: UserMessageDataAttachmentsItemSelectionEnd + start: UserMessageAttachmentSelectionDetailsStart + end: UserMessageAttachmentSelectionDetailsEnd @staticmethod - def from_dict(obj: Any) -> "UserMessageDataAttachmentsItemSelection": + def from_dict(obj: Any) -> "UserMessageAttachmentSelectionDetails": assert isinstance(obj, dict) - start = UserMessageDataAttachmentsItemSelectionStart.from_dict(obj.get("start")) - end = UserMessageDataAttachmentsItemSelectionEnd.from_dict(obj.get("end")) - return UserMessageDataAttachmentsItemSelection( + start = UserMessageAttachmentSelectionDetailsStart.from_dict(obj.get("start")) + end = UserMessageAttachmentSelectionDetailsEnd.from_dict(obj.get("end")) + return UserMessageAttachmentSelectionDetails( start=start, end=end, ) def to_dict(self) -> dict: result: dict = {} - result["start"] = to_class(UserMessageDataAttachmentsItemSelectionStart, self.start) - result["end"] = to_class(UserMessageDataAttachmentsItemSelectionEnd, self.end) + result["start"] = to_class(UserMessageAttachmentSelectionDetailsStart, self.start) + result["end"] = to_class(UserMessageAttachmentSelectionDetailsEnd, self.end) return result @dataclass -class UserMessageDataAttachmentsItem: +class UserMessageAttachment: "A user message attachment — a file, directory, code selection, blob, or GitHub reference" - type: UserMessageDataAttachmentsItemType + type: UserMessageAttachmentType path: str | None = None display_name: str | None = None - line_range: UserMessageDataAttachmentsItemLineRange | None = None + line_range: UserMessageAttachmentFileLineRange | None = None file_path: str | None = None text: str | None = None - selection: UserMessageDataAttachmentsItemSelection | None = None + selection: UserMessageAttachmentSelectionDetails | None = None number: float | None = None title: str | None = None - reference_type: UserMessageDataAttachmentsItemReferenceType | None = None + reference_type: UserMessageAttachmentGithubReferenceType | None = None state: str | None = None url: str | None = None data: str | None = None mime_type: str | None = None @staticmethod - def from_dict(obj: Any) -> "UserMessageDataAttachmentsItem": + def from_dict(obj: Any) -> "UserMessageAttachment": assert isinstance(obj, dict) - type = parse_enum(UserMessageDataAttachmentsItemType, obj.get("type")) + type = parse_enum(UserMessageAttachmentType, obj.get("type")) path = from_union([from_none, lambda x: from_str(x)], obj.get("path")) display_name = from_union([from_none, lambda x: from_str(x)], obj.get("displayName")) - line_range = from_union([from_none, lambda x: UserMessageDataAttachmentsItemLineRange.from_dict(x)], obj.get("lineRange")) + line_range = from_union([from_none, lambda x: UserMessageAttachmentFileLineRange.from_dict(x)], obj.get("lineRange")) file_path = from_union([from_none, lambda x: from_str(x)], obj.get("filePath")) text = from_union([from_none, lambda x: from_str(x)], obj.get("text")) - selection = from_union([from_none, lambda x: UserMessageDataAttachmentsItemSelection.from_dict(x)], obj.get("selection")) + selection = from_union([from_none, lambda x: UserMessageAttachmentSelectionDetails.from_dict(x)], obj.get("selection")) number = from_union([from_none, lambda x: from_float(x)], obj.get("number")) title = from_union([from_none, lambda x: from_str(x)], obj.get("title")) - reference_type = from_union([from_none, lambda x: parse_enum(UserMessageDataAttachmentsItemReferenceType, x)], obj.get("referenceType")) + reference_type = from_union([from_none, lambda x: parse_enum(UserMessageAttachmentGithubReferenceType, x)], obj.get("referenceType")) state = from_union([from_none, lambda x: from_str(x)], obj.get("state")) url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) data = from_union([from_none, lambda x: from_str(x)], obj.get("data")) mime_type = from_union([from_none, lambda x: from_str(x)], obj.get("mimeType")) - return UserMessageDataAttachmentsItem( + return UserMessageAttachment( type=type, path=path, display_name=display_name, @@ -1676,25 +1676,25 @@ def from_dict(obj: Any) -> "UserMessageDataAttachmentsItem": def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(UserMessageDataAttachmentsItemType, self.type) + result["type"] = to_enum(UserMessageAttachmentType, self.type) if self.path is not None: result["path"] = from_union([from_none, lambda x: from_str(x)], self.path) if self.display_name is not None: result["displayName"] = from_union([from_none, lambda x: from_str(x)], self.display_name) if self.line_range is not None: - result["lineRange"] = from_union([from_none, lambda x: to_class(UserMessageDataAttachmentsItemLineRange, x)], self.line_range) + result["lineRange"] = from_union([from_none, lambda x: to_class(UserMessageAttachmentFileLineRange, x)], self.line_range) if self.file_path is not None: result["filePath"] = from_union([from_none, lambda x: from_str(x)], self.file_path) if self.text is not None: result["text"] = from_union([from_none, lambda x: from_str(x)], self.text) if self.selection is not None: - result["selection"] = from_union([from_none, lambda x: to_class(UserMessageDataAttachmentsItemSelection, x)], self.selection) + result["selection"] = from_union([from_none, lambda x: to_class(UserMessageAttachmentSelectionDetails, x)], self.selection) if self.number is not None: result["number"] = from_union([from_none, lambda x: to_float(x)], self.number) if self.title is not None: result["title"] = from_union([from_none, lambda x: from_str(x)], self.title) if self.reference_type is not None: - result["referenceType"] = from_union([from_none, lambda x: to_enum(UserMessageDataAttachmentsItemReferenceType, x)], self.reference_type) + result["referenceType"] = from_union([from_none, lambda x: to_enum(UserMessageAttachmentGithubReferenceType, x)], self.reference_type) if self.state is not None: result["state"] = from_union([from_none, lambda x: from_str(x)], self.state) if self.url is not None: @@ -1710,9 +1710,9 @@ def to_dict(self) -> dict: class UserMessageData: content: str transformed_content: str | None = None - attachments: list[UserMessageDataAttachmentsItem] | None = None + attachments: list[UserMessageAttachment] | None = None source: str | None = None - agent_mode: UserMessageDataAgentMode | None = None + agent_mode: UserMessageAgentMode | None = None interaction_id: str | None = None @staticmethod @@ -1720,9 +1720,9 @@ def from_dict(obj: Any) -> "UserMessageData": assert isinstance(obj, dict) content = from_str(obj.get("content")) transformed_content = from_union([from_none, lambda x: from_str(x)], obj.get("transformedContent")) - attachments = from_union([from_none, lambda x: from_list(UserMessageDataAttachmentsItem.from_dict, x)], obj.get("attachments")) + attachments = from_union([from_none, lambda x: from_list(UserMessageAttachment.from_dict, x)], obj.get("attachments")) source = from_union([from_none, lambda x: from_str(x)], obj.get("source")) - agent_mode = from_union([from_none, lambda x: parse_enum(UserMessageDataAgentMode, x)], obj.get("agentMode")) + agent_mode = from_union([from_none, lambda x: parse_enum(UserMessageAgentMode, x)], obj.get("agentMode")) interaction_id = from_union([from_none, lambda x: from_str(x)], obj.get("interactionId")) return UserMessageData( content=content, @@ -1739,11 +1739,11 @@ def to_dict(self) -> dict: if self.transformed_content is not None: result["transformedContent"] = from_union([from_none, lambda x: from_str(x)], self.transformed_content) if self.attachments is not None: - result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: to_class(UserMessageDataAttachmentsItem, x), x)], self.attachments) + result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: to_class(UserMessageAttachment, x), x)], self.attachments) if self.source is not None: result["source"] = from_union([from_none, lambda x: from_str(x)], self.source) if self.agent_mode is not None: - result["agentMode"] = from_union([from_none, lambda x: to_enum(UserMessageDataAgentMode, x)], self.agent_mode) + result["agentMode"] = from_union([from_none, lambda x: to_enum(UserMessageAgentMode, x)], self.agent_mode) if self.interaction_id is not None: result["interactionId"] = from_union([from_none, lambda x: from_str(x)], self.interaction_id) return result @@ -1870,27 +1870,27 @@ def to_dict(self) -> dict: @dataclass -class AssistantMessageDataToolRequestsItem: +class AssistantMessageToolRequest: "A tool invocation request from the assistant" tool_call_id: str name: str arguments: Any = None - type: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType | None = None + type: AssistantMessageToolRequestType | None = None tool_title: str | None = None mcp_server_name: str | None = None intention_summary: str | None = None @staticmethod - def from_dict(obj: Any) -> "AssistantMessageDataToolRequestsItem": + def from_dict(obj: Any) -> "AssistantMessageToolRequest": assert isinstance(obj, dict) tool_call_id = from_str(obj.get("toolCallId")) name = from_str(obj.get("name")) arguments = obj.get("arguments") - type = from_union([from_none, lambda x: parse_enum(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType, x)], obj.get("type")) + type = from_union([from_none, lambda x: parse_enum(AssistantMessageToolRequestType, x)], obj.get("type")) tool_title = from_union([from_none, lambda x: from_str(x)], obj.get("toolTitle")) mcp_server_name = from_union([from_none, lambda x: from_str(x)], obj.get("mcpServerName")) intention_summary = from_union([from_none, lambda x: from_str(x)], obj.get("intentionSummary")) - return AssistantMessageDataToolRequestsItem( + return AssistantMessageToolRequest( tool_call_id=tool_call_id, name=name, arguments=arguments, @@ -1907,7 +1907,7 @@ def to_dict(self) -> dict: if self.arguments is not None: result["arguments"] = self.arguments if self.type is not None: - result["type"] = from_union([from_none, lambda x: to_enum(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType, x)], self.type) + result["type"] = from_union([from_none, lambda x: to_enum(AssistantMessageToolRequestType, x)], self.type) if self.tool_title is not None: result["toolTitle"] = from_union([from_none, lambda x: from_str(x)], self.tool_title) if self.mcp_server_name is not None: @@ -1922,7 +1922,7 @@ class AssistantMessageData: "Assistant response containing text content, optional tool requests, and interaction metadata" message_id: str content: str - tool_requests: list[AssistantMessageDataToolRequestsItem] | None = None + tool_requests: list[AssistantMessageToolRequest] | None = None reasoning_opaque: str | None = None reasoning_text: str | None = None encrypted_content: str | None = None @@ -1937,7 +1937,7 @@ def from_dict(obj: Any) -> "AssistantMessageData": assert isinstance(obj, dict) message_id = from_str(obj.get("messageId")) content = from_str(obj.get("content")) - tool_requests = from_union([from_none, lambda x: from_list(lambda x: AssistantMessageDataToolRequestsItem.from_dict(x), x)], obj.get("toolRequests")) + tool_requests = from_union([from_none, lambda x: from_list(lambda x: AssistantMessageToolRequest.from_dict(x), x)], obj.get("toolRequests")) reasoning_opaque = from_union([from_none, lambda x: from_str(x)], obj.get("reasoningOpaque")) reasoning_text = from_union([from_none, lambda x: from_str(x)], obj.get("reasoningText")) encrypted_content = from_union([from_none, lambda x: from_str(x)], obj.get("encryptedContent")) @@ -1965,7 +1965,7 @@ def to_dict(self) -> dict: result["messageId"] = from_str(self.message_id) result["content"] = from_str(self.content) if self.tool_requests is not None: - result["toolRequests"] = from_union([from_none, lambda x: from_list(lambda x: to_class(AssistantMessageDataToolRequestsItem, x), x)], self.tool_requests) + result["toolRequests"] = from_union([from_none, lambda x: from_list(lambda x: to_class(AssistantMessageToolRequest, x), x)], self.tool_requests) if self.reasoning_opaque is not None: result["reasoningOpaque"] = from_union([from_none, lambda x: from_str(x)], self.reasoning_opaque) if self.reasoning_text is not None: @@ -2033,7 +2033,7 @@ def to_dict(self) -> dict: @dataclass -class AssistantUsageDataQuotaSnapshotsValue: +class AssistantUsageQuotaSnapshot: is_unlimited_entitlement: bool entitlement_requests: float used_requests: float @@ -2044,7 +2044,7 @@ class AssistantUsageDataQuotaSnapshotsValue: reset_date: datetime | None = None @staticmethod - def from_dict(obj: Any) -> "AssistantUsageDataQuotaSnapshotsValue": + def from_dict(obj: Any) -> "AssistantUsageQuotaSnapshot": assert isinstance(obj, dict) is_unlimited_entitlement = from_bool(obj.get("isUnlimitedEntitlement")) entitlement_requests = from_float(obj.get("entitlementRequests")) @@ -2054,7 +2054,7 @@ def from_dict(obj: Any) -> "AssistantUsageDataQuotaSnapshotsValue": overage_allowed_with_exhausted_quota = from_bool(obj.get("overageAllowedWithExhaustedQuota")) remaining_percentage = from_float(obj.get("remainingPercentage")) reset_date = from_union([from_none, lambda x: from_datetime(x)], obj.get("resetDate")) - return AssistantUsageDataQuotaSnapshotsValue( + return AssistantUsageQuotaSnapshot( is_unlimited_entitlement=is_unlimited_entitlement, entitlement_requests=entitlement_requests, used_requests=used_requests, @@ -2080,7 +2080,7 @@ def to_dict(self) -> dict: @dataclass -class AssistantUsageDataCopilotUsageTokenDetailsItem: +class AssistantUsageCopilotUsageTokenDetail: "Token usage detail for a single billing category" batch_size: float cost_per_batch: float @@ -2088,13 +2088,13 @@ class AssistantUsageDataCopilotUsageTokenDetailsItem: token_type: str @staticmethod - def from_dict(obj: Any) -> "AssistantUsageDataCopilotUsageTokenDetailsItem": + def from_dict(obj: Any) -> "AssistantUsageCopilotUsageTokenDetail": assert isinstance(obj, dict) batch_size = from_float(obj.get("batchSize")) cost_per_batch = from_float(obj.get("costPerBatch")) token_count = from_float(obj.get("tokenCount")) token_type = from_str(obj.get("tokenType")) - return AssistantUsageDataCopilotUsageTokenDetailsItem( + return AssistantUsageCopilotUsageTokenDetail( batch_size=batch_size, cost_per_batch=cost_per_batch, token_count=token_count, @@ -2111,24 +2111,24 @@ def to_dict(self) -> dict: @dataclass -class AssistantUsageDataCopilotUsage: +class AssistantUsageCopilotUsage: "Per-request cost and usage data from the CAPI copilot_usage response field" - token_details: list[AssistantUsageDataCopilotUsageTokenDetailsItem] + token_details: list[AssistantUsageCopilotUsageTokenDetail] total_nano_aiu: float @staticmethod - def from_dict(obj: Any) -> "AssistantUsageDataCopilotUsage": + def from_dict(obj: Any) -> "AssistantUsageCopilotUsage": assert isinstance(obj, dict) - token_details = from_list(lambda x: AssistantUsageDataCopilotUsageTokenDetailsItem.from_dict(x), obj.get("tokenDetails")) + token_details = from_list(lambda x: AssistantUsageCopilotUsageTokenDetail.from_dict(x), obj.get("tokenDetails")) total_nano_aiu = from_float(obj.get("totalNanoAiu")) - return AssistantUsageDataCopilotUsage( + return AssistantUsageCopilotUsage( token_details=token_details, total_nano_aiu=total_nano_aiu, ) def to_dict(self) -> dict: result: dict = {} - result["tokenDetails"] = from_list(lambda x: to_class(AssistantUsageDataCopilotUsageTokenDetailsItem, x), self.token_details) + result["tokenDetails"] = from_list(lambda x: to_class(AssistantUsageCopilotUsageTokenDetail, x), self.token_details) result["totalNanoAiu"] = to_float(self.total_nano_aiu) return result @@ -2149,8 +2149,8 @@ class AssistantUsageData: api_call_id: str | None = None provider_call_id: str | None = None parent_tool_call_id: str | None = None - quota_snapshots: dict[str, AssistantUsageDataQuotaSnapshotsValue] | None = None - copilot_usage: AssistantUsageDataCopilotUsage | None = None + quota_snapshots: dict[str, AssistantUsageQuotaSnapshot] | None = None + copilot_usage: AssistantUsageCopilotUsage | None = None reasoning_effort: str | None = None @staticmethod @@ -2169,8 +2169,8 @@ def from_dict(obj: Any) -> "AssistantUsageData": api_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("apiCallId")) provider_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("providerCallId")) parent_tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("parentToolCallId")) - quota_snapshots = from_union([from_none, lambda x: from_dict(lambda x: AssistantUsageDataQuotaSnapshotsValue.from_dict(x), x)], obj.get("quotaSnapshots")) - copilot_usage = from_union([from_none, lambda x: AssistantUsageDataCopilotUsage.from_dict(x)], obj.get("copilotUsage")) + quota_snapshots = from_union([from_none, lambda x: from_dict(lambda x: AssistantUsageQuotaSnapshot.from_dict(x), x)], obj.get("quotaSnapshots")) + copilot_usage = from_union([from_none, lambda x: AssistantUsageCopilotUsage.from_dict(x)], obj.get("copilotUsage")) reasoning_effort = from_union([from_none, lambda x: from_str(x)], obj.get("reasoningEffort")) return AssistantUsageData( model=model, @@ -2219,9 +2219,9 @@ def to_dict(self) -> dict: if self.parent_tool_call_id is not None: result["parentToolCallId"] = from_union([from_none, lambda x: from_str(x)], self.parent_tool_call_id) if self.quota_snapshots is not None: - result["quotaSnapshots"] = from_union([from_none, lambda x: from_dict(lambda x: to_class(AssistantUsageDataQuotaSnapshotsValue, x), x)], self.quota_snapshots) + result["quotaSnapshots"] = from_union([from_none, lambda x: from_dict(lambda x: to_class(AssistantUsageQuotaSnapshot, x), x)], self.quota_snapshots) if self.copilot_usage is not None: - result["copilotUsage"] = from_union([from_none, lambda x: to_class(AssistantUsageDataCopilotUsage, x)], self.copilot_usage) + result["copilotUsage"] = from_union([from_none, lambda x: to_class(AssistantUsageCopilotUsage, x)], self.copilot_usage) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, lambda x: from_str(x)], self.reasoning_effort) return result @@ -3027,16 +3027,16 @@ def to_dict(self) -> dict: @dataclass -class PermissionRequestedDataPermissionRequestCommandsItem: +class PermissionRequestShellCommand: identifier: str read_only: bool @staticmethod - def from_dict(obj: Any) -> "PermissionRequestedDataPermissionRequestCommandsItem": + def from_dict(obj: Any) -> "PermissionRequestShellCommand": assert isinstance(obj, dict) identifier = from_str(obj.get("identifier")) read_only = from_bool(obj.get("readOnly")) - return PermissionRequestedDataPermissionRequestCommandsItem( + return PermissionRequestShellCommand( identifier=identifier, read_only=read_only, ) @@ -3049,14 +3049,14 @@ def to_dict(self) -> dict: @dataclass -class PermissionRequestedDataPermissionRequestPossibleUrlsItem: +class PermissionRequestShellPossibleURL: url: str @staticmethod - def from_dict(obj: Any) -> "PermissionRequestedDataPermissionRequestPossibleUrlsItem": + def from_dict(obj: Any) -> "PermissionRequestShellPossibleURL": assert isinstance(obj, dict) url = from_str(obj.get("url")) - return PermissionRequestedDataPermissionRequestPossibleUrlsItem( + return PermissionRequestShellPossibleURL( url=url, ) @@ -3067,15 +3067,15 @@ def to_dict(self) -> dict: @dataclass -class PermissionRequestedDataPermissionRequest: +class PermissionRequest: "Details of the permission being requested" kind: PermissionRequestedDataPermissionRequestKind tool_call_id: str | None = None full_command_text: str | None = None intention: str | None = None - commands: list[PermissionRequestedDataPermissionRequestCommandsItem] | None = None + commands: list[PermissionRequestShellCommand] | None = None possible_paths: list[str] | None = None - possible_urls: list[PermissionRequestedDataPermissionRequestPossibleUrlsItem] | None = None + possible_urls: list[PermissionRequestShellPossibleURL] | None = None has_write_file_redirection: bool | None = None can_offer_session_approval: bool | None = None warning: str | None = None @@ -3089,26 +3089,26 @@ class PermissionRequestedDataPermissionRequest: args: Any = None read_only: bool | None = None url: str | None = None - action: PermissionRequestedDataPermissionRequestAction | None = None + action: PermissionRequestMemoryAction | None = None subject: str | None = None fact: str | None = None citations: str | None = None - direction: PermissionRequestedDataPermissionRequestDirection | None = None + direction: PermissionRequestMemoryDirection | None = None reason: str | None = None tool_description: str | None = None tool_args: Any = None hook_message: str | None = None @staticmethod - def from_dict(obj: Any) -> "PermissionRequestedDataPermissionRequest": + def from_dict(obj: Any) -> "PermissionRequest": assert isinstance(obj, dict) kind = parse_enum(PermissionRequestedDataPermissionRequestKind, obj.get("kind")) tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("toolCallId")) full_command_text = from_union([from_none, lambda x: from_str(x)], obj.get("fullCommandText")) intention = from_union([from_none, lambda x: from_str(x)], obj.get("intention")) - commands = from_union([from_none, lambda x: from_list(lambda x: PermissionRequestedDataPermissionRequestCommandsItem.from_dict(x), x)], obj.get("commands")) + commands = from_union([from_none, lambda x: from_list(lambda x: PermissionRequestShellCommand.from_dict(x), x)], obj.get("commands")) possible_paths = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], obj.get("possiblePaths")) - possible_urls = from_union([from_none, lambda x: from_list(lambda x: PermissionRequestedDataPermissionRequestPossibleUrlsItem.from_dict(x), x)], obj.get("possibleUrls")) + possible_urls = from_union([from_none, lambda x: from_list(lambda x: PermissionRequestShellPossibleURL.from_dict(x), x)], obj.get("possibleUrls")) has_write_file_redirection = from_union([from_none, lambda x: from_bool(x)], obj.get("hasWriteFileRedirection")) can_offer_session_approval = from_union([from_none, lambda x: from_bool(x)], obj.get("canOfferSessionApproval")) warning = from_union([from_none, lambda x: from_str(x)], obj.get("warning")) @@ -3122,16 +3122,16 @@ def from_dict(obj: Any) -> "PermissionRequestedDataPermissionRequest": args = obj.get("args") read_only = from_union([from_none, lambda x: from_bool(x)], obj.get("readOnly")) url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) - action = from_union([from_none, lambda x: parse_enum(PermissionRequestedDataPermissionRequestAction, x)], obj.get("action", "store")) + action = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryAction, x)], obj.get("action", "store")) subject = from_union([from_none, lambda x: from_str(x)], obj.get("subject")) fact = from_union([from_none, lambda x: from_str(x)], obj.get("fact")) citations = from_union([from_none, lambda x: from_str(x)], obj.get("citations")) - direction = from_union([from_none, lambda x: parse_enum(PermissionRequestedDataPermissionRequestDirection, x)], obj.get("direction")) + direction = from_union([from_none, lambda x: parse_enum(PermissionRequestMemoryDirection, x)], obj.get("direction")) reason = from_union([from_none, lambda x: from_str(x)], obj.get("reason")) tool_description = from_union([from_none, lambda x: from_str(x)], obj.get("toolDescription")) tool_args = obj.get("toolArgs") hook_message = from_union([from_none, lambda x: from_str(x)], obj.get("hookMessage")) - return PermissionRequestedDataPermissionRequest( + return PermissionRequest( kind=kind, tool_call_id=tool_call_id, full_command_text=full_command_text, @@ -3173,11 +3173,11 @@ def to_dict(self) -> dict: if self.intention is not None: result["intention"] = from_union([from_none, lambda x: from_str(x)], self.intention) if self.commands is not None: - result["commands"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestedDataPermissionRequestCommandsItem, x), x)], self.commands) + result["commands"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestShellCommand, x), x)], self.commands) if self.possible_paths is not None: result["possiblePaths"] = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], self.possible_paths) if self.possible_urls is not None: - result["possibleUrls"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestedDataPermissionRequestPossibleUrlsItem, x), x)], self.possible_urls) + result["possibleUrls"] = from_union([from_none, lambda x: from_list(lambda x: to_class(PermissionRequestShellPossibleURL, x), x)], self.possible_urls) if self.has_write_file_redirection is not None: result["hasWriteFileRedirection"] = from_union([from_none, lambda x: from_bool(x)], self.has_write_file_redirection) if self.can_offer_session_approval is not None: @@ -3205,7 +3205,7 @@ def to_dict(self) -> dict: if self.url is not None: result["url"] = from_union([from_none, lambda x: from_str(x)], self.url) if self.action is not None: - result["action"] = from_union([from_none, lambda x: to_enum(PermissionRequestedDataPermissionRequestAction, x)], self.action) + result["action"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryAction, x)], self.action) if self.subject is not None: result["subject"] = from_union([from_none, lambda x: from_str(x)], self.subject) if self.fact is not None: @@ -3213,7 +3213,7 @@ def to_dict(self) -> dict: if self.citations is not None: result["citations"] = from_union([from_none, lambda x: from_str(x)], self.citations) if self.direction is not None: - result["direction"] = from_union([from_none, lambda x: to_enum(PermissionRequestedDataPermissionRequestDirection, x)], self.direction) + result["direction"] = from_union([from_none, lambda x: to_enum(PermissionRequestMemoryDirection, x)], self.direction) if self.reason is not None: result["reason"] = from_union([from_none, lambda x: from_str(x)], self.reason) if self.tool_description is not None: @@ -3229,14 +3229,14 @@ def to_dict(self) -> dict: class PermissionRequestedData: "Permission request notification requiring client approval with request details" request_id: str - permission_request: PermissionRequestedDataPermissionRequest + permission_request: PermissionRequest resolved_by_hook: bool | None = None @staticmethod def from_dict(obj: Any) -> "PermissionRequestedData": assert isinstance(obj, dict) request_id = from_str(obj.get("requestId")) - permission_request = PermissionRequestedDataPermissionRequest.from_dict(obj.get("permissionRequest")) + permission_request = PermissionRequest.from_dict(obj.get("permissionRequest")) resolved_by_hook = from_union([from_none, lambda x: from_bool(x)], obj.get("resolvedByHook")) return PermissionRequestedData( request_id=request_id, @@ -3247,7 +3247,7 @@ def from_dict(obj: Any) -> "PermissionRequestedData": def to_dict(self) -> dict: result: dict = {} result["requestId"] = from_str(self.request_id) - result["permissionRequest"] = to_class(PermissionRequestedDataPermissionRequest, self.permission_request) + result["permissionRequest"] = to_class(PermissionRequest, self.permission_request) if self.resolved_by_hook is not None: result["resolvedByHook"] = from_union([from_none, lambda x: from_bool(x)], self.resolved_by_hook) return result @@ -3256,19 +3256,19 @@ def to_dict(self) -> dict: @dataclass class PermissionCompletedDataResult: "The result of the permission request" - kind: PermissionCompletedDataResultKind + kind: PermissionCompletedKind @staticmethod def from_dict(obj: Any) -> "PermissionCompletedDataResult": assert isinstance(obj, dict) - kind = parse_enum(PermissionCompletedDataResultKind, obj.get("kind")) + kind = parse_enum(PermissionCompletedKind, obj.get("kind")) return PermissionCompletedDataResult( kind=kind, ) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionCompletedDataResultKind, self.kind) + result["kind"] = to_enum(PermissionCompletedKind, self.kind) return result @@ -3363,19 +3363,19 @@ def to_dict(self) -> dict: @dataclass -class ElicitationRequestedDataRequestedSchema: +class ElicitationRequestedSchema: "JSON Schema describing the form fields to present to the user (form mode only)" type: str properties: dict[str, Any] required: list[str] | None = None @staticmethod - def from_dict(obj: Any) -> "ElicitationRequestedDataRequestedSchema": + def from_dict(obj: Any) -> "ElicitationRequestedSchema": assert isinstance(obj, dict) type = from_str(obj.get("type")) properties = from_dict(lambda x: x, obj.get("properties")) required = from_union([from_none, lambda x: from_list(lambda x: from_str(x), x)], obj.get("required")) - return ElicitationRequestedDataRequestedSchema( + return ElicitationRequestedSchema( type=type, properties=properties, required=required, @@ -3397,8 +3397,8 @@ class ElicitationRequestedData: message: str tool_call_id: str | None = None elicitation_source: str | None = None - mode: ElicitationRequestedDataMode | None = None - requested_schema: ElicitationRequestedDataRequestedSchema | None = None + mode: ElicitationRequestedMode | None = None + requested_schema: ElicitationRequestedSchema | None = None url: str | None = None @staticmethod @@ -3408,8 +3408,8 @@ def from_dict(obj: Any) -> "ElicitationRequestedData": message = from_str(obj.get("message")) tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("toolCallId")) elicitation_source = from_union([from_none, lambda x: from_str(x)], obj.get("elicitationSource")) - mode = from_union([from_none, lambda x: parse_enum(ElicitationRequestedDataMode, x)], obj.get("mode")) - requested_schema = from_union([from_none, lambda x: ElicitationRequestedDataRequestedSchema.from_dict(x)], obj.get("requestedSchema")) + mode = from_union([from_none, lambda x: parse_enum(ElicitationRequestedMode, x)], obj.get("mode")) + requested_schema = from_union([from_none, lambda x: ElicitationRequestedSchema.from_dict(x)], obj.get("requestedSchema")) url = from_union([from_none, lambda x: from_str(x)], obj.get("url")) return ElicitationRequestedData( request_id=request_id, @@ -3430,9 +3430,9 @@ def to_dict(self) -> dict: if self.elicitation_source is not None: result["elicitationSource"] = from_union([from_none, lambda x: from_str(x)], self.elicitation_source) if self.mode is not None: - result["mode"] = from_union([from_none, lambda x: to_enum(ElicitationRequestedDataMode, x)], self.mode) + result["mode"] = from_union([from_none, lambda x: to_enum(ElicitationRequestedMode, x)], self.mode) if self.requested_schema is not None: - result["requestedSchema"] = from_union([from_none, lambda x: to_class(ElicitationRequestedDataRequestedSchema, x)], self.requested_schema) + result["requestedSchema"] = from_union([from_none, lambda x: to_class(ElicitationRequestedSchema, x)], self.requested_schema) if self.url is not None: result["url"] = from_union([from_none, lambda x: from_str(x)], self.url) return result @@ -3442,14 +3442,14 @@ def to_dict(self) -> dict: class ElicitationCompletedData: "Elicitation request completion with the user's response" request_id: str - action: ElicitationCompletedDataAction | None = None + action: ElicitationCompletedAction | None = None content: dict[str, Any] | None = None @staticmethod def from_dict(obj: Any) -> "ElicitationCompletedData": assert isinstance(obj, dict) request_id = from_str(obj.get("requestId")) - action = from_union([from_none, lambda x: parse_enum(ElicitationCompletedDataAction, x)], obj.get("action")) + action = from_union([from_none, lambda x: parse_enum(ElicitationCompletedAction, x)], obj.get("action")) content = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("content")) return ElicitationCompletedData( request_id=request_id, @@ -3461,7 +3461,7 @@ def to_dict(self) -> dict: result: dict = {} result["requestId"] = from_str(self.request_id) if self.action is not None: - result["action"] = from_union([from_none, lambda x: to_enum(ElicitationCompletedDataAction, x)], self.action) + result["action"] = from_union([from_none, lambda x: to_enum(ElicitationCompletedAction, x)], self.action) if self.content is not None: result["content"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.content) return result @@ -3514,17 +3514,17 @@ def to_dict(self) -> dict: @dataclass -class McpOauthRequiredDataStaticClientConfig: +class MCPOauthRequiredStaticClientConfig: "Static OAuth client configuration, if the server specifies one" client_id: str public_client: bool | None = None @staticmethod - def from_dict(obj: Any) -> "McpOauthRequiredDataStaticClientConfig": + def from_dict(obj: Any) -> "MCPOauthRequiredStaticClientConfig": assert isinstance(obj, dict) client_id = from_str(obj.get("clientId")) public_client = from_union([from_none, lambda x: from_bool(x)], obj.get("publicClient")) - return McpOauthRequiredDataStaticClientConfig( + return MCPOauthRequiredStaticClientConfig( client_id=client_id, public_client=public_client, ) @@ -3543,7 +3543,7 @@ class McpOauthRequiredData: request_id: str server_name: str server_url: str - static_client_config: McpOauthRequiredDataStaticClientConfig | None = None + static_client_config: MCPOauthRequiredStaticClientConfig | None = None @staticmethod def from_dict(obj: Any) -> "McpOauthRequiredData": @@ -3551,7 +3551,7 @@ def from_dict(obj: Any) -> "McpOauthRequiredData": request_id = from_str(obj.get("requestId")) server_name = from_str(obj.get("serverName")) server_url = from_str(obj.get("serverUrl")) - static_client_config = from_union([from_none, lambda x: McpOauthRequiredDataStaticClientConfig.from_dict(x)], obj.get("staticClientConfig")) + static_client_config = from_union([from_none, lambda x: MCPOauthRequiredStaticClientConfig.from_dict(x)], obj.get("staticClientConfig")) return McpOauthRequiredData( request_id=request_id, server_name=server_name, @@ -3565,7 +3565,7 @@ def to_dict(self) -> dict: result["serverName"] = from_str(self.server_name) result["serverUrl"] = from_str(self.server_url) if self.static_client_config is not None: - result["staticClientConfig"] = from_union([from_none, lambda x: to_class(McpOauthRequiredDataStaticClientConfig, x)], self.static_client_config) + result["staticClientConfig"] = from_union([from_none, lambda x: to_class(MCPOauthRequiredStaticClientConfig, x)], self.static_client_config) return result @@ -3727,16 +3727,16 @@ def to_dict(self) -> dict: @dataclass -class CommandsChangedDataCommandsItem: +class CommandsChangedCommand: name: str description: str | None = None @staticmethod - def from_dict(obj: Any) -> "CommandsChangedDataCommandsItem": + def from_dict(obj: Any) -> "CommandsChangedCommand": assert isinstance(obj, dict) name = from_str(obj.get("name")) description = from_union([from_none, lambda x: from_str(x)], obj.get("description")) - return CommandsChangedDataCommandsItem( + return CommandsChangedCommand( name=name, description=description, ) @@ -3752,32 +3752,32 @@ def to_dict(self) -> dict: @dataclass class CommandsChangedData: "SDK command registration change notification" - commands: list[CommandsChangedDataCommandsItem] + commands: list[CommandsChangedCommand] @staticmethod def from_dict(obj: Any) -> "CommandsChangedData": assert isinstance(obj, dict) - commands = from_list(lambda x: CommandsChangedDataCommandsItem.from_dict(x), obj.get("commands")) + commands = from_list(lambda x: CommandsChangedCommand.from_dict(x), obj.get("commands")) return CommandsChangedData( commands=commands, ) def to_dict(self) -> dict: result: dict = {} - result["commands"] = from_list(lambda x: to_class(CommandsChangedDataCommandsItem, x), self.commands) + result["commands"] = from_list(lambda x: to_class(CommandsChangedCommand, x), self.commands) return result @dataclass -class CapabilitiesChangedDataUi: +class CapabilitiesChangedUI: "UI capability changes" elicitation: bool | None = None @staticmethod - def from_dict(obj: Any) -> "CapabilitiesChangedDataUi": + def from_dict(obj: Any) -> "CapabilitiesChangedUI": assert isinstance(obj, dict) elicitation = from_union([from_none, lambda x: from_bool(x)], obj.get("elicitation")) - return CapabilitiesChangedDataUi( + return CapabilitiesChangedUI( elicitation=elicitation, ) @@ -3791,12 +3791,12 @@ def to_dict(self) -> dict: @dataclass class CapabilitiesChangedData: "Session capability change notification" - ui: CapabilitiesChangedDataUi | None = None + ui: CapabilitiesChangedUI | None = None @staticmethod def from_dict(obj: Any) -> "CapabilitiesChangedData": assert isinstance(obj, dict) - ui = from_union([from_none, lambda x: CapabilitiesChangedDataUi.from_dict(x)], obj.get("ui")) + ui = from_union([from_none, lambda x: CapabilitiesChangedUI.from_dict(x)], obj.get("ui")) return CapabilitiesChangedData( ui=ui, ) @@ -3804,7 +3804,7 @@ def from_dict(obj: Any) -> "CapabilitiesChangedData": def to_dict(self) -> dict: result: dict = {} if self.ui is not None: - result["ui"] = from_union([from_none, lambda x: to_class(CapabilitiesChangedDataUi, x)], self.ui) + result["ui"] = from_union([from_none, lambda x: to_class(CapabilitiesChangedUI, x)], self.ui) return result @@ -3912,7 +3912,7 @@ def to_dict(self) -> dict: @dataclass -class SessionSkillsLoadedDataSkillsItem: +class SkillsLoadedSkill: name: str description: str source: str @@ -3921,7 +3921,7 @@ class SessionSkillsLoadedDataSkillsItem: path: str | None = None @staticmethod - def from_dict(obj: Any) -> "SessionSkillsLoadedDataSkillsItem": + def from_dict(obj: Any) -> "SkillsLoadedSkill": assert isinstance(obj, dict) name = from_str(obj.get("name")) description = from_str(obj.get("description")) @@ -3929,7 +3929,7 @@ def from_dict(obj: Any) -> "SessionSkillsLoadedDataSkillsItem": user_invocable = from_bool(obj.get("userInvocable")) enabled = from_bool(obj.get("enabled")) path = from_union([from_none, lambda x: from_str(x)], obj.get("path")) - return SessionSkillsLoadedDataSkillsItem( + return SkillsLoadedSkill( name=name, description=description, source=source, @@ -3952,24 +3952,24 @@ def to_dict(self) -> dict: @dataclass class SessionSkillsLoadedData: - skills: list[SessionSkillsLoadedDataSkillsItem] + skills: list[SkillsLoadedSkill] @staticmethod def from_dict(obj: Any) -> "SessionSkillsLoadedData": assert isinstance(obj, dict) - skills = from_list(lambda x: SessionSkillsLoadedDataSkillsItem.from_dict(x), obj.get("skills")) + skills = from_list(lambda x: SkillsLoadedSkill.from_dict(x), obj.get("skills")) return SessionSkillsLoadedData( skills=skills, ) def to_dict(self) -> dict: result: dict = {} - result["skills"] = from_list(lambda x: to_class(SessionSkillsLoadedDataSkillsItem, x), self.skills) + result["skills"] = from_list(lambda x: to_class(SkillsLoadedSkill, x), self.skills) return result @dataclass -class SessionCustomAgentsUpdatedDataAgentsItem: +class CustomAgentsUpdatedAgent: id: str name: str display_name: str @@ -3980,7 +3980,7 @@ class SessionCustomAgentsUpdatedDataAgentsItem: model: str | None = None @staticmethod - def from_dict(obj: Any) -> "SessionCustomAgentsUpdatedDataAgentsItem": + def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": assert isinstance(obj, dict) id = from_str(obj.get("id")) name = from_str(obj.get("name")) @@ -3990,7 +3990,7 @@ def from_dict(obj: Any) -> "SessionCustomAgentsUpdatedDataAgentsItem": tools = from_list(lambda x: from_str(x), obj.get("tools")) user_invocable = from_bool(obj.get("userInvocable")) model = from_union([from_none, lambda x: from_str(x)], obj.get("model")) - return SessionCustomAgentsUpdatedDataAgentsItem( + return CustomAgentsUpdatedAgent( id=id, name=name, display_name=display_name, @@ -4017,14 +4017,14 @@ def to_dict(self) -> dict: @dataclass class SessionCustomAgentsUpdatedData: - agents: list[SessionCustomAgentsUpdatedDataAgentsItem] + agents: list[CustomAgentsUpdatedAgent] warnings: list[str] errors: list[str] @staticmethod def from_dict(obj: Any) -> "SessionCustomAgentsUpdatedData": assert isinstance(obj, dict) - agents = from_list(lambda x: SessionCustomAgentsUpdatedDataAgentsItem.from_dict(x), obj.get("agents")) + agents = from_list(lambda x: CustomAgentsUpdatedAgent.from_dict(x), obj.get("agents")) warnings = from_list(lambda x: from_str(x), obj.get("warnings")) errors = from_list(lambda x: from_str(x), obj.get("errors")) return SessionCustomAgentsUpdatedData( @@ -4035,27 +4035,27 @@ def from_dict(obj: Any) -> "SessionCustomAgentsUpdatedData": def to_dict(self) -> dict: result: dict = {} - result["agents"] = from_list(lambda x: to_class(SessionCustomAgentsUpdatedDataAgentsItem, x), self.agents) + result["agents"] = from_list(lambda x: to_class(CustomAgentsUpdatedAgent, x), self.agents) result["warnings"] = from_list(lambda x: from_str(x), self.warnings) result["errors"] = from_list(lambda x: from_str(x), self.errors) return result @dataclass -class SessionMcpServersLoadedDataServersItem: +class MCPServersLoadedServer: name: str - status: SessionMcpServersLoadedDataServersItemStatus + status: MCPServerStatus source: str | None = None error: str | None = None @staticmethod - def from_dict(obj: Any) -> "SessionMcpServersLoadedDataServersItem": + def from_dict(obj: Any) -> "MCPServersLoadedServer": assert isinstance(obj, dict) name = from_str(obj.get("name")) - status = parse_enum(SessionMcpServersLoadedDataServersItemStatus, obj.get("status")) + status = parse_enum(MCPServerStatus, obj.get("status")) source = from_union([from_none, lambda x: from_str(x)], obj.get("source")) error = from_union([from_none, lambda x: from_str(x)], obj.get("error")) - return SessionMcpServersLoadedDataServersItem( + return MCPServersLoadedServer( name=name, status=status, source=source, @@ -4065,7 +4065,7 @@ def from_dict(obj: Any) -> "SessionMcpServersLoadedDataServersItem": def to_dict(self) -> dict: result: dict = {} result["name"] = from_str(self.name) - result["status"] = to_enum(SessionMcpServersLoadedDataServersItemStatus, self.status) + result["status"] = to_enum(MCPServerStatus, self.status) if self.source is not None: result["source"] = from_union([from_none, lambda x: from_str(x)], self.source) if self.error is not None: @@ -4075,32 +4075,32 @@ def to_dict(self) -> dict: @dataclass class SessionMcpServersLoadedData: - servers: list[SessionMcpServersLoadedDataServersItem] + servers: list[MCPServersLoadedServer] @staticmethod def from_dict(obj: Any) -> "SessionMcpServersLoadedData": assert isinstance(obj, dict) - servers = from_list(lambda x: SessionMcpServersLoadedDataServersItem.from_dict(x), obj.get("servers")) + servers = from_list(lambda x: MCPServersLoadedServer.from_dict(x), obj.get("servers")) return SessionMcpServersLoadedData( servers=servers, ) def to_dict(self) -> dict: result: dict = {} - result["servers"] = from_list(lambda x: to_class(SessionMcpServersLoadedDataServersItem, x), self.servers) + result["servers"] = from_list(lambda x: to_class(MCPServersLoadedServer, x), self.servers) return result @dataclass class SessionMcpServerStatusChangedData: server_name: str - status: SessionMcpServersLoadedDataServersItemStatus + status: SessionMcpServerStatusChangedDataStatus @staticmethod def from_dict(obj: Any) -> "SessionMcpServerStatusChangedData": assert isinstance(obj, dict) server_name = from_str(obj.get("serverName")) - status = parse_enum(SessionMcpServersLoadedDataServersItemStatus, obj.get("status")) + status = parse_enum(SessionMcpServerStatusChangedDataStatus, obj.get("status")) return SessionMcpServerStatusChangedData( server_name=server_name, status=status, @@ -4109,25 +4109,25 @@ def from_dict(obj: Any) -> "SessionMcpServerStatusChangedData": def to_dict(self) -> dict: result: dict = {} result["serverName"] = from_str(self.server_name) - result["status"] = to_enum(SessionMcpServersLoadedDataServersItemStatus, self.status) + result["status"] = to_enum(SessionMcpServerStatusChangedDataStatus, self.status) return result @dataclass -class SessionExtensionsLoadedDataExtensionsItem: +class ExtensionsLoadedExtension: id: str name: str - source: SessionExtensionsLoadedDataExtensionsItemSource - status: SessionExtensionsLoadedDataExtensionsItemStatus + source: ExtensionsLoadedExtensionSource + status: ExtensionsLoadedExtensionStatus @staticmethod - def from_dict(obj: Any) -> "SessionExtensionsLoadedDataExtensionsItem": + def from_dict(obj: Any) -> "ExtensionsLoadedExtension": assert isinstance(obj, dict) id = from_str(obj.get("id")) name = from_str(obj.get("name")) - source = parse_enum(SessionExtensionsLoadedDataExtensionsItemSource, obj.get("source")) - status = parse_enum(SessionExtensionsLoadedDataExtensionsItemStatus, obj.get("status")) - return SessionExtensionsLoadedDataExtensionsItem( + source = parse_enum(ExtensionsLoadedExtensionSource, obj.get("source")) + status = parse_enum(ExtensionsLoadedExtensionStatus, obj.get("status")) + return ExtensionsLoadedExtension( id=id, name=name, source=source, @@ -4138,26 +4138,26 @@ def to_dict(self) -> dict: result: dict = {} result["id"] = from_str(self.id) result["name"] = from_str(self.name) - result["source"] = to_enum(SessionExtensionsLoadedDataExtensionsItemSource, self.source) - result["status"] = to_enum(SessionExtensionsLoadedDataExtensionsItemStatus, self.status) + result["source"] = to_enum(ExtensionsLoadedExtensionSource, self.source) + result["status"] = to_enum(ExtensionsLoadedExtensionStatus, self.status) return result @dataclass class SessionExtensionsLoadedData: - extensions: list[SessionExtensionsLoadedDataExtensionsItem] + extensions: list[ExtensionsLoadedExtension] @staticmethod def from_dict(obj: Any) -> "SessionExtensionsLoadedData": assert isinstance(obj, dict) - extensions = from_list(lambda x: SessionExtensionsLoadedDataExtensionsItem.from_dict(x), obj.get("extensions")) + extensions = from_list(lambda x: ExtensionsLoadedExtension.from_dict(x), obj.get("extensions")) return SessionExtensionsLoadedData( extensions=extensions, ) def to_dict(self) -> dict: result: dict = {} - result["extensions"] = from_list(lambda x: to_class(SessionExtensionsLoadedDataExtensionsItem, x), self.extensions) + result["extensions"] = from_list(lambda x: to_class(ExtensionsLoadedExtension, x), self.extensions) return result @@ -4167,6 +4167,12 @@ class SessionStartDataContextHostType(Enum): ADO = "ado" +class SessionResumeDataContextHostType(Enum): + "Hosting platform type of the repository (github or ado)" + GITHUB = "github" + ADO = "ado" + + class SessionPlanChangedDataOperation(Enum): "The type of operation performed on the plan file" CREATE = "create" @@ -4181,7 +4187,7 @@ class SessionWorkspaceFileChangedDataOperation(Enum): class SessionImportLegacyDataLegacySessionChatMessagesItemRole(Enum): - """SessionImportLegacyDataLegacySessionChatMessagesItem discriminator""" + "SessionImportLegacyDataLegacySessionChatMessagesItem discriminator" DEVELOPER = "developer" SYSTEM = "system" USER = "user" @@ -4191,7 +4197,7 @@ class SessionImportLegacyDataLegacySessionChatMessagesItemRole(Enum): class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType(Enum): - """SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem discriminator""" + "SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem discriminator" FUNCTION = "function" CUSTOM = "custom" @@ -4216,19 +4222,25 @@ class SessionImportLegacyDataLegacySessionSelectedModel(Enum): GPT_4_1 = "gpt-4.1" -class SessionHandoffDataSourceType(Enum): +class HandoffSourceType(Enum): "Origin type of the session being handed off" REMOTE = "remote" LOCAL = "local" -class SessionShutdownDataShutdownType(Enum): +class ShutdownType(Enum): "Whether the session ended normally (\"routine\") or due to a crash/fatal error (\"error\")" ROUTINE = "routine" ERROR = "error" -class UserMessageDataAttachmentsItemType(Enum): +class SessionContextChangedDataHostType(Enum): + "Hosting platform type of the repository (github or ado)" + GITHUB = "github" + ADO = "ado" + + +class UserMessageAttachmentType(Enum): "A user message attachment — a file, directory, code selection, blob, or GitHub reference discriminator" FILE = "file" DIRECTORY = "directory" @@ -4237,25 +4249,27 @@ class UserMessageDataAttachmentsItemType(Enum): BLOB = "blob" -class UserMessageDataAttachmentsItemReferenceType(Enum): +class UserMessageAttachmentGithubReferenceType(Enum): "Type of GitHub reference" ISSUE = "issue" PR = "pr" DISCUSSION = "discussion" -class UserMessageDataAgentMode(Enum): +class UserMessageAgentMode(Enum): "The agent mode that was active when this message was sent" INTERACTIVE = "interactive" PLAN = "plan" AUTOPILOT = "autopilot" SHELL = "shell" -class AssistantMessageDataToolRequestsItemType(Enum): + +class AssistantMessageToolRequestType(Enum): "Tool call type: \"function\" for standard tool calls, \"custom\" for grammar-based tool calls. Defaults to \"function\" when absent." FUNCTION = "function" CUSTOM = "custom" + class ToolExecutionCompleteDataResultContentsItemType(Enum): "A content block within a tool result, which may be text, terminal output, image, audio, or a resource discriminator" TEXT = "text" @@ -4304,19 +4318,19 @@ class PermissionRequestedDataPermissionRequestKind(Enum): HOOK = "hook" -class PermissionRequestedDataPermissionRequestAction(Enum): +class PermissionRequestMemoryAction(Enum): "Whether this is a store or vote memory operation" STORE = "store" VOTE = "vote" -class PermissionRequestedDataPermissionRequestDirection(Enum): +class PermissionRequestMemoryDirection(Enum): "Vote direction (vote only)" UPVOTE = "upvote" DOWNVOTE = "downvote" -class PermissionCompletedDataResultKind(Enum): +class PermissionCompletedKind(Enum): "The outcome of the permission request" APPROVED = "approved" DENIED_BY_RULES = "denied-by-rules" @@ -4326,20 +4340,20 @@ class PermissionCompletedDataResultKind(Enum): DENIED_BY_PERMISSION_REQUEST_HOOK = "denied-by-permission-request-hook" -class ElicitationRequestedDataMode(Enum): +class ElicitationRequestedMode(Enum): "Elicitation mode; \"form\" for structured input, \"url\" for browser-based. Defaults to \"form\" when absent." FORM = "form" URL = "url" -class ElicitationCompletedDataAction(Enum): +class ElicitationCompletedAction(Enum): "The user action: \"accept\" (submitted form), \"decline\" (explicitly refused), or \"cancel\" (dismissed)" ACCEPT = "accept" DECLINE = "decline" CANCEL = "cancel" -class SessionMcpServersLoadedDataServersItemStatus(Enum): +class MCPServerStatus(Enum): "Connection status: connected, failed, needs-auth, pending, disabled, or not_configured" CONNECTED = "connected" FAILED = "failed" @@ -4349,13 +4363,23 @@ class SessionMcpServersLoadedDataServersItemStatus(Enum): NOT_CONFIGURED = "not_configured" -class SessionExtensionsLoadedDataExtensionsItemSource(Enum): +class SessionMcpServerStatusChangedDataStatus(Enum): + "New connection status: connected, failed, needs-auth, pending, disabled, or not_configured" + CONNECTED = "connected" + FAILED = "failed" + NEEDS_AUTH = "needs-auth" + PENDING = "pending" + DISABLED = "disabled" + NOT_CONFIGURED = "not_configured" + + +class ExtensionsLoadedExtensionSource(Enum): "Discovery source" PROJECT = "project" USER = "user" -class SessionExtensionsLoadedDataExtensionsItemStatus(Enum): +class ExtensionsLoadedExtensionStatus(Enum): "Current status: running, disabled, failed, or starting" RUNNING = "running" DISABLED = "disabled" @@ -4491,3 +4515,4 @@ def session_event_from_dict(s: Any) -> SessionEvent: def session_event_to_dict(x: SessionEvent) -> Any: return x.to_dict() + diff --git a/python/copilot/session.py b/python/copilot/session.py index 5f664b9ce..443cfc969 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -50,8 +50,8 @@ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, + PermissionRequest, PermissionRequestedData, - PermissionRequestedDataPermissionRequest, SessionErrorData, SessionEvent, SessionIdleData, @@ -242,7 +242,7 @@ class PermissionRequestResult: _PermissionHandlerFn = Callable[ - [PermissionRequestedDataPermissionRequest, dict[str, str]], + [PermissionRequest, dict[str, str]], PermissionRequestResult | Awaitable[PermissionRequestResult], ] @@ -250,7 +250,7 @@ class PermissionRequestResult: class PermissionHandler: @staticmethod def approve_all( - request: PermissionRequestedDataPermissionRequest, invocation: dict[str, str] + request: PermissionRequest, invocation: dict[str, str] ) -> PermissionRequestResult: return PermissionRequestResult(kind="approved") @@ -1625,7 +1625,7 @@ def _register_permission_handler(self, handler: _PermissionHandlerFn | None) -> self._permission_handler = handler async def _handle_permission_request( - self, request: PermissionRequestedDataPermissionRequest + self, request: PermissionRequest ) -> PermissionRequestResult: """ Handle a permission request from the Copilot CLI. diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index 5612161c6..86beb3a5c 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -7,7 +7,7 @@ import pytest from copilot.generated.session_events import ( - PermissionRequestedDataPermissionRequest, + PermissionRequest, SessionIdleData, ToolExecutionCompleteData, ) @@ -25,7 +25,7 @@ async def test_should_invoke_permission_handler_for_write_operations(self, ctx: permission_requests = [] def on_permission_request( - request: PermissionRequestedDataPermissionRequest, invocation: dict + request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: permission_requests.append(request) assert invocation["session_id"] == session.session_id @@ -50,7 +50,7 @@ async def test_should_deny_permission_when_handler_returns_denied(self, ctx: E2E """Test denying permissions""" def on_permission_request( - request: PermissionRequestedDataPermissionRequest, invocation: dict + request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: return PermissionRequestResult(kind="denied-interactively-by-user") @@ -162,7 +162,7 @@ async def test_should_handle_async_permission_handler(self, ctx: E2ETestContext) permission_requests = [] async def on_permission_request( - request: PermissionRequestedDataPermissionRequest, invocation: dict + request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: permission_requests.append(request) # Simulate async permission check (e.g., user prompt) @@ -190,7 +190,7 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC # Resume with permission handler def on_permission_request( - request: PermissionRequestedDataPermissionRequest, invocation: dict + request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: permission_requests.append(request) return PermissionRequestResult(kind="approved") @@ -210,7 +210,7 @@ async def test_should_handle_permission_handler_errors_gracefully(self, ctx: E2E """Test that permission handler errors are handled gracefully""" def on_permission_request( - request: PermissionRequestedDataPermissionRequest, invocation: dict + request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: raise RuntimeError("Handler error") @@ -230,7 +230,7 @@ async def test_should_receive_toolcallid_in_permission_requests(self, ctx: E2ETe received_tool_call_id = False def on_permission_request( - request: PermissionRequestedDataPermissionRequest, invocation: dict + request: PermissionRequest, invocation: dict ) -> PermissionRequestResult: nonlocal received_tool_call_id if request.tool_call_id: diff --git a/python/test_commands_and_elicitation.py b/python/test_commands_and_elicitation.py index 5c5edc763..40f95724c 100644 --- a/python/test_commands_and_elicitation.py +++ b/python/test_commands_and_elicitation.py @@ -579,7 +579,7 @@ async def mock_request(method, params): from copilot.generated.session_events import ( ElicitationRequestedData, - ElicitationRequestedDataRequestedSchema, + ElicitationRequestedSchema, SessionEvent, SessionEventType, ) @@ -588,7 +588,7 @@ async def mock_request(method, params): data=ElicitationRequestedData( request_id="req-schema-1", message="Fill in your details", - requested_schema=ElicitationRequestedDataRequestedSchema( + requested_schema=ElicitationRequestedSchema( type="object", properties={ "name": {"type": "string"}, @@ -638,13 +638,13 @@ async def test_capabilities_changed_event_updates_session(self): from copilot.generated.session_events import ( CapabilitiesChangedData, - CapabilitiesChangedDataUi, + CapabilitiesChangedUI, SessionEvent, SessionEventType, ) event = SessionEvent( - data=CapabilitiesChangedData(ui=CapabilitiesChangedDataUi(elicitation=True)), + data=CapabilitiesChangedData(ui=CapabilitiesChangedUI(elicitation=True)), id="evt-cap-1", timestamp="2025-01-01T00:00:00Z", type=SessionEventType.CAPABILITIES_CHANGED, diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 9447d3fbd..733a9b24b 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -14,15 +14,15 @@ from copilot.generated.session_events import ( Data, - ElicitationCompletedDataAction, - ElicitationRequestedDataMode, - ElicitationRequestedDataRequestedSchema, - PermissionRequestedDataPermissionRequest, - PermissionRequestedDataPermissionRequestAction, + ElicitationCompletedAction, + ElicitationRequestedMode, + ElicitationRequestedSchema, + PermissionRequest, + PermissionRequestMemoryAction, SessionEventType, SessionTaskCompleteData, - UserMessageDataAgentMode, - UserMessageDataAttachmentsItemReferenceType, + UserMessageAgentMode, + UserMessageAttachmentGithubReferenceType, session_event_from_dict, ) @@ -77,12 +77,12 @@ def test_malformed_timestamp_raises_error(self): def test_explicit_generated_symbols_remain_available(self): """Explicit generated helper symbols should remain importable.""" - assert ElicitationCompletedDataAction.ACCEPT.value == "accept" - assert UserMessageDataAgentMode.INTERACTIVE.value == "interactive" - assert ElicitationRequestedDataMode.FORM.value == "form" - assert UserMessageDataAttachmentsItemReferenceType.PR.value == "pr" + assert ElicitationCompletedAction.ACCEPT.value == "accept" + assert UserMessageAgentMode.INTERACTIVE.value == "interactive" + assert ElicitationRequestedMode.FORM.value == "form" + assert UserMessageAttachmentGithubReferenceType.PR.value == "pr" - schema = ElicitationRequestedDataRequestedSchema( + schema = ElicitationRequestedSchema( properties={"answer": {"type": "string"}}, type="object" ) assert schema.to_dict()["type"] == "object" @@ -105,10 +105,8 @@ def test_data_shim_preserves_raw_mapping_values(self): def test_schema_defaults_are_applied_for_missing_optional_fields(self): """Generated event models should honor primitive schema defaults during parsing.""" - request = PermissionRequestedDataPermissionRequest.from_dict( - {"kind": "memory", "fact": "remember this"} - ) - assert request.action == PermissionRequestedDataPermissionRequestAction.STORE + request = PermissionRequest.from_dict({"kind": "memory", "fact": "remember this"}) + assert request.action == PermissionRequestMemoryAction.STORE task_complete = SessionTaskCompleteData.from_dict({"success": True}) assert task_complete.summary == "" diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index b5aba76b6..a47f9ce86 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -233,7 +233,7 @@ interface PyResolvedType { interface PyCodegenCtx { classes: string[]; enums: string[]; - enumsByValues: Map; + enumsByName: Map; generatedNames: Set; usesTimedelta: boolean; usesIntegerTimedelta: boolean; @@ -259,6 +259,58 @@ function wrapSerializer(resolved: PyResolvedType, arg = "x"): string { return `lambda ${arg}: ${resolved.toExpr(arg)}`; } +const PY_SESSION_EVENT_TYPE_RENAMES: Record = { + AssistantMessageDataToolRequestsItem: "AssistantMessageToolRequest", + AssistantMessageDataToolRequestsItemType: "AssistantMessageToolRequestType", + AssistantUsageDataCopilotUsage: "AssistantUsageCopilotUsage", + AssistantUsageDataCopilotUsageTokenDetailsItem: "AssistantUsageCopilotUsageTokenDetail", + AssistantUsageDataQuotaSnapshotsValue: "AssistantUsageQuotaSnapshot", + CapabilitiesChangedDataUi: "CapabilitiesChangedUI", + CommandsChangedDataCommandsItem: "CommandsChangedCommand", + ElicitationCompletedDataAction: "ElicitationCompletedAction", + ElicitationRequestedDataMode: "ElicitationRequestedMode", + ElicitationRequestedDataRequestedSchema: "ElicitationRequestedSchema", + McpOauthRequiredDataStaticClientConfig: "MCPOauthRequiredStaticClientConfig", + PermissionCompletedDataResultKind: "PermissionCompletedKind", + PermissionRequestedDataPermissionRequest: "PermissionRequest", + PermissionRequestedDataPermissionRequestAction: "PermissionRequestMemoryAction", + PermissionRequestedDataPermissionRequestCommandsItem: "PermissionRequestShellCommand", + PermissionRequestedDataPermissionRequestDirection: "PermissionRequestMemoryDirection", + PermissionRequestedDataPermissionRequestPossibleUrlsItem: "PermissionRequestShellPossibleURL", + SessionCompactionCompleteDataCompactionTokensUsed: "CompactionCompleteCompactionTokensUsed", + SessionCustomAgentsUpdatedDataAgentsItem: "CustomAgentsUpdatedAgent", + SessionExtensionsLoadedDataExtensionsItem: "ExtensionsLoadedExtension", + SessionExtensionsLoadedDataExtensionsItemSource: "ExtensionsLoadedExtensionSource", + SessionExtensionsLoadedDataExtensionsItemStatus: "ExtensionsLoadedExtensionStatus", + SessionHandoffDataRepository: "HandoffRepository", + SessionHandoffDataSourceType: "HandoffSourceType", + SessionMcpServersLoadedDataServersItem: "MCPServersLoadedServer", + SessionMcpServersLoadedDataServersItemStatus: "MCPServerStatus", + SessionShutdownDataCodeChanges: "ShutdownCodeChanges", + SessionShutdownDataModelMetricsValue: "ShutdownModelMetric", + SessionShutdownDataModelMetricsValueRequests: "ShutdownModelMetricRequests", + SessionShutdownDataModelMetricsValueUsage: "ShutdownModelMetricUsage", + SessionShutdownDataShutdownType: "ShutdownType", + SessionSkillsLoadedDataSkillsItem: "SkillsLoadedSkill", + UserMessageDataAgentMode: "UserMessageAgentMode", + UserMessageDataAttachmentsItem: "UserMessageAttachment", + UserMessageDataAttachmentsItemLineRange: "UserMessageAttachmentFileLineRange", + UserMessageDataAttachmentsItemReferenceType: "UserMessageAttachmentGithubReferenceType", + UserMessageDataAttachmentsItemSelection: "UserMessageAttachmentSelectionDetails", + UserMessageDataAttachmentsItemSelectionEnd: "UserMessageAttachmentSelectionDetailsEnd", + UserMessageDataAttachmentsItemSelectionStart: "UserMessageAttachmentSelectionDetailsStart", + UserMessageDataAttachmentsItemType: "UserMessageAttachmentType", +}; + +function postProcessPythonSessionEventCode(code: string): string { + for (const [from, to] of Object.entries(PY_SESSION_EVENT_TYPE_RENAMES).sort( + ([left], [right]) => right.length - left.length + )) { + code = code.replace(new RegExp(`\\b${from}\\b`, "g"), to); + } + return code; +} + function pyPrimitiveResolvedType(annotation: string, fromFn: string, toFn = fromFn): PyResolvedType { return { annotation, @@ -394,8 +446,7 @@ function getOrCreatePyEnum( ctx: PyCodegenCtx, description?: string ): string { - const valuesKey = [...values].sort().join("|"); - const existing = ctx.enumsByValues.get(valuesKey); + const existing = ctx.enumsByName.get(enumName); if (existing) { return existing; } @@ -410,7 +461,7 @@ function getOrCreatePyEnum( for (const value of values) { lines.push(` ${toEnumMemberName(value)} = ${JSON.stringify(value)}`); } - ctx.enumsByValues.set(valuesKey, enumName); + ctx.enumsByName.set(enumName, enumName); ctx.enums.push(lines.join("\n")); return enumName; } @@ -882,7 +933,7 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { const ctx: PyCodegenCtx = { classes: [], enums: [], - enumsByValues: new Map(), + enumsByName: new Map(), generatedNames: new Set(), usesTimedelta: false, usesIntegerTimedelta: false, @@ -1188,7 +1239,7 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(``); out.push(``); - return out.join("\n"); + return postProcessPythonSessionEventCode(out.join("\n")); } async function generateSessionEvents(schemaPath?: string): Promise { From 6c3e675d544aa18588111bee06b9d09a15bb112d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 14 Apr 2026 10:48:51 -0400 Subject: [PATCH 13/14] Fix Node.js lint and generated file drift Normalize trailing whitespace in pyDocstringLiteral to prevent cross-platform codegen drift, and reformat test file with Prettier. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/test/python-codegen.test.ts | 17 +++++++++++++---- scripts/codegen/python.ts | 6 +++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/python-codegen.test.ts index ab4e2ff37..4032ce2cc 100644 --- a/nodejs/test/python-codegen.test.ts +++ b/nodejs/test/python-codegen.test.ts @@ -120,7 +120,10 @@ describe("python session event codegen", () => { type: "array", items: { type: "object", - required: ["identifier", "readOnly"], + required: [ + "identifier", + "readOnly", + ], properties: { identifier: { type: "string" }, readOnly: { type: "boolean" }, @@ -136,11 +139,17 @@ describe("python session event codegen", () => { items: { type: "object", required: ["url"], - properties: { url: { type: "string" } }, + properties: { + url: { type: "string" }, + }, }, }, - hasWriteFileRedirection: { type: "boolean" }, - canOfferSessionApproval: { type: "boolean" }, + hasWriteFileRedirection: { + type: "boolean", + }, + canOfferSessionApproval: { + type: "boolean", + }, }, }, { diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index a47f9ce86..c1a80aa06 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -80,7 +80,11 @@ function splitTopLevelCommas(s: string): string[] { } function pyDocstringLiteral(text: string): string { - return JSON.stringify(text); + const normalized = text + .split(/\r?\n/) + .map((line) => line.replace(/\s+$/g, "")) + .join("\n"); + return JSON.stringify(normalized); } function modernizePython(code: string): string { From 69b8425d54a1515f0dbe9784a2dceabc71f01920 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 14 Apr 2026 11:17:00 -0400 Subject: [PATCH 14/14] Regenerate Python events and fix doc example - Regenerate session_events.py to match latest schema (removes session.import_legacy, adds reasoning_tokens to AssistantUsageData) - Update docs/setup/local-cli.md Python example to use match-based type narrowing with None check, consistent with python/README.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/setup/local-cli.md | 6 +- python/copilot/generated/session_events.py | 281 +-------------------- 2 files changed, 16 insertions(+), 271 deletions(-) diff --git a/docs/setup/local-cli.md b/docs/setup/local-cli.md index b7f8de04c..0e2d11020 100644 --- a/docs/setup/local-cli.md +++ b/docs/setup/local-cli.md @@ -54,6 +54,7 @@ await client.stop(); ```python from copilot import CopilotClient +from copilot.generated.session_events import AssistantMessageData from copilot.session import PermissionHandler client = CopilotClient({ @@ -63,7 +64,10 @@ await client.start() session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") response = await session.send_and_wait("Hello!") -print(response.data.content) +if response: + match response.data: + case AssistantMessageData() as data: + print(data.content) await client.stop() ``` diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index c95df5c56..400883850 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -116,7 +116,6 @@ class SessionEventType(Enum): SESSION_MODE_CHANGED = "session.mode_changed" SESSION_PLAN_CHANGED = "session.plan_changed" SESSION_WORKSPACE_FILE_CHANGED = "session.workspace_file_changed" - SESSION_IMPORT_LEGACY = "session.import_legacy" SESSION_HANDOFF = "session.handoff" SESSION_TRUNCATION = "session.truncation" SESSION_SNAPSHOT_REWIND = "session.snapshot_rewind" @@ -714,237 +713,6 @@ def to_dict(self) -> dict: return result -@dataclass -class SessionImportLegacyDataLegacySessionChatMessagesItemAudio: - id: str - - @staticmethod - def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemAudio": - assert isinstance(obj, dict) - id = from_str(obj.get("id")) - return SessionImportLegacyDataLegacySessionChatMessagesItemAudio( - id=id, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["id"] = from_str(self.id) - return result - - -@dataclass -class SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall: - name: str - arguments: str - - @staticmethod - def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall": - assert isinstance(obj, dict) - name = from_str(obj.get("name")) - arguments = from_str(obj.get("arguments")) - return SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall( - name=name, - arguments=arguments, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["name"] = from_str(self.name) - result["arguments"] = from_str(self.arguments) - return result - - -@dataclass -class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction: - name: str - arguments: str - - @staticmethod - def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction": - assert isinstance(obj, dict) - name = from_str(obj.get("name")) - arguments = from_str(obj.get("arguments")) - return SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction( - name=name, - arguments=arguments, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["name"] = from_str(self.name) - result["arguments"] = from_str(self.arguments) - return result - - -@dataclass -class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom: - name: str - input: str - - @staticmethod - def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom": - assert isinstance(obj, dict) - name = from_str(obj.get("name")) - input = from_str(obj.get("input")) - return SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom( - name=name, - input=input, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["name"] = from_str(self.name) - result["input"] = from_str(self.input) - return result - - -@dataclass -class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem: - type: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType - id: str - function: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction | None = None - custom: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom | None = None - - @staticmethod - def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem": - assert isinstance(obj, dict) - type = parse_enum(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType, obj.get("type")) - id = from_str(obj.get("id")) - function = from_union([from_none, lambda x: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction.from_dict(x)], obj.get("function")) - custom = from_union([from_none, lambda x: SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom.from_dict(x)], obj.get("custom")) - return SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem( - type=type, - id=id, - function=function, - custom=custom, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["type"] = to_enum(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType, self.type) - result["id"] = from_str(self.id) - if self.function is not None: - result["function"] = from_union([from_none, lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemFunction, x)], self.function) - if self.custom is not None: - result["custom"] = from_union([from_none, lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemCustom, x)], self.custom) - return result - - -@dataclass -class SessionImportLegacyDataLegacySessionChatMessagesItem: - role: SessionImportLegacyDataLegacySessionChatMessagesItemRole - content: Any = None - name: str | None = None - refusal: str | None = None - audio: SessionImportLegacyDataLegacySessionChatMessagesItemAudio | None = None - function_call: SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall | None = None - tool_calls: list[SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem] | None = None - tool_call_id: str | None = None - - @staticmethod - def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySessionChatMessagesItem": - assert isinstance(obj, dict) - role = parse_enum(SessionImportLegacyDataLegacySessionChatMessagesItemRole, obj.get("role")) - content = obj.get("content") - name = from_union([from_none, lambda x: from_str(x)], obj.get("name")) - refusal = from_union([from_none, lambda x: from_str(x)], obj.get("refusal")) - audio = from_union([from_none, lambda x: SessionImportLegacyDataLegacySessionChatMessagesItemAudio.from_dict(x)], obj.get("audio")) - function_call = from_union([from_none, lambda x: SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall.from_dict(x)], obj.get("function_call")) - tool_calls = from_union([from_none, lambda x: from_list(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem.from_dict, x)], obj.get("tool_calls")) - tool_call_id = from_union([from_none, lambda x: from_str(x)], obj.get("tool_call_id")) - return SessionImportLegacyDataLegacySessionChatMessagesItem( - role=role, - content=content, - name=name, - refusal=refusal, - audio=audio, - function_call=function_call, - tool_calls=tool_calls, - tool_call_id=tool_call_id, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["role"] = to_enum(SessionImportLegacyDataLegacySessionChatMessagesItemRole, self.role) - if self.content is not None: - result["content"] = self.content - if self.name is not None: - result["name"] = from_union([from_none, lambda x: from_str(x)], self.name) - if self.refusal is not None: - result["refusal"] = from_union([from_none, lambda x: from_str(x)], self.refusal) - if self.audio is not None: - result["audio"] = from_union([from_none, lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemAudio, x)], self.audio) - if self.function_call is not None: - result["function_call"] = from_union([from_none, lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemFunctionCall, x)], self.function_call) - if self.tool_calls is not None: - result["tool_calls"] = from_union([from_none, lambda x: from_list(lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem, x), x)], self.tool_calls) - if self.tool_call_id is not None: - result["tool_call_id"] = from_union([from_none, lambda x: from_str(x)], self.tool_call_id) - return result - - -@dataclass -class SessionImportLegacyDataLegacySession: - session_id: str - start_time: datetime - chat_messages: list[SessionImportLegacyDataLegacySessionChatMessagesItem] - timeline: list[Any] - selected_model: SessionImportLegacyDataLegacySessionSelectedModel | None = None - - @staticmethod - def from_dict(obj: Any) -> "SessionImportLegacyDataLegacySession": - assert isinstance(obj, dict) - session_id = from_str(obj.get("sessionId")) - start_time = from_datetime(obj.get("startTime")) - chat_messages = from_list(SessionImportLegacyDataLegacySessionChatMessagesItem.from_dict, obj.get("chatMessages")) - timeline = from_list(lambda x: x, obj.get("timeline")) - selected_model = from_union([from_none, lambda x: parse_enum(SessionImportLegacyDataLegacySessionSelectedModel, x)], obj.get("selectedModel")) - return SessionImportLegacyDataLegacySession( - session_id=session_id, - start_time=start_time, - chat_messages=chat_messages, - timeline=timeline, - selected_model=selected_model, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["sessionId"] = from_str(self.session_id) - result["startTime"] = to_datetime(self.start_time) - result["chatMessages"] = from_list(lambda x: to_class(SessionImportLegacyDataLegacySessionChatMessagesItem, x), self.chat_messages) - result["timeline"] = from_list(lambda x: x, self.timeline) - if self.selected_model is not None: - result["selectedModel"] = from_union([from_none, lambda x: to_enum(SessionImportLegacyDataLegacySessionSelectedModel, x)], self.selected_model) - return result - - -@dataclass -class SessionImportLegacyData: - "Legacy session import data including the complete session JSON, import timestamp, and source file path" - legacy_session: SessionImportLegacyDataLegacySession - import_time: datetime - source_file: str - - @staticmethod - def from_dict(obj: Any) -> "SessionImportLegacyData": - assert isinstance(obj, dict) - legacy_session = SessionImportLegacyDataLegacySession.from_dict(obj.get("legacySession")) - import_time = from_datetime(obj.get("importTime")) - source_file = from_str(obj.get("sourceFile")) - return SessionImportLegacyData( - legacy_session=legacy_session, - import_time=import_time, - source_file=source_file, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["legacySession"] = to_class(SessionImportLegacyDataLegacySession, self.legacy_session) - result["importTime"] = to_datetime(self.import_time) - result["sourceFile"] = from_str(self.source_file) - return result - - @dataclass class HandoffRepository: "Repository context for the handed-off session" @@ -1148,6 +916,7 @@ class ShutdownModelMetricUsage: output_tokens: float cache_read_tokens: float cache_write_tokens: float + reasoning_tokens: float | None = None @staticmethod def from_dict(obj: Any) -> "ShutdownModelMetricUsage": @@ -1156,11 +925,13 @@ def from_dict(obj: Any) -> "ShutdownModelMetricUsage": output_tokens = from_float(obj.get("outputTokens")) cache_read_tokens = from_float(obj.get("cacheReadTokens")) cache_write_tokens = from_float(obj.get("cacheWriteTokens")) + reasoning_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("reasoningTokens")) return ShutdownModelMetricUsage( input_tokens=input_tokens, output_tokens=output_tokens, cache_read_tokens=cache_read_tokens, cache_write_tokens=cache_write_tokens, + reasoning_tokens=reasoning_tokens, ) def to_dict(self) -> dict: @@ -1169,6 +940,8 @@ def to_dict(self) -> dict: result["outputTokens"] = to_float(self.output_tokens) result["cacheReadTokens"] = to_float(self.cache_read_tokens) result["cacheWriteTokens"] = to_float(self.cache_write_tokens) + if self.reasoning_tokens is not None: + result["reasoningTokens"] = from_union([from_none, lambda x: to_float(x)], self.reasoning_tokens) return result @@ -2141,6 +1914,7 @@ class AssistantUsageData: output_tokens: float | None = None cache_read_tokens: float | None = None cache_write_tokens: float | None = None + reasoning_tokens: float | None = None cost: float | None = None duration: float | None = None ttft_ms: float | None = None @@ -2161,6 +1935,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": output_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("outputTokens")) cache_read_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("cacheReadTokens")) cache_write_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("cacheWriteTokens")) + reasoning_tokens = from_union([from_none, lambda x: from_float(x)], obj.get("reasoningTokens")) cost = from_union([from_none, lambda x: from_float(x)], obj.get("cost")) duration = from_union([from_none, lambda x: from_float(x)], obj.get("duration")) ttft_ms = from_union([from_none, lambda x: from_float(x)], obj.get("ttftMs")) @@ -2178,6 +1953,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": output_tokens=output_tokens, cache_read_tokens=cache_read_tokens, cache_write_tokens=cache_write_tokens, + reasoning_tokens=reasoning_tokens, cost=cost, duration=duration, ttft_ms=ttft_ms, @@ -2202,6 +1978,8 @@ def to_dict(self) -> dict: result["cacheReadTokens"] = from_union([from_none, lambda x: to_float(x)], self.cache_read_tokens) if self.cache_write_tokens is not None: result["cacheWriteTokens"] = from_union([from_none, lambda x: to_float(x)], self.cache_write_tokens) + if self.reasoning_tokens is not None: + result["reasoningTokens"] = from_union([from_none, lambda x: to_float(x)], self.reasoning_tokens) if self.cost is not None: result["cost"] = from_union([from_none, lambda x: to_float(x)], self.cost) if self.duration is not None: @@ -4186,42 +3964,6 @@ class SessionWorkspaceFileChangedDataOperation(Enum): UPDATE = "update" -class SessionImportLegacyDataLegacySessionChatMessagesItemRole(Enum): - "SessionImportLegacyDataLegacySessionChatMessagesItem discriminator" - DEVELOPER = "developer" - SYSTEM = "system" - USER = "user" - ASSISTANT = "assistant" - TOOL = "tool" - FUNCTION = "function" - - -class SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItemType(Enum): - "SessionImportLegacyDataLegacySessionChatMessagesItemToolCallsItem discriminator" - FUNCTION = "function" - CUSTOM = "custom" - - -class SessionImportLegacyDataLegacySessionSelectedModel(Enum): - CLAUDE_SONNET_4_6 = "claude-sonnet-4.6" - CLAUDE_SONNET_4_5 = "claude-sonnet-4.5" - CLAUDE_HAIKU_4_5 = "claude-haiku-4.5" - CLAUDE_OPUS_4_6 = "claude-opus-4.6" - CLAUDE_OPUS_4_6_FAST = "claude-opus-4.6-fast" - CLAUDE_OPUS_4_6_1M = "claude-opus-4.6-1m" - CLAUDE_OPUS_4_5 = "claude-opus-4.5" - CLAUDE_SONNET_4 = "claude-sonnet-4" - GOLDENEYE = "goldeneye" - GPT_5_4 = "gpt-5.4" - GPT_5_3_CODEX = "gpt-5.3-codex" - GPT_5_2_CODEX = "gpt-5.2-codex" - GPT_5_2 = "gpt-5.2" - GPT_5_1 = "gpt-5.1" - GPT_5_4_MINI = "gpt-5.4-mini" - GPT_5_MINI = "gpt-5-mini" - GPT_4_1 = "gpt-4.1" - - class HandoffSourceType(Enum): "Origin type of the session being handed off" REMOTE = "remote" @@ -4387,7 +4129,7 @@ class ExtensionsLoadedExtensionStatus(Enum): STARTING = "starting" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPlanChangedData | SessionWorkspaceFileChangedData | SessionImportLegacyData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | RawSessionEventData | Data +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPlanChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | RawSessionEventData | Data @dataclass @@ -4423,7 +4165,6 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_MODE_CHANGED: data = SessionModeChangedData.from_dict(data_obj) case SessionEventType.SESSION_PLAN_CHANGED: data = SessionPlanChangedData.from_dict(data_obj) case SessionEventType.SESSION_WORKSPACE_FILE_CHANGED: data = SessionWorkspaceFileChangedData.from_dict(data_obj) - case SessionEventType.SESSION_IMPORT_LEGACY: data = SessionImportLegacyData.from_dict(data_obj) case SessionEventType.SESSION_HANDOFF: data = SessionHandoffData.from_dict(data_obj) case SessionEventType.SESSION_TRUNCATION: data = SessionTruncationData.from_dict(data_obj) case SessionEventType.SESSION_SNAPSHOT_REWIND: data = SessionSnapshotRewindData.from_dict(data_obj)