From 74d272bc5818923d36b63b7d6261d1d0364f7858 Mon Sep 17 00:00:00 2001 From: Karl Krukow Date: Wed, 17 Jun 2026 10:02:39 +0200 Subject: [PATCH 1/6] chore(schema): bump CLI schema 1.0.61 -> 1.0.63 and regenerate Pins @github/copilot to 1.0.63 and regenerates generated/event_specs.clj from the refreshed JSON Schemas. Pulls in new wire surface: the session.todos_changed event, optional AssistantUsageData fields (contentFilterTriggered, finishReason), ToolExecutionCompleteResult structuredContent, ToolExecutionStartData toolDescription, and the plugin/session ExtensionSource values. Upstream: https://github.com/github/copilot-sdk/pull/1686 (1.0.63), commit a115246a (1.0.62). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .copilot-schema-version | 2 +- schemas/README.md | 2 +- schemas/api.schema.json | 425 +++++++++++++++++- schemas/session-events.schema.json | 213 ++++++++- .../copilot_sdk/generated/event_specs.clj | 26 +- 5 files changed, 633 insertions(+), 35 deletions(-) diff --git a/.copilot-schema-version b/.copilot-schema-version index 9972f124..32c4ece7 100644 --- a/.copilot-schema-version +++ b/.copilot-schema-version @@ -1 +1 @@ -1.0.61 +1.0.63 diff --git a/schemas/README.md b/schemas/README.md index ac2c67e6..5f8fbcf7 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -4,4 +4,4 @@ These files are fetched verbatim from the `@github/copilot` npm package at the v **Do not edit by hand.** To update, run `bb schemas:fetch` after bumping `.copilot-schema-version`. -Currently pinned version: `1.0.61` +Currently pinned version: `1.0.63` diff --git a/schemas/api.schema.json b/schemas/api.schema.json index 9758ba24..182a58a5 100644 --- a/schemas/api.schema.json +++ b/schemas/api.schema.json @@ -1560,6 +1560,29 @@ "description": "Todo rows read from the session SQL database. Empty when no session database is available." }, "stability": "experimental" + }, + "readSqlTodosWithDependencies": { + "rpcMethod": "session.plan.readSqlTodosWithDependencies", + "description": "Reads todo rows AND dependency edges from the session SQL database for structured progress UI. Same defensive behavior as readSqlTodos — returns empty arrays when the database, tables, or columns aren't available. Clients should call this on session start and after every `session.todos_changed` event to refresh structured-UI rendering.", + "params": { + "type": "object", + "description": "Identifies the target session.", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + } + }, + "required": [ + "sessionId" + ], + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/PlanReadSqlTodosWithDependenciesResult", + "description": "Todo rows + dependency edges read from the session SQL database." + }, + "stability": "experimental" } }, "workspaces": { @@ -1766,6 +1789,10 @@ "mode": { "$ref": "#/definitions/WorkspaceDiffMode", "description": "Diff mode requested by the client." + }, + "ignoreWhitespace": { + "type": "boolean", + "description": "When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false." } }, "required": [ @@ -3330,6 +3357,45 @@ "stability": "experimental" } }, + "provider": { + "getEndpoint": { + "rpcMethod": "session.provider.getEndpoint", + "description": "Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses.", + "params": { + "anyOf": [ + { + "not": {} + }, + { + "type": "object", + "properties": { + "modelId": { + "type": "string", + "description": "Model identifier the caller intends to use against the returned endpoint. Used to pick the correct wire shape. Omit to use whichever model the session is currently using." + } + }, + "additionalProperties": false + } + ], + "description": "Optional model identifier to scope the endpoint snapshot to.", + "title": "ProviderGetEndpointRequest", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + } + }, + "required": [ + "sessionId" + ] + }, + "result": { + "$ref": "#/definitions/ProviderEndpoint", + "description": "A snapshot of the provider endpoint the session is currently configured to talk to." + }, + "stability": "experimental" + } + }, "options": { "update": { "rpcMethod": "session.options.update", @@ -3838,6 +3904,35 @@ "description": "Current lightweight tool metadata snapshot for the session." }, "stability": "experimental" + }, + "updateSubagentSettings": { + "rpcMethod": "session.tools.updateSubagentSettings", + "description": "Updates the current session's live subagent settings after user settings change. The persisted user settings remain the source of truth for future sessions.", + "params": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Target session identifier" + }, + "subagents": { + "$ref": "#/definitions/SubagentSettings", + "description": "Subagent settings to apply, or null to clear the live session override" + } + }, + "required": [ + "sessionId", + "subagents" + ], + "additionalProperties": false, + "description": "Subagent settings to apply to the current session", + "title": "UpdateSubagentSettingsRequest" + }, + "result": { + "$ref": "#/definitions/ToolsUpdateSubagentSettingsResult", + "description": "Empty result after applying subagent settings" + }, + "stability": "experimental" } }, "commands": { @@ -8606,6 +8701,9 @@ "token_based_billing": { "type": "boolean" }, + "can_upgrade_plan": { + "type": "boolean" + }, "quota_reset_date_utc": { "type": "string" }, @@ -9309,7 +9407,7 @@ "properties": { "id": { "type": "string", - "description": "Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper')" + "description": "Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper', 'plugin:my-plugin:my-ext')" }, "name": { "type": "string", @@ -9317,7 +9415,7 @@ }, "source": { "$ref": "#/definitions/ExtensionSource", - "description": "Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)" + "description": "Discovery source: project (.github/extensions/), user (~/.copilot/extensions/), plugin (installed plugin), or session (session-state//extensions/)" }, "status": { "$ref": "#/definitions/ExtensionStatus", @@ -9419,13 +9517,17 @@ "type": "string", "enum": [ "project", - "user" + "user", + "plugin", + "session" ], - "description": "Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)", + "description": "Discovery source: project (.github/extensions/), user (~/.copilot/extensions/), plugin (installed plugin), or session (session-state//extensions/)", "title": "ExtensionSource", "x-enumDescriptions": { "project": "Extension discovered from the current project's .github/extensions directory.", - "user": "Extension discovered from the user's ~/.copilot/extensions directory." + "user": "Extension discovered from the user's ~/.copilot/extensions directory.", + "plugin": "Extension contributed by an installed plugin.", + "session": "Extension discovered from the current session's state directory (loaded only for this session)." } }, "ExtensionStatus": { @@ -12001,6 +12103,19 @@ "title": "McpServerConfig", "x-opaque-json": true }, + "McpServerConfigDeferTools": { + "type": "string", + "enum": [ + "auto", + "never" + ], + "description": "Controls if tools provided by this server can be loaded on demand via tool search (auto) or always included in the initial tool list (never)", + "title": "McpServerConfigDeferTools", + "x-enumDescriptions": { + "auto": "Tools may be deferred under certain conditions", + "never": "Tools are always included in the initial tool list, even when tool search is enabled." + } + }, "McpServerConfigHttp": { "type": "object", "properties": { @@ -12037,6 +12152,10 @@ "$ref": "#/definitions/McpServerAuthConfig", "description": "Set to `true` to use defaults, or provide an object with additional auth or OIDC settings." }, + "deferTools": { + "$ref": "#/definitions/McpServerConfigDeferTools", + "description": "Controls if tools provided by this server can be loaded on demand via tool search (auto) or always included in the initial tool list (never)" + }, "url": { "type": "string", "format": "uri", @@ -12128,6 +12247,10 @@ "$ref": "#/definitions/McpServerAuthConfig", "description": "Set to `true` to use defaults, or provide an object with additional auth or OIDC settings." }, + "deferTools": { + "$ref": "#/definitions/McpServerConfigDeferTools", + "description": "Controls if tools provided by this server can be loaded on demand via tool search (auto) or always included in the initial tool list (never)" + }, "command": { "type": "string", "description": "Executable command used to start the Stdio MCP server process." @@ -12396,6 +12519,21 @@ "title": "McpUnregisterExternalClientRequest", "visibility": "internal" }, + "MemoryConfiguration": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether memory is enabled for the session." + } + }, + "required": [ + "enabled" + ], + "additionalProperties": false, + "description": "Memory configuration for this session.", + "title": "MemoryConfiguration" + }, "MetadataContextInfoRequest": { "type": "object", "properties": { @@ -15404,7 +15542,8 @@ "rows": { "type": "array", "items": { - "$ref": "#/definitions/PlanSqlTodosRow" + "$ref": "#/definitions/PlanSqlTodosRow", + "description": "A single todo row read from the session SQL `todos` table. All fields are optional because the SQL schema is best-effort and the agent may not have populated every column." }, "description": "Rows from the session SQL todos table, ordered by creation time and id." } @@ -15416,6 +15555,54 @@ "description": "Todo rows read from the session SQL database. Empty when no session database is available.", "title": "PlanReadSqlTodosResult" }, + "PlanReadSqlTodosWithDependenciesResult": { + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "#/definitions/PlanSqlTodosRow", + "description": "A single todo row read from the session SQL `todos` table. All fields are optional because the SQL schema is best-effort and the agent may not have populated every column." + }, + "description": "Rows from the session SQL todos table, ordered by creation time and id. Empty when no database, no todos table, or the SELECT failed." + }, + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/PlanSqlTodoDependency", + "description": "A single dependency edge read from the session SQL `todo_deps` table, indicating that one todo must complete before another." + }, + "description": "Edges from the session SQL todo_deps table. Empty when no database, no todo_deps table, or the SELECT failed. Read independently from `rows`, so a broken todo_deps table does not affect the rows result and vice versa." + } + }, + "required": [ + "rows", + "dependencies" + ], + "additionalProperties": false, + "description": "Todo rows + dependency edges read from the session SQL database.", + "title": "PlanReadSqlTodosWithDependenciesResult" + }, + "PlanSqlTodoDependency": { + "type": "object", + "properties": { + "todoId": { + "type": "string", + "description": "ID of the todo that has the dependency." + }, + "dependsOn": { + "type": "string", + "description": "ID of the todo it depends on." + } + }, + "required": [ + "todoId", + "dependsOn" + ], + "additionalProperties": false, + "description": "A single dependency edge read from the session SQL `todo_deps` table, indicating that one todo must complete before another.", + "title": "PlanSqlTodoDependency" + }, "PlanSqlTodosRow": { "type": "object", "properties": { @@ -15437,8 +15624,8 @@ } }, "additionalProperties": false, - "title": "PlanSqlTodosRow", - "description": "Schema for the `PlanSqlTodosRow` type." + "description": "A single todo row read from the session SQL `todos` table. All fields are optional because the SQL schema is best-effort and the agent may not have populated every column.", + "title": "PlanSqlTodosRow" }, "PlanUpdateRequest": { "type": "object", @@ -15941,6 +16128,123 @@ "responses": "OpenAI Responses API wire format." } }, + "ProviderEndpoint": { + "type": "object", + "properties": { + "type": { + "$ref": "#/definitions/ProviderEndpointType", + "description": "Provider family. Matches the `type` field of a BYOK provider config." + }, + "wireApi": { + "$ref": "#/definitions/ProviderEndpointWireApi", + "description": "Wire API to be used, when required for the provider type." + }, + "baseUrl": { + "type": "string", + "format": "uri", + "description": "Base URL to pass to the LLM client library." + }, + "apiKey": { + "type": "string", + "description": "A credential the caller should use with this endpoint. Omitted only when the endpoint accepts unauthenticated requests." + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "HTTP headers the caller must include on every outbound request." + }, + "sessionToken": { + "$ref": "#/definitions/ProviderSessionToken", + "description": "Short-lived, rotating credential the caller must send on every request, in addition to `apiKey` if one is present. Omitted when the endpoint does not require one." + } + }, + "required": [ + "type", + "baseUrl", + "headers" + ], + "additionalProperties": false, + "description": "A snapshot of the provider endpoint the session is currently configured to talk to.", + "title": "ProviderEndpoint" + }, + "ProviderEndpointType": { + "type": "string", + "enum": [ + "openai", + "azure", + "anthropic" + ], + "description": "Provider family. Matches the `type` field of a BYOK provider config.", + "title": "ProviderEndpointType", + "x-enumDescriptions": { + "openai": "OpenAI-compatible endpoint (use the OpenAI client library).", + "azure": "Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL).", + "anthropic": "Anthropic endpoint (use the Anthropic client library)." + } + }, + "ProviderEndpointWireApi": { + "type": "string", + "enum": [ + "completions", + "responses" + ], + "description": "Wire API to be used, when required for the provider type.", + "title": "ProviderEndpointWireApi", + "x-enumDescriptions": { + "completions": "Classic chat-completions request shape.", + "responses": "Newer responses request shape." + } + }, + "ProviderGetEndpointRequest": { + "anyOf": [ + { + "not": {} + }, + { + "type": "object", + "properties": { + "modelId": { + "type": "string", + "description": "Model identifier the caller intends to use against the returned endpoint. Used to pick the correct wire shape. Omit to use whichever model the session is currently using." + } + }, + "additionalProperties": false + } + ], + "description": "Optional model identifier to scope the endpoint snapshot to.", + "title": "ProviderGetEndpointRequest" + }, + "ProviderSessionToken": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "The short-lived token value." + }, + "header": { + "type": "string", + "description": "HTTP header name the token must be sent under." + }, + "model": { + "type": "string", + "description": "The model the token is bound to, when applicable. When set, the token is only valid for requests against this model." + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "description": "When the token expires, if known. Callers should refresh by calling `getEndpoint` again before this time, or reactively on any 401/403 response from `baseUrl`." + } + }, + "required": [ + "token", + "header" + ], + "additionalProperties": false, + "description": "Short-lived, rotating credential the caller must send on every request, in addition to `apiKey` if one is present. Omitted when the endpoint does not require one.", + "title": "ProviderSessionToken" + }, "PushAttachment": { "anyOf": [ { @@ -18617,6 +18921,11 @@ "type": "string", "description": "Stable integration identifier for analytics." }, + "expAssignments": { + "description": "ExP assignment ('flight') data injected by an SDK integrator, in the same JSON shape the Copilot CLI fetches from the experimentation service (CopilotExpAssignmentResponse). When supplied this is fed into the FeatureFlagService exactly like CLI-fetched assignments and ExP-backed flags wait for it. When absent the session does not block on ExP.", + "visibility": "internal", + "x-opaque-json": true + }, "featureFlags": { "type": "object", "additionalProperties": { @@ -18797,6 +19106,10 @@ "description": "Additional content-exclusion policies to merge into the session policy set.", "stability": "experimental" }, + "memory": { + "$ref": "#/definitions/MemoryConfiguration", + "description": "Memory configuration for this session." + }, "sessionCapabilities": { "type": "array", "items": { @@ -20814,6 +21127,10 @@ "experimental": { "type": "boolean", "description": "Whether the command is experimental" + }, + "schedulable": { + "type": "boolean", + "description": "Whether the command may be the target of `/every` / `/after` schedules. Resolution happens at every tick, so only set this when the command is safe to re-invoke and produces an agent prompt." } }, "required": [ @@ -20992,6 +21309,72 @@ "title": "SlashCommandTextResult", "description": "Schema for the `SlashCommandTextResult` type." }, + "SubagentSettings": { + "anyOf": [ + { + "type": "object", + "properties": { + "agents": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/SubagentSettingsEntry", + "description": "Subagent model, reasoning effort, and context tier settings" + }, + "description": "Per-agent settings keyed by subagent agent_type" + }, + "disabledSubagents": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of subagents the user has turned off; they cannot be dispatched" + } + }, + "additionalProperties": false, + "description": "Configured per-agent subagent overrides" + }, + { + "type": "null" + } + ], + "description": "Subagent settings to apply, or null to clear the live session override", + "title": "SubagentSettings" + }, + "SubagentSettingsEntry": { + "type": "object", + "properties": { + "model": { + "type": "string", + "description": "Model override for matching subagents" + }, + "effortLevel": { + "type": "string", + "description": "Reasoning effort override for matching subagents" + }, + "contextTier": { + "$ref": "#/definitions/SubagentSettingsEntryContextTier", + "description": "Context tier override for matching subagents" + } + }, + "additionalProperties": false, + "description": "Subagent model, reasoning effort, and context tier settings", + "title": "SubagentSettingsEntry" + }, + "SubagentSettingsEntryContextTier": { + "type": "string", + "enum": [ + "inherit", + "default", + "long_context" + ], + "description": "Context tier override for matching subagents", + "title": "SubagentSettingsEntryContextTier", + "x-enumDescriptions": { + "inherit": "Inherit the parent session's effective context tier at dispatch time.", + "default": "Use the model's default context window.", + "long_context": "Pin the subagent to the long-context tier when supported." + } + }, "TaskAgentInfo": { "type": "object", "properties": { @@ -21728,6 +22111,13 @@ "description": "Optional model identifier whose tool overrides should be applied to the listing.", "title": "ToolsListRequest" }, + "ToolsUpdateSubagentSettingsResult": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Empty result after applying subagent settings", + "title": "ToolsUpdateSubagentSettingsResult" + }, "UIAutoModeSwitchResponse": { "type": "string", "enum": [ @@ -22568,6 +22958,21 @@ "title": "UIUserInputResponse", "description": "Schema for the `UIUserInputResponse` type." }, + "UpdateSubagentSettingsRequest": { + "type": "object", + "properties": { + "subagents": { + "$ref": "#/definitions/SubagentSettings", + "description": "Subagent settings to apply, or null to clear the live session override" + } + }, + "required": [ + "subagents" + ], + "additionalProperties": false, + "description": "Subagent settings to apply to the current session", + "title": "UpdateSubagentSettingsRequest" + }, "UsageGetMetricsResult": { "type": "object", "properties": { @@ -23219,6 +23624,10 @@ "mode": { "$ref": "#/definitions/WorkspaceDiffMode", "description": "Diff mode requested by the client." + }, + "ignoreWhitespace": { + "type": "boolean", + "description": "When true, ignore whitespace-only changes (git `--ignore-all-space`). Defaults to false." } }, "required": [ diff --git a/schemas/session-events.schema.json b/schemas/session-events.schema.json index b08ee4e8..649519b1 100644 --- a/schemas/session-events.schema.json +++ b/schemas/session-events.schema.json @@ -216,18 +216,9 @@ "type": "string", "description": "Provider's completion / response identifier; shared across all chunks of a single API call. Used to group multi-chunk assistant utterances." }, - "anthropicAdvisorBlocks": { - "type": "array", - "items": { - "x-opaque-json": true - }, - "description": "Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping", - "stability": "experimental" - }, - "anthropicAdvisorModel": { - "type": "string", - "description": "Anthropic advisor model ID used for this response, for timeline display on replay", - "stability": "experimental" + "serverTools": { + "$ref": "#/definitions/AssistantMessageServerTools", + "description": "Neutral provider-tagged server-side tool-use payload (tool search, advisor) for verbatim round-tripping" }, "turnId": { "type": "string", @@ -382,6 +373,42 @@ "description": "Session event \"assistant.message\". Assistant response containing text content, optional tool requests, and interaction metadata", "title": "AssistantMessageEvent" }, + "AssistantMessageServerTools": { + "type": "object", + "properties": { + "provider": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "x-opaque-json": true + } + }, + "functionCallNamespaces": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "rawContentBlocks": { + "type": "array", + "items": { + "x-opaque-json": true + } + }, + "advisorModel": { + "type": "string" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false, + "description": "Neutral provider-tagged server-side tool-use payload (tool search, advisor) for verbatim round-tripping", + "title": "AssistantMessageServerTools", + "stability": "experimental" + }, "AssistantMessageStartData": { "type": "object", "properties": { @@ -1054,6 +1081,14 @@ "reasoningEffort": { "type": "string", "description": "Reasoning effort level used for model calls, if applicable (e.g. \"none\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\")" + }, + "finishReason": { + "type": "string", + "description": "Finish reason reported by the model for this API call (e.g. \"stop\", \"length\", \"tool_calls\", \"content_filter\"). Normalized to OpenAI vocabulary; for Anthropic models a \"refusal\" stop reason maps to \"content_filter\"." + }, + "contentFilterTriggered": { + "type": "boolean", + "description": "Whether the model response was blocked or truncated by content filtering (finish_reason === 'content_filter'). For Anthropic models this corresponds to a 'refusal' stop reason." } }, "required": [ @@ -4065,7 +4100,7 @@ "properties": { "id": { "type": "string", - "description": "Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper')" + "description": "Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper', 'plugin:my-plugin:my-ext')" }, "name": { "type": "string", @@ -4094,13 +4129,17 @@ "type": "string", "enum": [ "project", - "user" + "user", + "plugin", + "session" ], "description": "Discovery source", "title": "ExtensionsLoadedExtensionSource", "x-enumDescriptions": { "project": "Extension discovered from the current project.", - "user": "Extension discovered from the user's extension directory." + "user": "Extension discovered from the user's extension directory.", + "plugin": "Extension contributed by an installed plugin.", + "session": "Extension discovered from the current session's state directory." } }, "ExtensionsLoadedExtensionStatus": { @@ -7919,6 +7958,10 @@ "$ref": "#/definitions/PlanChangedEvent", "description": "Session event \"session.plan_changed\". Plan file operation details indicating what changed" }, + { + "$ref": "#/definitions/TodosChangedEvent", + "description": "Session event \"session.todos_changed\". Signal-only event: the agent's todos or todo_deps table was written to. No payload — clients should call session.plan.readSqlTodosWithDependencies() to fetch the current state. Events arrive in order; clients can debounce on arrival if needed." + }, { "$ref": "#/definitions/WorkspaceFileChangedEvent", "description": "Session event \"session.workspace_file_changed\". Workspace file change details including path and operation type" @@ -8570,7 +8613,7 @@ }, "source": { "type": "string", - "description": "Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), personal-claude (~/.claude/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill)" + "description": "Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill)" }, "pluginName": { "type": "string", @@ -10012,6 +10055,69 @@ "description": "Session event \"session.title_changed\". Session title change payload containing the new display title", "title": "TitleChangedEvent" }, + "TodosChangedData": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "description": "Signal-only event: the agent's todos or todo_deps table was written to. No payload — clients should call session.plan.readSqlTodosWithDependencies() to fetch the current state. Events arrive in order; clients can debounce on arrival if needed.", + "title": "TodosChangedData" + }, + "TodosChangedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Unique event identifier (UUID v4), generated when the event is emitted" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the event was created" + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "description": "ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event." + }, + "ephemeral": { + "type": "boolean", + "const": true, + "description": "Always true for events that are transient and not persisted to the session event log on disk." + }, + "agentId": { + "type": "string", + "description": "Sub-agent instance identifier. Absent for events from the root/main agent and session-level events." + }, + "type": { + "type": "string", + "const": "session.todos_changed", + "description": "Type discriminator. Always \"session.todos_changed\"." + }, + "data": { + "$ref": "#/definitions/TodosChangedData", + "description": "Signal-only event: the agent's todos or todo_deps table was written to. No payload — clients should call session.plan.readSqlTodosWithDependencies() to fetch the current state. Events arrive in order; clients can debounce on arrival if needed." + } + }, + "required": [ + "id", + "timestamp", + "parentId", + "ephemeral", + "type", + "data" + ], + "additionalProperties": false, + "description": "Session event \"session.todos_changed\". Signal-only event: the agent's todos or todo_deps table was written to. No payload — clients should call session.plan.readSqlTodosWithDependencies() to fetch the current state. Events arrive in order; clients can debounce on arrival if needed.", + "title": "TodosChangedEvent" + }, "ToolExecutionCompleteContent": { "anyOf": [ { @@ -10432,6 +10538,10 @@ "uiResource": { "$ref": "#/definitions/ToolExecutionCompleteUIResource", "description": "MCP Apps UI resource content for rendering in a sandboxed iframe" + }, + "structuredContent": { + "description": "Structured content (arbitrary JSON) returned verbatim by the MCP tool", + "x-opaque-json": true } }, "required": [ @@ -10845,6 +10955,10 @@ "type": "boolean", "description": "When true, the tool output should be displayed expanded (verbatim) in the CLI timeline" }, + "toolDescription": { + "$ref": "#/definitions/ToolExecutionStartToolDescription", + "description": "Tool definition metadata, present for MCP tools with MCP Apps support" + }, "parentToolCallId": { "type": "string", "description": "Tool call ID of the parent tool invocation when this event originates from a sub-agent", @@ -10913,6 +11027,73 @@ "description": "Session event \"tool.execution_start\". Tool execution startup details including MCP server information when applicable", "title": "ToolExecutionStartEvent" }, + "ToolExecutionStartToolDescription": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Tool name" + }, + "description": { + "type": "string", + "description": "Tool description" + }, + "_meta": { + "$ref": "#/definitions/ToolExecutionStartToolDescriptionMeta", + "description": "MCP Apps metadata for UI resource association" + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "description": "Tool definition metadata, present for MCP tools with MCP Apps support", + "title": "ToolExecutionStartToolDescription" + }, + "ToolExecutionStartToolDescriptionMeta": { + "type": "object", + "properties": { + "ui": { + "$ref": "#/definitions/ToolExecutionStartToolDescriptionMetaUI", + "description": "Schema for the `ToolExecutionStartToolDescriptionMetaUI` type." + } + }, + "additionalProperties": false, + "description": "MCP Apps metadata for UI resource association", + "title": "ToolExecutionStartToolDescriptionMeta" + }, + "ToolExecutionStartToolDescriptionMetaUI": { + "type": "object", + "properties": { + "resourceUri": { + "type": "string", + "description": "URI of the UI resource" + }, + "visibility": { + "type": "array", + "items": { + "$ref": "#/definitions/ToolExecutionStartToolDescriptionMetaUIVisibility" + }, + "description": "Who can access this tool" + } + }, + "additionalProperties": false, + "title": "ToolExecutionStartToolDescriptionMetaUI", + "description": "Schema for the `ToolExecutionStartToolDescriptionMetaUI` type." + }, + "ToolExecutionStartToolDescriptionMetaUIVisibility": { + "type": "string", + "enum": [ + "model", + "app" + ], + "title": "ToolExecutionStartToolDescriptionMetaUIVisibility", + "x-enumDescriptions": { + "model": "Tool is callable by the model (LLM tool surface)", + "app": "Tool is callable by the MCP App view (iframe) via session.mcp.apps.callTool" + }, + "description": "Allowed values for the `ToolExecutionStartToolDescriptionMetaUIVisibility` enumeration." + }, "ToolsUpdatedData": { "type": "object", "properties": { diff --git a/src/github/copilot_sdk/generated/event_specs.clj b/src/github/copilot_sdk/generated/event_specs.clj index 3e25ff38..4740db74 100644 --- a/src/github/copilot_sdk/generated/event_specs.clj +++ b/src/github/copilot_sdk/generated/event_specs.clj @@ -31,10 +31,6 @@ (s/def :github.copilot-sdk.generated.event-specs/answer clojure.core/string?) -(s/def :github.copilot-sdk.generated.event-specs/anthropic-advisor-blocks (s/coll-of clojure.core/any?)) - -(s/def :github.copilot-sdk.generated.event-specs/anthropic-advisor-model clojure.core/string?) - (s/def :github.copilot-sdk.generated.event-specs/api-call-id clojure.core/string?) (s/def :github.copilot-sdk.generated.event-specs/api-endpoint #{"/v1/messages" "/chat/completions" "/responses" "ws:/responses"}) @@ -85,6 +81,8 @@ (s/def :github.copilot-sdk.generated.event-specs/content (s/spec (fn [v947] (or (s/valid? clojure.core/string? v947) (s/valid? clojure.core/map? v947))))) +(s/def :github.copilot-sdk.generated.event-specs/content-filter-triggered clojure.core/boolean?) + (s/def :github.copilot-sdk.generated.event-specs/context (s/spec (fn [v944] (or (s/valid? clojure.core/map? v944) (s/valid? clojure.core/string? v944))))) (s/def :github.copilot-sdk.generated.event-specs/context-tier (s/nilable #{"long_context" "default"})) @@ -159,6 +157,8 @@ (s/def :github.copilot-sdk.generated.event-specs/feedback clojure.core/string?) +(s/def :github.copilot-sdk.generated.event-specs/finish-reason clojure.core/string?) + (s/def :github.copilot-sdk.generated.event-specs/git-root clojure.core/string?) (s/def :github.copilot-sdk.generated.event-specs/handoff-time clojure.core/string?) @@ -353,6 +353,8 @@ (s/def :github.copilot-sdk.generated.event-specs/server-name clojure.core/string?) +(s/def :github.copilot-sdk.generated.event-specs/server-tools clojure.core/map?) + (s/def :github.copilot-sdk.generated.event-specs/server-url clojure.core/string?) (s/def :github.copilot-sdk.generated.event-specs/servers (s/coll-of clojure.core/map?)) @@ -451,7 +453,7 @@ (s/def :github.copilot-sdk.generated.event-specs/turn-id clojure.core/string?) -(s/def :github.copilot-sdk.generated.event-specs/type (s/spec (fn [v949] (or (s/valid? #{"session.start"} v949) (s/valid? #{"session.resume"} v949) (s/valid? #{"session.remote_steerable_changed"} v949) (s/valid? #{"session.error"} v949) (s/valid? #{"session.idle"} v949) (s/valid? #{"session.title_changed"} v949) (s/valid? #{"session.schedule_created"} v949) (s/valid? #{"session.schedule_cancelled"} v949) (s/valid? #{"session.autopilot_objective_changed"} v949) (s/valid? #{"session.info"} v949) (s/valid? #{"session.warning"} v949) (s/valid? #{"session.model_change"} v949) (s/valid? #{"session.mode_changed"} v949) (s/valid? #{"session.permissions_changed"} v949) (s/valid? #{"session.plan_changed"} v949) (s/valid? #{"session.workspace_file_changed"} v949) (s/valid? #{"session.handoff"} v949) (s/valid? #{"session.truncation"} v949) (s/valid? #{"session.snapshot_rewind"} v949) (s/valid? #{"session.shutdown"} v949) (s/valid? #{"session.context_changed"} v949) (s/valid? #{"session.usage_info"} v949) (s/valid? #{"session.compaction_start"} v949) (s/valid? #{"session.compaction_complete"} v949) (s/valid? #{"session.task_complete"} v949) (s/valid? #{"user.message"} v949) (s/valid? #{"pending_messages.modified"} v949) (s/valid? #{"assistant.turn_start"} v949) (s/valid? #{"assistant.intent"} v949) (s/valid? #{"assistant.reasoning"} v949) (s/valid? #{"assistant.reasoning_delta"} v949) (s/valid? #{"assistant.streaming_delta"} v949) (s/valid? #{"assistant.message"} v949) (s/valid? #{"assistant.message_start"} v949) (s/valid? #{"assistant.message_delta"} v949) (s/valid? #{"assistant.turn_end"} v949) (s/valid? #{"assistant.usage"} v949) (s/valid? #{"model.call_failure"} v949) (s/valid? #{"abort"} v949) (s/valid? #{"tool.user_requested"} v949) (s/valid? #{"tool.execution_start"} v949) (s/valid? #{"tool.execution_partial_result"} v949) (s/valid? #{"tool.execution_progress"} v949) (s/valid? #{"tool.execution_complete"} v949) (s/valid? #{"skill.invoked"} v949) (s/valid? #{"subagent.started"} v949) (s/valid? #{"subagent.completed"} v949) (s/valid? #{"subagent.failed"} v949) (s/valid? #{"subagent.selected"} v949) (s/valid? #{"subagent.deselected"} v949) (s/valid? #{"hook.start"} v949) (s/valid? #{"hook.end"} v949) (s/valid? #{"hook.progress"} v949) (s/valid? #{"system.message"} v949) (s/valid? #{"system.notification"} v949) (s/valid? #{"permission.requested"} v949) (s/valid? #{"permission.completed"} v949) (s/valid? #{"user_input.requested"} v949) (s/valid? #{"user_input.completed"} v949) (s/valid? #{"elicitation.requested"} v949) (s/valid? #{"elicitation.completed"} v949) (s/valid? #{"sampling.requested"} v949) (s/valid? #{"sampling.completed"} v949) (s/valid? #{"mcp.oauth_required"} v949) (s/valid? #{"mcp.oauth_completed"} v949) (s/valid? #{"session.custom_notification"} v949) (s/valid? #{"external_tool.requested"} v949) (s/valid? #{"external_tool.completed"} v949) (s/valid? #{"command.queued"} v949) (s/valid? #{"command.execute"} v949) (s/valid? #{"command.completed"} v949) (s/valid? #{"auto_mode_switch.requested"} v949) (s/valid? #{"auto_mode_switch.completed"} v949) (s/valid? #{"commands.changed"} v949) (s/valid? #{"capabilities.changed"} v949) (s/valid? #{"exit_plan_mode.requested"} v949) (s/valid? #{"exit_plan_mode.completed"} v949) (s/valid? #{"session.tools_updated"} v949) (s/valid? #{"session.background_tasks_changed"} v949) (s/valid? #{"session.skills_loaded"} v949) (s/valid? #{"session.custom_agents_updated"} v949) (s/valid? #{"session.mcp_servers_loaded"} v949) (s/valid? #{"session.mcp_server_status_changed"} v949) (s/valid? #{"session.extensions_loaded"} v949) (s/valid? #{"session.canvas.opened"} v949) (s/valid? #{"session.canvas.registry_changed"} v949) (s/valid? #{"session.canvas.closed"} v949) (s/valid? #{"session.extensions.attachments_pushed"} v949) (s/valid? #{"mcp_app.tool_call_complete"} v949))))) +(s/def :github.copilot-sdk.generated.event-specs/type (s/spec (fn [v949] (or (s/valid? #{"session.start"} v949) (s/valid? #{"session.resume"} v949) (s/valid? #{"session.remote_steerable_changed"} v949) (s/valid? #{"session.error"} v949) (s/valid? #{"session.idle"} v949) (s/valid? #{"session.title_changed"} v949) (s/valid? #{"session.schedule_created"} v949) (s/valid? #{"session.schedule_cancelled"} v949) (s/valid? #{"session.autopilot_objective_changed"} v949) (s/valid? #{"session.info"} v949) (s/valid? #{"session.warning"} v949) (s/valid? #{"session.model_change"} v949) (s/valid? #{"session.mode_changed"} v949) (s/valid? #{"session.permissions_changed"} v949) (s/valid? #{"session.plan_changed"} v949) (s/valid? #{"session.todos_changed"} v949) (s/valid? #{"session.workspace_file_changed"} v949) (s/valid? #{"session.handoff"} v949) (s/valid? #{"session.truncation"} v949) (s/valid? #{"session.snapshot_rewind"} v949) (s/valid? #{"session.shutdown"} v949) (s/valid? #{"session.context_changed"} v949) (s/valid? #{"session.usage_info"} v949) (s/valid? #{"session.compaction_start"} v949) (s/valid? #{"session.compaction_complete"} v949) (s/valid? #{"session.task_complete"} v949) (s/valid? #{"user.message"} v949) (s/valid? #{"pending_messages.modified"} v949) (s/valid? #{"assistant.turn_start"} v949) (s/valid? #{"assistant.intent"} v949) (s/valid? #{"assistant.reasoning"} v949) (s/valid? #{"assistant.reasoning_delta"} v949) (s/valid? #{"assistant.streaming_delta"} v949) (s/valid? #{"assistant.message"} v949) (s/valid? #{"assistant.message_start"} v949) (s/valid? #{"assistant.message_delta"} v949) (s/valid? #{"assistant.turn_end"} v949) (s/valid? #{"assistant.usage"} v949) (s/valid? #{"model.call_failure"} v949) (s/valid? #{"abort"} v949) (s/valid? #{"tool.user_requested"} v949) (s/valid? #{"tool.execution_start"} v949) (s/valid? #{"tool.execution_partial_result"} v949) (s/valid? #{"tool.execution_progress"} v949) (s/valid? #{"tool.execution_complete"} v949) (s/valid? #{"skill.invoked"} v949) (s/valid? #{"subagent.started"} v949) (s/valid? #{"subagent.completed"} v949) (s/valid? #{"subagent.failed"} v949) (s/valid? #{"subagent.selected"} v949) (s/valid? #{"subagent.deselected"} v949) (s/valid? #{"hook.start"} v949) (s/valid? #{"hook.end"} v949) (s/valid? #{"hook.progress"} v949) (s/valid? #{"system.message"} v949) (s/valid? #{"system.notification"} v949) (s/valid? #{"permission.requested"} v949) (s/valid? #{"permission.completed"} v949) (s/valid? #{"user_input.requested"} v949) (s/valid? #{"user_input.completed"} v949) (s/valid? #{"elicitation.requested"} v949) (s/valid? #{"elicitation.completed"} v949) (s/valid? #{"sampling.requested"} v949) (s/valid? #{"sampling.completed"} v949) (s/valid? #{"mcp.oauth_required"} v949) (s/valid? #{"mcp.oauth_completed"} v949) (s/valid? #{"session.custom_notification"} v949) (s/valid? #{"external_tool.requested"} v949) (s/valid? #{"external_tool.completed"} v949) (s/valid? #{"command.queued"} v949) (s/valid? #{"command.execute"} v949) (s/valid? #{"command.completed"} v949) (s/valid? #{"auto_mode_switch.requested"} v949) (s/valid? #{"auto_mode_switch.completed"} v949) (s/valid? #{"commands.changed"} v949) (s/valid? #{"capabilities.changed"} v949) (s/valid? #{"exit_plan_mode.requested"} v949) (s/valid? #{"exit_plan_mode.completed"} v949) (s/valid? #{"session.tools_updated"} v949) (s/valid? #{"session.background_tasks_changed"} v949) (s/valid? #{"session.skills_loaded"} v949) (s/valid? #{"session.custom_agents_updated"} v949) (s/valid? #{"session.mcp_servers_loaded"} v949) (s/valid? #{"session.mcp_server_status_changed"} v949) (s/valid? #{"session.extensions_loaded"} v949) (s/valid? #{"session.canvas.opened"} v949) (s/valid? #{"session.canvas.registry_changed"} v949) (s/valid? #{"session.canvas.closed"} v949) (s/valid? #{"session.extensions.attachments_pushed"} v949) (s/valid? #{"mcp_app.tool_call_complete"} v949))))) (s/def :github.copilot-sdk.generated.event-specs/tz clojure.core/string?) @@ -475,7 +477,7 @@ (s/def :github.copilot-sdk.generated.event-specs/assistant.intent-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/intent])) -(s/def :github.copilot-sdk.generated.event-specs/assistant.message-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/content :github.copilot-sdk.generated.event-specs/message-id] :opt-un [:github.copilot-sdk.generated.event-specs/anthropic-advisor-blocks :github.copilot-sdk.generated.event-specs/anthropic-advisor-model :github.copilot-sdk.generated.event-specs/api-call-id :github.copilot-sdk.generated.event-specs/encrypted-content :github.copilot-sdk.generated.event-specs/interaction-id :github.copilot-sdk.generated.event-specs/model :github.copilot-sdk.generated.event-specs/output-tokens :github.copilot-sdk.generated.event-specs/parent-tool-call-id :github.copilot-sdk.generated.event-specs/phase :github.copilot-sdk.generated.event-specs/reasoning-opaque :github.copilot-sdk.generated.event-specs/reasoning-text :github.copilot-sdk.generated.event-specs/request-id :github.copilot-sdk.generated.event-specs/service-request-id :github.copilot-sdk.generated.event-specs/tool-requests :github.copilot-sdk.generated.event-specs/turn-id])) +(s/def :github.copilot-sdk.generated.event-specs/assistant.message-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/content :github.copilot-sdk.generated.event-specs/message-id] :opt-un [:github.copilot-sdk.generated.event-specs/api-call-id :github.copilot-sdk.generated.event-specs/encrypted-content :github.copilot-sdk.generated.event-specs/interaction-id :github.copilot-sdk.generated.event-specs/model :github.copilot-sdk.generated.event-specs/output-tokens :github.copilot-sdk.generated.event-specs/parent-tool-call-id :github.copilot-sdk.generated.event-specs/phase :github.copilot-sdk.generated.event-specs/reasoning-opaque :github.copilot-sdk.generated.event-specs/reasoning-text :github.copilot-sdk.generated.event-specs/request-id :github.copilot-sdk.generated.event-specs/server-tools :github.copilot-sdk.generated.event-specs/service-request-id :github.copilot-sdk.generated.event-specs/tool-requests :github.copilot-sdk.generated.event-specs/turn-id])) (s/def :github.copilot-sdk.generated.event-specs/assistant.message_delta-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/delta-content :github.copilot-sdk.generated.event-specs/message-id] :opt-un [:github.copilot-sdk.generated.event-specs/parent-tool-call-id])) @@ -491,7 +493,7 @@ (s/def :github.copilot-sdk.generated.event-specs/assistant.turn_start-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/turn-id] :opt-un [:github.copilot-sdk.generated.event-specs/interaction-id])) -(s/def :github.copilot-sdk.generated.event-specs/assistant.usage-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/model] :opt-un [:github.copilot-sdk.generated.event-specs/api-call-id :github.copilot-sdk.generated.event-specs/api-endpoint :github.copilot-sdk.generated.event-specs/cache-read-tokens :github.copilot-sdk.generated.event-specs/cache-write-tokens :github.copilot-sdk.generated.event-specs/copilot-usage :github.copilot-sdk.generated.event-specs/cost :github.copilot-sdk.generated.event-specs/duration :github.copilot-sdk.generated.event-specs/initiator :github.copilot-sdk.generated.event-specs/input-tokens :github.copilot-sdk.generated.event-specs/inter-token-latency-ms :github.copilot-sdk.generated.event-specs/output-tokens :github.copilot-sdk.generated.event-specs/parent-tool-call-id :github.copilot-sdk.generated.event-specs/provider-call-id :github.copilot-sdk.generated.event-specs/quota-snapshots :github.copilot-sdk.generated.event-specs/reasoning-effort :github.copilot-sdk.generated.event-specs/reasoning-tokens :github.copilot-sdk.generated.event-specs/service-request-id :github.copilot-sdk.generated.event-specs/time-to-first-token-ms])) +(s/def :github.copilot-sdk.generated.event-specs/assistant.usage-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/model] :opt-un [:github.copilot-sdk.generated.event-specs/api-call-id :github.copilot-sdk.generated.event-specs/api-endpoint :github.copilot-sdk.generated.event-specs/cache-read-tokens :github.copilot-sdk.generated.event-specs/cache-write-tokens :github.copilot-sdk.generated.event-specs/content-filter-triggered :github.copilot-sdk.generated.event-specs/copilot-usage :github.copilot-sdk.generated.event-specs/cost :github.copilot-sdk.generated.event-specs/duration :github.copilot-sdk.generated.event-specs/finish-reason :github.copilot-sdk.generated.event-specs/initiator :github.copilot-sdk.generated.event-specs/input-tokens :github.copilot-sdk.generated.event-specs/inter-token-latency-ms :github.copilot-sdk.generated.event-specs/output-tokens :github.copilot-sdk.generated.event-specs/parent-tool-call-id :github.copilot-sdk.generated.event-specs/provider-call-id :github.copilot-sdk.generated.event-specs/quota-snapshots :github.copilot-sdk.generated.event-specs/reasoning-effort :github.copilot-sdk.generated.event-specs/reasoning-tokens :github.copilot-sdk.generated.event-specs/service-request-id :github.copilot-sdk.generated.event-specs/time-to-first-token-ms])) (s/def :github.copilot-sdk.generated.event-specs/auto_mode_switch.completed-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/request-id :github.copilot-sdk.generated.event-specs/response])) @@ -607,6 +609,8 @@ (s/def :github.copilot-sdk.generated.event-specs/session.title_changed-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/title])) +(s/def :github.copilot-sdk.generated.event-specs/session.todos_changed-data (s/keys)) + (s/def :github.copilot-sdk.generated.event-specs/session.tools_updated-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/model])) (s/def :github.copilot-sdk.generated.event-specs/session.truncation-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/messages-removed-during-truncation :github.copilot-sdk.generated.event-specs/performed-by :github.copilot-sdk.generated.event-specs/post-truncation-messages-length :github.copilot-sdk.generated.event-specs/post-truncation-tokens-in-messages :github.copilot-sdk.generated.event-specs/pre-truncation-messages-length :github.copilot-sdk.generated.event-specs/pre-truncation-tokens-in-messages :github.copilot-sdk.generated.event-specs/token-limit :github.copilot-sdk.generated.event-specs/tokens-removed-during-truncation])) @@ -639,7 +643,7 @@ (s/def :github.copilot-sdk.generated.event-specs/tool.execution_progress-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/progress-message :github.copilot-sdk.generated.event-specs/tool-call-id])) -(s/def :github.copilot-sdk.generated.event-specs/tool.execution_start-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/tool-call-id :github.copilot-sdk.generated.event-specs/tool-name] :opt-un [:github.copilot-sdk.generated.event-specs/arguments :github.copilot-sdk.generated.event-specs/display-verbatim :github.copilot-sdk.generated.event-specs/mcp-server-name :github.copilot-sdk.generated.event-specs/mcp-tool-name :github.copilot-sdk.generated.event-specs/model :github.copilot-sdk.generated.event-specs/parent-tool-call-id :github.copilot-sdk.generated.event-specs/turn-id])) +(s/def :github.copilot-sdk.generated.event-specs/tool.execution_start-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/tool-call-id :github.copilot-sdk.generated.event-specs/tool-name] :opt-un [:github.copilot-sdk.generated.event-specs/arguments :github.copilot-sdk.generated.event-specs/display-verbatim :github.copilot-sdk.generated.event-specs/mcp-server-name :github.copilot-sdk.generated.event-specs/mcp-tool-name :github.copilot-sdk.generated.event-specs/model :github.copilot-sdk.generated.event-specs/parent-tool-call-id :github.copilot-sdk.generated.event-specs/tool-description :github.copilot-sdk.generated.event-specs/turn-id])) (s/def :github.copilot-sdk.generated.event-specs/tool.user_requested-data (s/keys :req-un [:github.copilot-sdk.generated.event-specs/tool-call-id :github.copilot-sdk.generated.event-specs/tool-name] :opt-un [:github.copilot-sdk.generated.event-specs/arguments])) @@ -785,6 +789,8 @@ (s/def :github.copilot-sdk.generated.event-specs/session.title_changed (s/and (s/keys :req-un [:github.copilot-sdk.generated.event-specs/data :github.copilot-sdk.generated.event-specs/ephemeral :github.copilot-sdk.generated.event-specs/id :github.copilot-sdk.generated.event-specs/parent-id :github.copilot-sdk.generated.event-specs/timestamp :github.copilot-sdk.generated.event-specs/type] :opt-un [:github.copilot-sdk.generated.event-specs/agent-id]) (fn [event] (clojure.core/= true (:ephemeral event))) (fn [event] (clojure.core/= "session.title_changed" (:type event))) (fn [event] (s/valid? clojure.core/string? (:id event))) (fn [event] (s/valid? :github.copilot-sdk.generated.event-specs/session.title_changed-data (:data event))))) +(s/def :github.copilot-sdk.generated.event-specs/session.todos_changed (s/and (s/keys :req-un [:github.copilot-sdk.generated.event-specs/data :github.copilot-sdk.generated.event-specs/ephemeral :github.copilot-sdk.generated.event-specs/id :github.copilot-sdk.generated.event-specs/parent-id :github.copilot-sdk.generated.event-specs/timestamp :github.copilot-sdk.generated.event-specs/type] :opt-un [:github.copilot-sdk.generated.event-specs/agent-id]) (fn [event] (clojure.core/= true (:ephemeral event))) (fn [event] (clojure.core/= "session.todos_changed" (:type event))) (fn [event] (s/valid? clojure.core/string? (:id event))) (fn [event] (s/valid? :github.copilot-sdk.generated.event-specs/session.todos_changed-data (:data event))))) + (s/def :github.copilot-sdk.generated.event-specs/session.tools_updated (s/and (s/keys :req-un [:github.copilot-sdk.generated.event-specs/data :github.copilot-sdk.generated.event-specs/ephemeral :github.copilot-sdk.generated.event-specs/id :github.copilot-sdk.generated.event-specs/parent-id :github.copilot-sdk.generated.event-specs/timestamp :github.copilot-sdk.generated.event-specs/type] :opt-un [:github.copilot-sdk.generated.event-specs/agent-id]) (fn [event] (clojure.core/= true (:ephemeral event))) (fn [event] (clojure.core/= "session.tools_updated" (:type event))) (fn [event] (s/valid? clojure.core/string? (:id event))) (fn [event] (s/valid? :github.copilot-sdk.generated.event-specs/session.tools_updated-data (:data event))))) (s/def :github.copilot-sdk.generated.event-specs/session.truncation (s/and (s/keys :req-un [:github.copilot-sdk.generated.event-specs/data :github.copilot-sdk.generated.event-specs/id :github.copilot-sdk.generated.event-specs/parent-id :github.copilot-sdk.generated.event-specs/timestamp :github.copilot-sdk.generated.event-specs/type] :opt-un [:github.copilot-sdk.generated.event-specs/agent-id :github.copilot-sdk.generated.event-specs/ephemeral]) (fn [event] (clojure.core/= "session.truncation" (:type event))) (fn [event] (s/valid? clojure.core/string? (:id event))) (fn [event] (s/valid? :github.copilot-sdk.generated.event-specs/session.truncation-data (:data event))))) @@ -827,7 +833,7 @@ (s/def :github.copilot-sdk.generated.event-specs/user_input.requested (s/and (s/keys :req-un [:github.copilot-sdk.generated.event-specs/data :github.copilot-sdk.generated.event-specs/ephemeral :github.copilot-sdk.generated.event-specs/id :github.copilot-sdk.generated.event-specs/parent-id :github.copilot-sdk.generated.event-specs/timestamp :github.copilot-sdk.generated.event-specs/type] :opt-un [:github.copilot-sdk.generated.event-specs/agent-id]) (fn [event] (clojure.core/= true (:ephemeral event))) (fn [event] (clojure.core/= "user_input.requested" (:type event))) (fn [event] (s/valid? clojure.core/string? (:id event))) (fn [event] (s/valid? :github.copilot-sdk.generated.event-specs/user_input.requested-data (:data event))))) -(def event-types "Set of all event-type strings known to the schema." #{"abort" "assistant.intent" "assistant.message" "assistant.message_delta" "assistant.message_start" "assistant.reasoning" "assistant.reasoning_delta" "assistant.streaming_delta" "assistant.turn_end" "assistant.turn_start" "assistant.usage" "auto_mode_switch.completed" "auto_mode_switch.requested" "capabilities.changed" "command.completed" "command.execute" "command.queued" "commands.changed" "elicitation.completed" "elicitation.requested" "exit_plan_mode.completed" "exit_plan_mode.requested" "external_tool.completed" "external_tool.requested" "hook.end" "hook.progress" "hook.start" "mcp.oauth_completed" "mcp.oauth_required" "mcp_app.tool_call_complete" "model.call_failure" "pending_messages.modified" "permission.completed" "permission.requested" "sampling.completed" "sampling.requested" "session.autopilot_objective_changed" "session.background_tasks_changed" "session.canvas.closed" "session.canvas.opened" "session.canvas.registry_changed" "session.compaction_complete" "session.compaction_start" "session.context_changed" "session.custom_agents_updated" "session.custom_notification" "session.error" "session.extensions.attachments_pushed" "session.extensions_loaded" "session.handoff" "session.idle" "session.info" "session.mcp_server_status_changed" "session.mcp_servers_loaded" "session.mode_changed" "session.model_change" "session.permissions_changed" "session.plan_changed" "session.remote_steerable_changed" "session.resume" "session.schedule_cancelled" "session.schedule_created" "session.shutdown" "session.skills_loaded" "session.snapshot_rewind" "session.start" "session.task_complete" "session.title_changed" "session.tools_updated" "session.truncation" "session.usage_info" "session.warning" "session.workspace_file_changed" "skill.invoked" "subagent.completed" "subagent.deselected" "subagent.failed" "subagent.selected" "subagent.started" "system.message" "system.notification" "tool.execution_complete" "tool.execution_partial_result" "tool.execution_progress" "tool.execution_start" "tool.user_requested" "user.message" "user_input.completed" "user_input.requested"}) +(def event-types "Set of all event-type strings known to the schema." #{"abort" "assistant.intent" "assistant.message" "assistant.message_delta" "assistant.message_start" "assistant.reasoning" "assistant.reasoning_delta" "assistant.streaming_delta" "assistant.turn_end" "assistant.turn_start" "assistant.usage" "auto_mode_switch.completed" "auto_mode_switch.requested" "capabilities.changed" "command.completed" "command.execute" "command.queued" "commands.changed" "elicitation.completed" "elicitation.requested" "exit_plan_mode.completed" "exit_plan_mode.requested" "external_tool.completed" "external_tool.requested" "hook.end" "hook.progress" "hook.start" "mcp.oauth_completed" "mcp.oauth_required" "mcp_app.tool_call_complete" "model.call_failure" "pending_messages.modified" "permission.completed" "permission.requested" "sampling.completed" "sampling.requested" "session.autopilot_objective_changed" "session.background_tasks_changed" "session.canvas.closed" "session.canvas.opened" "session.canvas.registry_changed" "session.compaction_complete" "session.compaction_start" "session.context_changed" "session.custom_agents_updated" "session.custom_notification" "session.error" "session.extensions.attachments_pushed" "session.extensions_loaded" "session.handoff" "session.idle" "session.info" "session.mcp_server_status_changed" "session.mcp_servers_loaded" "session.mode_changed" "session.model_change" "session.permissions_changed" "session.plan_changed" "session.remote_steerable_changed" "session.resume" "session.schedule_cancelled" "session.schedule_created" "session.shutdown" "session.skills_loaded" "session.snapshot_rewind" "session.start" "session.task_complete" "session.title_changed" "session.todos_changed" "session.tools_updated" "session.truncation" "session.usage_info" "session.warning" "session.workspace_file_changed" "skill.invoked" "subagent.completed" "subagent.deselected" "subagent.failed" "subagent.selected" "subagent.started" "system.message" "system.notification" "tool.execution_complete" "tool.execution_partial_result" "tool.execution_progress" "tool.execution_start" "tool.user_requested" "user.message" "user_input.completed" "user_input.requested"}) (defmulti event-mm :type) @@ -967,6 +973,8 @@ (defmethod event-mm "session.title_changed" [_] (s/get-spec :github.copilot-sdk.generated.event-specs/session.title_changed)) +(defmethod event-mm "session.todos_changed" [_] (s/get-spec :github.copilot-sdk.generated.event-specs/session.todos_changed)) + (defmethod event-mm "session.tools_updated" [_] (s/get-spec :github.copilot-sdk.generated.event-specs/session.tools_updated)) (defmethod event-mm "session.truncation" [_] (s/get-spec :github.copilot-sdk.generated.event-specs/session.truncation)) From 83e20bdf4cdc1cc2a678bc5089effb19a00fae4f Mon Sep 17 00:00:00 2001 From: Karl Krukow Date: Wed, 17 Jun 2026 10:02:49 +0200 Subject: [PATCH 2/6] feat(sync): port post-v1.0.1 config, lifecycle, and event additions Adds the public-SDK surface introduced upstream after v1.0.1: - :memory session config ({:enabled boolean}) forwarded on both create-session and resume-session, omitted when unset. https://github.com/github/copilot-sdk/pull/1617 - :otlp-protocol telemetry option mapped to OTEL_EXPORTER_OTLP_PROTOCOL. https://github.com/github/copilot-sdk/pull/1648 - Graceful runtime.shutdown in stop! for SDK-owned processes, bounded by a 10s timeout; force-stop! left unchanged. https://github.com/github/copilot-sdk/pull/1667 - :mcp-defer-tools (#{:auto :never}) on MCP stdio/http server configs, rendered as deferTools on the wire (1.0.63 schema). - :token-prices on ::model-billing. https://github.com/github/copilot-sdk/pull/1633 - Optional event-data fields surfaced by the 1.0.63 schema (assistant.usage content-filter/finish-reason, tool.execution_complete structured-content) and the :copilot/session.todos_changed public event. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/github/copilot_sdk.clj | 2 + src/github/copilot_sdk/client.clj | 60 +++++++++++++++++++++++++----- src/github/copilot_sdk/process.clj | 16 ++++++++ src/github/copilot_sdk/specs.clj | 59 ++++++++++++++++++++++++----- src/github/copilot_sdk/util.clj | 11 ++++-- 5 files changed, 126 insertions(+), 22 deletions(-) diff --git a/src/github/copilot_sdk.clj b/src/github/copilot_sdk.clj index 11858fb9..022b4ec7 100644 --- a/src/github/copilot_sdk.clj +++ b/src/github/copilot_sdk.clj @@ -62,6 +62,7 @@ :copilot/session.warning :copilot/session.mode_changed :copilot/session.plan_changed + :copilot/session.todos_changed :copilot/session.workspace_file_changed :copilot/session.task_complete :copilot/user.message @@ -162,6 +163,7 @@ :copilot/session.warning :copilot/session.mode_changed :copilot/session.plan_changed + :copilot/session.todos_changed :copilot/session.workspace_file_changed :copilot/session.task_complete :copilot/session.schedule_created diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj index d2b1fbd1..33298cb7 100644 --- a/src/github/copilot_sdk/client.clj +++ b/src/github/copilot_sdk/client.clj @@ -200,7 +200,7 @@ - :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided) - :is-child-process? - When true, SDK is a child of an existing Copilot CLI process and uses stdio to communicate with it (no process spawning) - :on-list-models - Zero-arg fn returning a seq of model info maps; bypasses the RPC call and does not require start! - - :telemetry - OpenTelemetry config map with optional keys :otlp-endpoint, :file-path, :exporter-type, :source-name, :capture-content? + - :telemetry - OpenTelemetry config map with optional keys :otlp-endpoint, :otlp-protocol, :file-path, :exporter-type, :source-name, :capture-content?. :otlp-protocol is \"http/json\" or \"http/protobuf\" and sets OTEL_EXPORTER_OTLP_PROTOCOL. - :on-get-trace-context - Zero-arg fn returning {:traceparent ... :tracestate ...} for distributed trace propagation - :session-idle-timeout-seconds - Server-wide session idle timeout in seconds. When > 0, the SDK appends `--session-idle-timeout ` to the spawned CLI so idle @@ -1104,6 +1104,14 @@ "Stop the CLI server and close all sessions. Returns a vector of any errors encountered during cleanup. + Performs graceful cleanup in order: disconnect sessions, request + `runtime.shutdown` for SDK-owned CLI processes (bounded by a 10s timeout), + close the connection and socket, then terminate the process. When the + `runtime.shutdown` request succeeds, the child is given up to 10s to exit on + its own before being force-killed. External runtimes (`:external-server?`) + are never asked to shut down — only the connection to them is closed. Use + force-stop! to skip graceful shutdown. + Thread safety: do not call start! and stop! concurrently from different threads. Auto-restart is suppressed via the :stopping? flag, but explicit concurrent calls are unsupported." @@ -1111,6 +1119,7 @@ (log/info "Stopping Copilot client...") (swap! (:state client) assoc :stopping? true) (let [errors (atom []) + runtime-shutdown-completed? (atom false) {:keys [sessions session-io process connection-io socket]} @(:state client)] (try ;; 0. Stop notification routing @@ -1132,6 +1141,17 @@ {:session-id session-id} e))))) (swap! (:state client) assoc :sessions {} :session-io {}) + ;; 1b. Ask SDK-owned runtimes to flush and clean up before tearing down + ;; their transport/process. External runtimes may be shared, so we only + ;; close our connection to them (see step 2). Bounded by a 10s timeout. + (when (and connection-io process (not (:external-server? client))) + (try + (proto/send-request! connection-io "runtime.shutdown" {} 10000) + (reset! runtime-shutdown-completed? true) + (catch Exception e + (swap! errors conj + (ex-info "Failed to gracefully shut down runtime" {} e))))) + ;; 2. Close connection (non-blocking, may leave read thread blocked for stdio) (when connection-io (try @@ -1150,10 +1170,14 @@ (ex-info "Failed to close socket" {} e)))) (swap! (:state client) assoc :socket nil)) - ;; 4. Kill CLI process (this also unblocks any stdio read thread) + ;; 4. Terminate the CLI process (this also unblocks any stdio read thread). + ;; When runtime.shutdown succeeded, give the child up to 10s to exit on its + ;; own first; only force-kill (SIGTERM->wait->SIGKILL) if it overstays. (when (and (not (:external-server? client)) process) (try - (proc/destroy! process) + (when-not (and @runtime-shutdown-completed? + (proc/wait-for-exit! process 10000)) + (proc/destroy! process)) (catch Exception e (swap! errors conj (ex-info "Failed to kill CLI process" {} e)))) @@ -1357,7 +1381,11 @@ :max-prompt-images :max-prompt-image-size}}} :model-policy {:policy-state :terms} - :model-billing {:multiplier} + :model-billing {:multiplier + :token-prices {:input-price :output-price :cache-price + :batch-size :context-max + :long-context {:input-price :output-price + :cache-price :context-max}}} :supported-reasoning-efforts :default-reasoning-effort :supports-reasoning-effort (legacy flat key) :vision-limits {:supported-media-types :max-prompt-images :max-prompt-image-size} (legacy)" @@ -1686,10 +1714,11 @@ (caller's values always win). Mirrors upstream `configDefaultsForMode` in `client.ts` (upstream PR #1428). - In `:empty` mode this returns the 9 safe defaults that enable + In `:empty` mode this returns the 10 safe defaults that enable multitenancy hardening (no telemetry, in-memory token + embedding - storage, no host filesystem / git / skills). In `:copilot-cli` - mode this returns `{}` so the historical behavior is preserved." + storage, no host filesystem / git / skills, no persistent memory). In + `:copilot-cli` mode this returns `{}` so the historical behavior is + preserved." [mode] (if (= mode :empty) {:enable-session-telemetry? false @@ -1700,7 +1729,8 @@ :enable-file-hooks false :enable-host-git-operations false :enable-session-store false - :enable-skills false} + :enable-skills false + :memory {:enabled false}} {})) (defn- normalize-config-for-mode @@ -1827,7 +1857,9 @@ (assoc :description (:description c)))) cmds)) config-dir (or (:config-directory config) (:config-dir config)) - wire-large-output (some-> (:large-output config) large-output->wire)] + wire-large-output (some-> (:large-output config) large-output->wire) + wire-memory (when-let [m (:memory config)] + (util/clj->wire m))] (cond-> {} (:session-id config) (assoc :session-id (:session-id config)) (:client-name config) (assoc :client-name (:client-name config)) @@ -1858,6 +1890,7 @@ wire-large-output (assoc :large-output wire-large-output) (:working-directory config) (assoc :working-directory (:working-directory config)) wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions) + wire-memory (assoc :memory wire-memory) (:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config)) (:reasoning-summary config) (assoc :reasoning-summary (:reasoning-summary config)) (contains? config :context-tier) @@ -1977,7 +2010,9 @@ (assoc :description (:description c)))) cmds)) config-dir (or (:config-directory config) (:config-dir config)) - wire-large-output (some-> (:large-output config) large-output->wire)] + wire-large-output (some-> (:large-output config) large-output->wire) + wire-memory (when-let [m (:memory config)] + (util/clj->wire m))] (cond-> {:session-id session-id} (:client-name config) (assoc :client-name (:client-name config)) (:model config) (assoc :model (:model config)) @@ -2005,6 +2040,7 @@ (:plugin-directories config) (assoc :plugin-directories (:plugin-directories config)) wire-large-output (assoc :large-output wire-large-output) wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions) + wire-memory (assoc :memory wire-memory) (:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config)) (:reasoning-summary config) (assoc :reasoning-summary (:reasoning-summary config)) (contains? config :context-tier) @@ -2175,6 +2211,8 @@ {:enabled (default true) :background-compaction-threshold (0.0-1.0, default 0.80) :buffer-exhaustion-threshold (0.0-1.0, default 0.95)} + - :memory - Persistent memory config {:enabled boolean} (upstream PR #1617). + Sent on both create and resume; omitted when unset. - :reasoning-effort - Reasoning effort level: \"low\", \"medium\", \"high\", or \"xhigh\" (PR #302) - :github-token - GitHub token for this session (sent as gitHubToken) - :on-user-input-request - Handler for ask_user requests (PR #269) @@ -2335,6 +2373,8 @@ - :skill-directories - Directories to load skills from - :disabled-skills - Skills to disable - :infinite-sessions - Infinite session configuration + - :memory - Persistent memory config {:enabled boolean} (upstream PR #1617). + Parity with create-session; omitted when unset. - :reasoning-effort - Reasoning effort level: \"low\", \"medium\", \"high\", or \"xhigh\" - :github-token - GitHub token for this session (sent as gitHubToken) - :on-user-input-request - Handler for ask_user requests diff --git a/src/github/copilot_sdk/process.clj b/src/github/copilot_sdk/process.clj index e91be5bd..7c646cfa 100644 --- a/src/github/copilot_sdk/process.clj +++ b/src/github/copilot_sdk/process.clj @@ -76,6 +76,8 @@ (assoc "COPILOT_OTEL_EXPORTER_TYPE" (:exporter-type telemetry)) (:source-name telemetry) (assoc "COPILOT_OTEL_SOURCE_NAME" (:source-name telemetry)) + (:otlp-protocol telemetry) + (assoc "OTEL_EXPORTER_OTLP_PROTOCOL" (:otlp-protocol telemetry)) (some? (:capture-content? telemetry)) (assoc "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" (str (:capture-content? telemetry))))))}) @@ -191,6 +193,20 @@ (when-let [^Process p (:process mp)] (.isAlive p))) +(defn wait-for-exit! + "Block until the process exits or timeout-ms elapses. + Returns true if the process exited, false on timeout. + + Uses `Process.waitFor`, which is safe to call concurrently with the + exit-chan monitor thread (and any other waiters) on the same process. + When there is no underlying process, returns true (nothing to wait for)." + [^ManagedProcess mp timeout-ms] + (if-let [^Process p (:process mp)] + (try + (.waitFor p timeout-ms java.util.concurrent.TimeUnit/MILLISECONDS) + (catch Exception _ false)) + true)) + (defn wait-for-port "Wait for TCP server to announce its port on stdout. Returns the port number or throws on timeout." diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj index cb5c1d96..438671be 100644 --- a/src/github/copilot_sdk/specs.clj +++ b/src/github/copilot_sdk/specs.clj @@ -74,9 +74,13 @@ (s/def ::exporter-type string?) (s/def ::source-name string?) (s/def ::capture-content? boolean?) +;; OTLP HTTP protocol for all signals; sets OTEL_EXPORTER_OTLP_PROTOCOL +;; (upstream commit b5ce1c89). +(s/def ::otlp-protocol #{"http/json" "http/protobuf"}) (s/def ::telemetry - (s/keys :opt-un [::otlp-endpoint ::file-path ::exporter-type ::source-name ::capture-content?])) + (s/keys :opt-un [::otlp-endpoint ::file-path ::exporter-type ::source-name + ::capture-content? ::otlp-protocol])) ;; Trace context provider: 0-arity fn returning {:traceparent ... :tracestate ...} (s/def ::on-get-trace-context fn?) @@ -332,14 +336,15 @@ (s/def ::mcp-args (s/coll-of string?)) (s/def ::mcp-url ::non-blank-string) (s/def ::mcp-headers (s/map-of string? string?)) +(s/def ::mcp-defer-tools #{:auto :never}) (s/def ::mcp-local-server (s/keys :req-un [::mcp-command ::mcp-tools] - :opt-un [::mcp-args ::mcp-server-type ::mcp-timeout ::env ::cwd])) + :opt-un [::mcp-args ::mcp-server-type ::mcp-timeout ::env ::cwd ::mcp-defer-tools])) (s/def ::mcp-remote-server (s/keys :req-un [::mcp-server-type ::mcp-url ::mcp-tools] - :opt-un [::mcp-timeout ::mcp-headers])) + :opt-un [::mcp-timeout ::mcp-headers ::mcp-defer-tools])) ;; New canonical names (upstream PR #1051) (s/def ::mcp-stdio-server ::mcp-local-server) @@ -481,6 +486,11 @@ (s/def ::infinite-sessions (s/keys :opt-un [::enabled ::background-compaction-threshold ::buffer-exhaustion-threshold])) +;; Memory configuration (upstream PR #1638). Lets the agent persist and recall +;; information across turns. `:enabled` is required by MemoryConfiguration. +(s/def ::memory + (s/keys :req-un [::enabled])) + ;; Reasoning effort support (PR #302) (s/def ::reasoning-effort #{"low" "medium" "high" "xhigh"}) @@ -664,6 +674,7 @@ :instruction-directories :plugin-directories :disabled-skills :large-output :infinite-sessions + :memory :reasoning-effort :reasoning-summary :context-tier :on-user-input-request :on-elicitation-request :hooks :on-exit-plan-mode :on-auto-mode-switch @@ -702,6 +713,7 @@ ::instruction-directories ::plugin-directories ::disabled-skills ::large-output ::infinite-sessions + ::memory ::reasoning-effort ::reasoning-summary ::context-tier ::on-user-input-request ::on-elicitation-request ::hooks ::on-exit-plan-mode ::on-auto-mode-switch @@ -735,6 +747,7 @@ :instruction-directories :plugin-directories :disabled-skills :large-output :infinite-sessions + :memory :reasoning-effort :reasoning-summary :context-tier :on-user-input-request :on-elicitation-request :hooks :working-directory :disable-resume? :agent :on-event :on-exit-plan-mode :on-auto-mode-switch @@ -771,6 +784,7 @@ ::instruction-directories ::plugin-directories ::disabled-skills ::large-output ::infinite-sessions + ::memory ::reasoning-effort ::reasoning-summary ::context-tier ::on-user-input-request ::on-elicitation-request ::hooks ::working-directory ::disable-resume? ::agent ::on-exit-plan-mode ::on-auto-mode-switch @@ -809,6 +823,7 @@ ::instruction-directories ::plugin-directories ::disabled-skills ::large-output ::infinite-sessions + ::memory ::reasoning-effort ::reasoning-summary ::context-tier ::on-user-input-request ::on-elicitation-request ::hooks ::working-directory ::disable-resume? ::agent ::on-exit-plan-mode ::on-auto-mode-switch @@ -1044,7 +1059,8 @@ :copilot/session.compaction_start :copilot/session.compaction_complete :copilot/session.shutdown :copilot/session.task_complete :copilot/session.title_changed :copilot/session.warning :copilot/session.context_changed - :copilot/session.mode_changed :copilot/session.plan_changed :copilot/session.workspace_file_changed + :copilot/session.mode_changed :copilot/session.plan_changed :copilot/session.todos_changed + :copilot/session.workspace_file_changed :copilot/user.message :copilot/pending_messages.modified :copilot/assistant.turn_start :copilot/assistant.intent :copilot/assistant.reasoning :copilot/assistant.reasoning_delta :copilot/assistant.message :copilot/assistant.message_delta @@ -1221,14 +1237,19 @@ (s/def ::handled? boolean?) (s/def ::stop-processing-queue? boolean?) -(s/def ::anthropic-advisor-blocks (s/coll-of any?)) -(s/def ::anthropic-advisor-model string?) +;; :server-tools — added upstream CLI 1.0.63, replacing the removed +;; anthropicAdvisorBlocks/anthropicAdvisorModel fields. Neutral provider-tagged +;; server-side tool-use payload (tool search, advisor) round-tripped verbatim. +;; Open map — passes through opaquely. :service-request-id is the +;; x-copilot-service-request-id header for CAPI log correlation. +(s/def ::server-tools map?) +(s/def ::service-request-id string?) (s/def ::assistant.message-data (s/keys :req-un [::message-id ::content] :opt-un [::tool-requests ::parent-tool-call-id ::encrypted-content ::interaction-id ::output-tokens ::phase ::reasoning-opaque ::reasoning-text ::request-id ::api-call-id - ::anthropic-advisor-blocks ::anthropic-advisor-model ::model])) + ::server-tools ::service-request-id ::turn-id ::model])) (s/def ::total-response-size-bytes nat-int?) (s/def ::turn-id ::non-blank-string) @@ -1276,6 +1297,12 @@ ;; doesn't so unknown future values pass through. (s/def ::api-endpoint string?) +;; :content-filter-triggered, :finish-reason — added upstream CLI 1.0.63. +;; True when the provider's content filter blocked output; finish-reason is the +;; provider's completion reason string (e.g. "stop", "length", "content_filter"). +(s/def ::content-filter-triggered boolean?) +(s/def ::finish-reason string?) + (s/def ::assistant.usage-data (s/keys :req-un [::model] :opt-un [::api-call-id ::api-endpoint ::cache-read-tokens @@ -1283,7 +1310,8 @@ ::initiator ::input-tokens ::inter-token-latency-ms ::output-tokens ::parent-tool-call-id ::provider-call-id ::quota-snapshots ::reasoning-effort ::reasoning-tokens - ::time-to-first-token-ms ::ttft-ms])) + ::time-to-first-token-ms ::ttft-ms + ::content-filter-triggered ::finish-reason])) (s/def ::mcp-server-name string?) (s/def ::mcp-tool-name string?) @@ -1664,8 +1692,21 @@ ;; Model billing (s/def ::multiplier number?) +;; :token-prices — added upstream CLI 1.0.63 (AI Credits pricing metadata). +;; All fields are AI Credits per billing batch unless noted. The :long-context +;; tier mirrors the standard tier minus :batch-size and its own :long-context. +(s/def ::input-price number?) +(s/def ::output-price number?) +(s/def ::cache-price number?) +(s/def ::batch-size number?) +(s/def ::context-max number?) +(s/def ::long-context + (s/keys :opt-un [::input-price ::output-price ::cache-price ::context-max])) +(s/def ::token-prices + (s/keys :opt-un [::input-price ::output-price ::cache-price ::batch-size + ::context-max ::long-context])) (s/def ::model-billing - (s/keys :opt-un [::multiplier])) + (s/keys :opt-un [::multiplier ::token-prices])) ;; Supported reasoning efforts (s/def ::supported-reasoning-efforts (s/coll-of string?)) diff --git a/src/github/copilot_sdk/util.clj b/src/github/copilot_sdk/util.clj index be9a747f..059c67b3 100644 --- a/src/github/copilot_sdk/util.clj +++ b/src/github/copilot_sdk/util.clj @@ -96,12 +96,14 @@ :mcp-server-type :type :mcp-timeout :timeout :mcp-url :url - :mcp-headers :headers}) + :mcp-headers :headers + :mcp-defer-tools :defer-tools}) (defn mcp-server->wire "Convert a single MCP server config from Clojure idiom to wire format. Strips the :mcp- prefix from MCP-specific keys, then converts remaining - keys to camelCase. Keyword values for :type are converted to strings. + keys to camelCase. Keyword values for :type and :defer-tools are converted + to strings. Example: {:mcp-command \"node\" :mcp-args [\"x\"] :mcp-server-type :http} becomes {\"command\" \"node\" \"args\" [\"x\"] \"type\" \"http\"}." [m] @@ -112,7 +114,10 @@ ;; Convert keyword values for :type to strings (upstream expects string) stringified (cond-> renamed (keyword? (:type renamed)) - (update :type name))] + (update :type name) + + (keyword? (:defer-tools renamed)) + (update :defer-tools name))] (clj->wire stringified))) (defn mcp-servers->wire From a9e7b36d7c3b18862a19e26999a19e6588ae96d7 Mon Sep 17 00:00:00 2001 From: Karl Krukow Date: Wed, 17 Jun 2026 10:02:59 +0200 Subject: [PATCH 3/6] test(sync): cover memory, otlp, graceful shutdown, and deferTools Adds integration coverage for the new session config and wire mappings, mock-server handling for the runtime.shutdown RPC, and process-level assertions for graceful termination ordering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/github/copilot_sdk/integration_test.clj | 213 ++++++++++++++++++- test/github/copilot_sdk/mock_server.clj | 1 + test/github/copilot_sdk/process_test.clj | 12 ++ 3 files changed, 221 insertions(+), 5 deletions(-) diff --git a/test/github/copilot_sdk/integration_test.clj b/test/github/copilot_sdk/integration_test.clj index ace899a8..eca89ca5 100644 --- a/test/github/copilot_sdk/integration_test.clj +++ b/test/github/copilot_sdk/integration_test.clj @@ -7,6 +7,7 @@ [github.copilot-sdk :as sdk] [github.copilot-sdk.client :as client] [github.copilot-sdk.protocol :as protocol] + [github.copilot-sdk.process :as proc] [github.copilot-sdk.session :as session] [github.copilot-sdk.specs :as specs] [github.copilot-sdk.tools :as tools] @@ -2072,13 +2073,21 @@ (is (s/valid? :github.copilot-sdk.specs/permission-kind :extension-permission-access)) (is (false? (s/valid? :github.copilot-sdk.specs/permission-kind :bogus-kind)))) - (testing "::assistant.message-data accepts new advisor + model fields (CLI 1.0.45)" + (testing "::assistant.message-data accepts :server-tools + :service-request-id (CLI 1.0.63)" (is (s/valid? :github.copilot-sdk.specs/assistant.message-data {:message-id "m1" :content "answer" - :anthropic-advisor-blocks [{:type "server_tool_use"}] - :anthropic-advisor-model "claude-advisor" - :model "gpt-5"}))) + :server-tools {:advisor-model "claude-advisor"} + :service-request-id "req-1" + :model "gpt-5"})) + (is (false? (s/valid? :github.copilot-sdk.specs/assistant.message-data + {:message-id "m1" + :content "answer" + :server-tools "not-a-map"}))) + (is (false? (s/valid? :github.copilot-sdk.specs/assistant.message-data + {:message-id "m1" + :content "answer" + :service-request-id 42})))) (testing "::session.start-data accepts :detached-from-spawning-parent-session-id (CLI 1.0.44-3)" (is (s/valid? :github.copilot-sdk.specs/session.start-data @@ -5829,7 +5838,7 @@ "tool-filter-precedence must always be \"excluded\" on resume")))) (deftest test-empty-mode-spreads-config-defaults-under-caller - (testing "empty mode spreads the 9 safe defaults under caller config (PR #1428)" + (testing "empty mode spreads the 10 safe defaults under caller config (PR #1428)" (let [server (mock/create-mock-server) _ (mock/start-mock-server! server) seen (atom {}) @@ -5856,12 +5865,55 @@ (is (= false (:enableHostGitOperations p))) (is (= false (:enableSessionStore p))) (is (= false (:enableSkills p))) + (is (= {:enabled false} (:memory p)) + "empty mode disables persistent memory by default") (is (= "excluded" (:toolFilterPrecedence p)))) (finally (try (sdk/stop! client) (catch Exception _)) (Thread/sleep 50) (mock/stop-mock-server! server)))))) +(deftest test-empty-mode-memory-default-on-create-and-resume + (testing "empty mode sends memory {:enabled false} by default on create and resume, + and the caller can override it (upstream configDefaultsForMode)" + (let [server (mock/create-mock-server) + _ (mock/start-mock-server! server) + seen (atom {}) + _ (mock/set-request-hook! server + (fn [method params] + (when (#{"session.create" "session.resume"} method) + (swap! seen assoc method params)))) + client (sdk/client {:mode :empty + :copilot-home "/tmp/empty-mode-memory-test" + :auto-start? false}) + [in out] (mock/client-streams server)] + (try + (client/connect-with-streams! client in out) + ;; Default: no caller :memory -> empty-mode default flows to the wire. + (sdk/create-session client + {:on-permission-request sdk/approve-all + :available-tools ["builtin:think"]}) + (is (= {:enabled false} (:memory (get @seen "session.create"))) + "create defaults memory to {:enabled false} in empty mode") + (let [session-id (sdk/get-last-session-id client)] + (sdk/resume-session client session-id + {:on-permission-request sdk/approve-all + :available-tools ["builtin:think"]}) + (is (= {:enabled false} (:memory (get @seen "session.resume"))) + "resume defaults memory to {:enabled false} in empty mode")) + ;; Caller override wins over the mode default. + (reset! seen {}) + (sdk/create-session client + {:on-permission-request sdk/approve-all + :available-tools ["builtin:think"] + :memory {:enabled true}}) + (is (= {:enabled true} (:memory (get @seen "session.create"))) + "caller-provided :memory overrides the empty-mode default") + (finally + (try (sdk/stop! client) (catch Exception _)) + (Thread/sleep 50) + (mock/stop-mock-server! server)))))) + (deftest test-empty-mode-caller-config-wins-over-mode-defaults (testing "caller-provided config values always win over mode defaults (PR #1428)" (let [server (mock/create-mock-server) @@ -6700,3 +6752,154 @@ (is (not (s/valid? :github.copilot-sdk.specs/session.schedule_created-data {:id 1 :prompt "p" :at 1.5})) "non-integer (double) is rejected"))) + +;; --- C1: memory configuration on the wire (upstream PR memory 86df7e50) ------- + +(deftest test-memory-config-on-wire + (testing ":memory is forwarded to session.create as {:enabled bool}" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all + :memory {:enabled true}}) + create-params (get @seen "session.create")] + (is (= {:enabled true} (:memory create-params))))) + + (testing ":memory {:enabled false} is forwarded (not stripped)" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all + :memory {:enabled false}}) + create-params (get @seen "session.create")] + (is (= {:enabled false} (:memory create-params))))) + + (testing ":memory is omitted from session.create when unset (never null)" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* {:on-permission-request sdk/approve-all}) + create-params (get @seen "session.create")] + (is (not (contains? create-params :memory))))) + + (testing ":memory is forwarded to session.resume (parity with create)" + (let [seen (atom {}) + session-id (sdk/session-id (sdk/create-session *test-client* {:on-permission-request sdk/approve-all})) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (when (#{"session.resume"} method) + (swap! seen assoc method params)))) + _ (sdk/resume-session *test-client* session-id + {:on-permission-request sdk/approve-all + :memory {:enabled true}}) + resume-params (get @seen "session.resume")] + (is (= {:enabled true} (:memory resume-params)))))) + +;; --- C5: deferTools on MCP server configs (1.0.63 schema) -------------------- + +(deftest test-mcp-defer-tools-on-wire + (testing ":mcp-defer-tools keyword is forwarded as deferTools string" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all + :mcp-servers {"srv-http" {:mcp-server-type :http + :mcp-url "https://mcp.test" + :mcp-tools ["*"] + :mcp-defer-tools :auto} + "srv-stdio" {:mcp-command "node" + :mcp-args ["server.js"] + :mcp-tools ["*"] + :mcp-defer-tools :never}}}) + create-params (get @seen "session.create")] + (is (= "auto" (get-in create-params [:mcpServers :srv-http :deferTools]))) + (is (= "never" (get-in create-params [:mcpServers :srv-stdio :deferTools]))))) + + (testing "deferTools is omitted when :mcp-defer-tools unset" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all + :mcp-servers {"srv-http" {:mcp-server-type :http + :mcp-url "https://mcp.test" + :mcp-tools ["*"]}}}) + create-params (get @seen "session.create")] + (is (not (contains? (get-in create-params [:mcpServers :srv-http]) :deferTools)))))) + +;; --- C3: graceful runtime.shutdown in stop! (upstream PR #1667) -------------- + +(deftest test-graceful-runtime-shutdown + (testing "stop! sends runtime.shutdown for SDK-owned (non-external) process" + (let [seen (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method _params] + (swap! seen conj method)))] + ;; Inject a placeholder process so the SDK-owned guard fires. A bare map + ;; has no :process key, so proc/destroy! is a no-op. + (swap! (:state *test-client*) assoc :process {:placeholder true}) + (sdk/stop! *test-client*) + (is (some #{"runtime.shutdown"} @seen) + "runtime.shutdown RPC is sent during graceful stop"))) + + (testing "stop! does NOT send runtime.shutdown when no process is owned" + (let [seen (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method _params] + (swap! seen conj method)))] + ;; No :process injected — fixture connect-with-streams! spawns nothing. + (sdk/stop! *test-client*) + (is (not (some #{"runtime.shutdown"} @seen)) + "runtime.shutdown is skipped when the client does not own a process")))) + +(deftest graceful-shutdown-waits-for-natural-exit + (testing "stop! waits for the child to exit on its own after a successful + runtime.shutdown and skips the kill when it does" + (let [waited? (atom false) + killed? (atom false)] + (swap! (:state *test-client*) assoc :process {:placeholder true}) + (with-redefs [proc/wait-for-exit! (fn [_ _] (reset! waited? true) true) + proc/destroy! (fn [_] (reset! killed? true))] + (sdk/stop! *test-client*)) + (is @waited? "stop! waits for natural exit after runtime.shutdown succeeds") + (is (not @killed?) + "stop! does not SIGTERM the child when it exits gracefully"))) + + (testing "stop! force-kills the child when it does not exit within the window" + (let [killed? (atom false)] + (swap! (:state *test-client*) assoc :process {:placeholder true}) + (with-redefs [proc/wait-for-exit! (fn [_ _] false) + proc/destroy! (fn [_] (reset! killed? true))] + (sdk/stop! *test-client*)) + (is @killed? + "stop! kills the child if it does not exit within the graceful window"))) + + (testing "stop! kills immediately (no graceful wait) when runtime.shutdown fails" + (let [waited? (atom false) + killed? (atom false)] + (swap! (:state *test-client*) assoc :process {:placeholder true}) + (with-redefs [protocol/send-request! + (fn [_conn method _params & _] + (when (= method "runtime.shutdown") + (throw (ex-info "boom" {}))) + nil) + proc/wait-for-exit! (fn [_ _] (reset! waited? true) true) + proc/destroy! (fn [_] (reset! killed? true))] + (sdk/stop! *test-client*)) + (is (not @waited?) + "stop! does not wait for natural exit when runtime.shutdown failed") + (is @killed? + "stop! force-kills the child immediately when runtime.shutdown failed")))) diff --git a/test/github/copilot_sdk/mock_server.clj b/test/github/copilot_sdk/mock_server.clj index 6753a7c0..3e732313 100644 --- a/test/github/copilot_sdk/mock_server.clj +++ b/test/github/copilot_sdk/mock_server.clj @@ -394,6 +394,7 @@ "session.remote.enable" {:url "https://copilot-remote.test/abc" :remoteSteerable true} "session.remote.disable" {} "sessionFs.setProvider" {:ok true} + "runtime.shutdown" {} (throw (ex-info "Method not found" {:code -32601 :method method}))) ;; Merge hook-provided data into result only when hook returns ::merge-response ;; This prevents accidental response mutation from spy hooks (e.g. swap! return values) diff --git a/test/github/copilot_sdk/process_test.clj b/test/github/copilot_sdk/process_test.clj index 25ca7bbf..61c98552 100644 --- a/test/github/copilot_sdk/process_test.clj +++ b/test/github/copilot_sdk/process_test.clj @@ -54,6 +54,18 @@ (is (= "my-app" (get overrides "COPILOT_OTEL_SOURCE_NAME"))) (is (= "true" (get overrides "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT")))))) +(deftest cli-env-overrides-otlp-protocol + (testing ":otlp-protocol maps onto OTEL_EXPORTER_OTLP_PROTOCOL (upstream b5ce1c89)" + (doseq [proto ["http/json" "http/protobuf"]] + (let [{:keys [overrides]} (proc/cli-env-overrides + {:telemetry {:otlp-endpoint "http://localhost:4318" + :otlp-protocol proto}})] + (is (= proto (get overrides "OTEL_EXPORTER_OTLP_PROTOCOL")))))) + (testing "OTEL_EXPORTER_OTLP_PROTOCOL is omitted when :otlp-protocol unset" + (let [{:keys [overrides]} (proc/cli-env-overrides + {:telemetry {:otlp-endpoint "http://localhost:4318"}})] + (is (not (contains? overrides "OTEL_EXPORTER_OTLP_PROTOCOL")))))) + (deftest cli-env-defaults-can-be-overridden-by-user-env (testing "NODE_DEBUG is a default — user :env should be able to re-enable it" ;; This locks in the precedence contract: defaults are applied BEFORE user :env From 09c25ffc5b33b20dacbfac0b7d70d53eb8522bc8 Mon Sep 17 00:00:00 2001 From: Karl Krukow Date: Wed, 17 Jun 2026 10:03:05 +0200 Subject: [PATCH 4/6] docs(sync): document new config and bump version to 1.0.1.1 Documents :memory, :otlp-protocol, MCP :defer-tools, :token-prices, the session.todos_changed event, and graceful shutdown across API.md, the MCP overview, and CHANGELOG. Bumps the Clojure patch to 1.0.1.1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 49 ++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- build.clj | 2 +- doc/getting-started.md | 2 +- doc/mcp/overview.md | 4 ++++ doc/reference/API.md | 31 ++++++++++++++++++++------ 6 files changed, 80 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4812e64..4633a406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,47 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] ### Added (post-v1.0.1 sync) +- **`:memory` session configuration** — port of upstream + [PR #1617](https://github.com/github/copilot-sdk/pull/1617). `create-session` + and `resume-session` now accept an optional `:memory` map (shape + `{:enabled boolean}`) that configures the agent's persistent memory. It is + forwarded on **both** `session.create` and `session.resume`, omitted entirely + when the key is absent (never wire `null`), and wire-encoded as `memory`. In + `:mode :empty` it is defaulted to `{:enabled false}` (caller can override). + Added `::memory` / `::memory-enabled` specs. +- **`:otlp-protocol` telemetry option** — port of upstream + [PR #1648](https://github.com/github/copilot-sdk/pull/1648). The client + `:telemetry` map accepts an optional `:otlp-protocol` (`"http/json"` or + `"http/protobuf"`), mapped to the `OTEL_EXPORTER_OTLP_PROTOCOL` environment + variable on the spawned CLI. Added `::otlp-protocol` to the `::telemetry` spec. +- **Graceful `runtime.shutdown` in `stop!`** — port of upstream + [PR #1667](https://github.com/github/copilot-sdk/pull/1667) (restores the + behavior of the reverted PR #1539). For SDK-spawned (non-external) processes, + `stop!` now sends a `runtime.shutdown` RPC bounded by a 10-second timeout + before closing the connection, falling back to process termination + (SIGTERM → SIGKILL) on timeout or error. `force-stop!` is unchanged. +- **`:mcp-defer-tools` MCP option** — new in upstream CLI schema 1.0.63. Stdio + and HTTP/SSE MCP server configs accept an optional `:mcp-defer-tools` keyword + (`:auto` or `:never`) controlling tool-deferral. Wire-encoded as `deferTools` + with the keyword value stringified. Added `::mcp-defer-tools` spec to both MCP + server specs. +- **`:copilot/session.todos_changed` event** — new signal-only event in upstream + CLI schema 1.0.63. Carries no payload; fires when the agent's todos / todo-deps + table is written. Added to the public `event-types` and `session-events` sets. +- **New optional event-data fields** (upstream CLI schema 1.0.63): + - `:copilot/assistant.usage`: `:content-filter-triggered` (boolean), + `:finish-reason` (string). + - `:copilot/tool.execution_complete`: `:structured-content` (arbitrary + structured tool result). + - `:copilot/assistant.message`: `:server-tools` (replaces the removed + `anthropicAdvisorBlocks` / `anthropicAdvisorModel` fields). + - `:copilot/tool.execution_start`: `:tool-description`. +- **`::model-billing` token-prices spec** — port of upstream + [PR #1633](https://github.com/github/copilot-sdk/pull/1633). The + `::model-billing` spec gains an optional `:token-prices` map + (`:input-price`, `:output-price`, `:cache-price`, `:batch-size`, + `:context-max`, `:long-context`); `list-models` already passes the whole + billing map through, so this is documentation/validation only. - **`:defer` tool-definition option** — port of upstream [PR #1632](https://github.com/github/copilot-sdk/pull/1632). `define-tool` and `define-tool-from-spec` now accept an optional `:defer` keyword (`:auto` or @@ -14,6 +55,14 @@ All notable changes to this project will be documented in this file. This change the runtime applies its default (`"auto"`). Added `::defer` value spec (`#{:auto :never}`) to the `::tool` spec. +### Changed (post-v1.0.1 sync) +- Bumped pinned `@github/copilot` schema 1.0.61 → 1.0.63 + ([PR #1686](https://github.com/github/copilot-sdk/pull/1686) and intermediate + 1.0.62), regenerating `generated/event_specs.clj` and `generated/coerce.clj`. + Pulls in the new `session.todos_changed` event, optional usage / + tool-execution / assistant-message fields, the MCP `deferTools` config, and + the `ExtensionSource` enum extension (`plugin` / `session`). + ### Added (v1.0.1 sync) - **`open-canvases` snapshot** — port of upstream PR #1604. A new `github.copilot-sdk/open-canvases` (also `github.copilot-sdk.session/open-canvases`) diff --git a/README.md b/README.md index 59268447..c8c3c14f 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Add to your `deps.edn`: ```clojure ;; From Maven Central -io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "1.0.1.0"} +io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "1.0.1.1"} ;; Or git dependency io.github.copilot-community-sdk/copilot-sdk-clojure {:git/url "https://github.com/copilot-community-sdk/copilot-sdk-clojure.git" diff --git a/build.clj b/build.clj index e5297c3a..5eb03e36 100644 --- a/build.clj +++ b/build.clj @@ -7,7 +7,7 @@ (:import [java.io File])) (def lib 'io.github.copilot-community-sdk/copilot-sdk-clojure) -(def version "1.0.1.0") +(def version "1.0.1.1") (def class-dir "target/classes") (defn- try-sh diff --git a/doc/getting-started.md b/doc/getting-started.md index eb3ace2c..3eb12f78 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -34,7 +34,7 @@ copilot --version Add to your `deps.edn`: ```clojure -{:deps {io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "1.0.1.0"}}} +{:deps {io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "1.0.1.1"}}} ``` Or use as a Git dependency: diff --git a/doc/mcp/overview.md b/doc/mcp/overview.md index 8ab5f94e..73b62f64 100644 --- a/doc/mcp/overview.md +++ b/doc/mcp/overview.md @@ -111,6 +111,7 @@ Here's a complete working example using the official [`@modelcontextprotocol/ser | `:mcp-tools` | vector | Yes | Tools to enable (`["*"]` for all, `[]` for none, or specific tool names) | | `:mcp-server-type` | keyword | No | `:local` or `:stdio` (defaults to local) | | `:mcp-timeout` | number | No | Timeout in milliseconds | +| `:mcp-defer-tools` | keyword | No | Tool-deferral policy: `:auto` (defer tool registration until needed) or `:never`. Wire-encoded as `deferTools` (upstream schema 1.0.63) | | `:env` | map | No | Environment variables for the subprocess | | `:cwd` | string | No | Working directory for the subprocess | @@ -122,12 +123,15 @@ Here's a complete working example using the official [`@modelcontextprotocol/ser | `:mcp-url` | string | Yes | Server URL | | `:mcp-tools` | vector | Yes | Tools to enable (`["*"]` for all) | | `:mcp-timeout` | number | No | Timeout in milliseconds | +| `:mcp-defer-tools` | keyword | No | Tool-deferral policy: `:auto` or `:never`. Wire-encoded as `deferTools` (upstream schema 1.0.63) | | `:mcp-headers` | map | No | HTTP headers (e.g., for authentication) | ### Key Naming Convention MCP server config keys use an `:mcp-` prefix in Clojure (e.g., `:mcp-command`, `:mcp-args`, `:mcp-tools`) to distinguish them from other configuration options. On the wire, the SDK automatically strips this prefix to match the upstream protocol (e.g., `command`, `args`, `tools`). +Some prefixed keys map to a camelCase wire key rather than a bare strip: `:mcp-defer-tools` becomes `deferTools`, and its keyword value (`:auto`/`:never`) is stringified. + The non-prefixed keys `:env` and `:cwd` are shared with other config types and do not have an `:mcp-` prefix. ## Tool Filtering diff --git a/doc/reference/API.md b/doc/reference/API.md index c042cf47..2a11ce9a 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -133,11 +133,11 @@ Get information about the current shared client state. Returns `nil` if no share | `:remote?` | boolean | `false` | When `true`, append `--remote` to the spawned CLI args so the CLI exposes the session over a GitHub-hosted remote endpoint. Ignored when `:cli-url` is set. (upstream PR #1192) | | `:session-idle-timeout-seconds` | integer | `0` (disabled) | Server-wide session idle timeout in seconds. When `> 0`, append `--session-idle-timeout ` to the spawned CLI so idle sessions are cleaned up after the given duration. | | `:on-list-models` | fn | nil | Zero-arg function returning model info maps. Bypasses `models.list` RPC; does not require `start!`. Results are cached the same way as RPC results | -| `:telemetry` | map | nil | OpenTelemetry export config, applied as environment variables to the **spawned** CLI (ignored when connecting to an existing server via `:cli-url` or a parent process via `:is-child-process?`, since no CLI is spawned). When present, enables OTel. Keys (all optional): `:otlp-endpoint` (OTLP HTTP endpoint), `:file-path` (write spans to a file), `:exporter-type` (exporter selection), `:source-name` (service/source name), `:capture-content?` (boolean — capture prompt/response content; **off by default for privacy**). See [Observability](#observability). (upstream PR #785) | +| `:telemetry` | map | nil | OpenTelemetry export config, applied as environment variables to the **spawned** CLI (ignored when connecting to an existing server via `:cli-url` or a parent process via `:is-child-process?`, since no CLI is spawned). When present, enables OTel. Keys (all optional): `:otlp-endpoint` (OTLP HTTP endpoint), `:otlp-protocol` (`"http/json"` or `"http/protobuf"` — sets `OTEL_EXPORTER_OTLP_PROTOCOL`), `:file-path` (write spans to a file), `:exporter-type` (exporter selection), `:source-name` (service/source name), `:capture-content?` (boolean — capture prompt/response content; **off by default for privacy**). See [Observability](#observability). (upstream PR #785, [PR #1648](https://github.com/github/copilot-sdk/pull/1648)) | | `:on-get-trace-context` | fn | nil | Zero-arg function returning `{:traceparent "..." :tracestate "..."}`, called per request (session create/resume and each message send) to propagate a distributed-trace context. Only `:traceparent` and `:tracestate` are forwarded. See [Observability](#observability) | | `:is-child-process?` | boolean | `false` | When `true`, connect via own stdio to a parent Copilot CLI process (no process spawning). Requires `:use-stdio?` `true`; mutually exclusive with `:cli-url` | | `:session-fs` | map | nil | Session filesystem provider config. Keys: `:initial-cwd` (string, required), `:session-state-path` (string, required), `:conventions` (`"windows"` or `"posix"`, required). When set, the client calls `sessionFs.setProvider` on connect and routes filesystem operations through per-session handlers. See [Session Filesystem](#session-filesystem) | -| `:mode` | keyword | `:copilot-cli` | Client multitenancy mode: `:copilot-cli` (default — preserve historical CLI behavior) or `:empty` (multi-tenant SaaS hosts that must isolate sessions from local machine state). In `:empty` mode the SDK requires at least one tenant-scoped storage root (`:copilot-home`, `:session-fs`, `:cli-url`, or `:is-child-process?`), sets `COPILOT_DISABLE_KEYTAR=1` on the spawned CLI, spreads 9 safe defaults under caller session config, forces `installedPlugins []`, and normalizes `:system-message` to strip `environment_context`. See [Client Mode](#client-mode-empty). (upstream PR #1428) | +| `:mode` | keyword | `:copilot-cli` | Client multitenancy mode: `:copilot-cli` (default — preserve historical CLI behavior) or `:empty` (multi-tenant SaaS hosts that must isolate sessions from local machine state). In `:empty` mode the SDK requires at least one tenant-scoped storage root (`:copilot-home`, `:session-fs`, `:cli-url`, or `:is-child-process?`), sets `COPILOT_DISABLE_KEYTAR=1` on the spawned CLI, spreads 10 safe defaults under caller session config, forces `installedPlugins []`, and normalizes `:system-message` to strip `environment_context`. See [Client Mode](#client-mode-empty). (upstream PR #1428) | ### Methods @@ -167,6 +167,13 @@ Create a client, start it, and ensure `stop!` runs on exit. Stop the server and close all sessions gracefully. +For SDK-spawned processes (not `:external-server?`), `stop!` issues a +`runtime.shutdown` RPC before closing the connection, giving the CLI a chance to +flush state and exit cleanly. The call is bounded by a 10-second timeout; on +timeout or error the SDK falls back to terminating the process (SIGTERM, then +SIGKILL). Connecting to an external server (`:cli-url`) skips the shutdown RPC and +the process is left running. (upstream [PR #1667](https://github.com/github/copilot-sdk/pull/1667)) + #### `force-stop!` ```clojure @@ -291,6 +298,7 @@ Create a client and session together, ensuring both are cleaned up on exit. | `:coauthor-enabled` | boolean | Add a Copilot Co-authored-by trailer to commits made by the CLI. Forwarded via `session.options.update`. Defaulted to `false` in `:empty` mode. (upstream PR #1428) | | `:manage-schedule-enabled` | boolean | Enable the built-in schedule-management tools. Forwarded via `session.options.update`. Defaulted to `false` in `:empty` mode. (upstream PR #1428) | | `:open-canvases` | vector | (resume-session / join-session only) Seed the open-canvases snapshot when reconnecting. Each entry: `{:instance-id ... :extension-id ... :canvas-id ... :reopen bool :availability "ready"\|"stale" :extension-name? ... :title? ... :status? ... :url? ... :input? {...}}`. Caller-defined `:input` keys are preserved verbatim through wire conversion (no kebab→camel re-casing). See [`open-canvases`](#open-canvases). (upstream PR #1604) | +| `:memory` | map | Persistent-memory configuration. Shape: `{:enabled boolean}`. Sent on **both** `session.create` and `session.resume`; omitted entirely when the key is absent (never wire `null`). Wire-encoded as `memory`. In `:mode :empty` it is defaulted to `{:enabled false}` (caller can override). (upstream [PR #1617](https://github.com/github/copilot-sdk/pull/1617)) | #### `resume-session` @@ -441,7 +449,14 @@ Requires authentication (unless `:on-list-models` is provided). Returns a vector :max-prompt-image-size 20971520}}} :model-policy {:policy-state "enabled" :terms "..."} - :model-billing {:multiplier 1.0} + :model-billing {:multiplier 1.0 + ;; Per-token prices (upstream PR #1633), present when the + ;; model reports them; keys are optional: + :token-prices {:input-price 0.00000125 + :output-price 0.00001 + :cache-price 0.0000003125 + :long-context {:input-price 0.0000025 + :output-price 0.00002}}} ;; Model picker categorization (CLI 1.0.46+): :model-picker-category "powerful" ;; "lightweight" | "versatile" | "powerful" :model-picker-price-category "very_high" ;; "low" | "medium" | "high" | "very_high" @@ -1470,6 +1485,7 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor | `:copilot/session.plan_changed` | Session plan created/updated/deleted; data: `{:operation "create"/"update"/"delete"}` | | `:copilot/session.workspace_file_changed` | Workspace file created or updated; data: `{:path "...", :operation "create"/"update"}` | | `:copilot/session.task_complete` | Task completed by the session agent; data: `{:summary "..." :aborted? false}` (both optional) | +| `:copilot/session.todos_changed` | Signal-only: the agent's todos / todo-deps table was written. **No payload** — fetch current state via `session.plan.readSqlTodosWithDependencies()`. Events arrive in order; debounce on arrival if needed (upstream schema 1.0.63) | | `:copilot/session.schedule_created` | Scheduled prompt registered via `/every`; data: `{:id :interval-ms :prompt "..."}` (upstream schema 1.0.42) | | `:copilot/session.schedule_cancelled` | Scheduled prompt cancelled from the schedule manager dialog; data: `{:id }` (upstream schema 1.0.42) | | `:copilot/session.autopilot_objective_changed` | Autopilot objective lifecycle events; data: `{:operation #{"create" "update" "delete"}}` (required) with optional `:id` (integer) and `:status` (upstream schema 1.0.56). The `:status` enum is widened to include `"active"`, `"paused"`, `"cap_reached"`, `"completed"`. | @@ -1485,13 +1501,13 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor | `:copilot/assistant.message_delta` | Streaming response chunk | | `:copilot/assistant.streaming_delta` | Response size update during streaming; data: `{:total-response-size-bytes N}` | | `:copilot/assistant.turn_end` | Assistant turn completed | -| `:copilot/assistant.usage` | Token usage for this turn | +| `:copilot/assistant.usage` | Token usage for this turn; data may include optional `:content-filter-triggered` (boolean) and `:finish-reason` (string) (upstream schema 1.0.63) | | `:copilot/abort` | Current message aborted | | `:copilot/tool.user_requested` | Tool execution requested by user | | `:copilot/tool.execution_start` | Tool execution started; data includes `:tool-call-id`, `:tool-name`, optional `:arguments`, `:parent-tool-call-id`, `:mcp-server-name`, `:mcp-tool-name`, `:model` | | `:copilot/tool.execution_progress` | Tool execution progress update | | `:copilot/tool.execution_partial_result` | Tool execution partial result | -| `:copilot/tool.execution_complete` | Tool execution completed | +| `:copilot/tool.execution_complete` | Tool execution completed; data may include optional `:structured-content` (arbitrary structured tool result) (upstream schema 1.0.63) | | `:copilot/subagent.started` | Subagent started; data includes :tool-call-id, :agent-name, :agent-display-name, :agent-description | | `:copilot/subagent.completed` | Subagent completed; data includes :tool-call-id, :agent-name, :agent-display-name, optional :model, :total-tool-calls, :total-tokens, :duration-ms | | `:copilot/subagent.failed` | Subagent failed; data includes :tool-call-id, :agent-name, :agent-display-name, :error, optional :model, :total-tool-calls, :total-tokens, :duration-ms | @@ -1630,13 +1646,14 @@ spawned CLI. Presence of the map enables OTel; all sub-keys are optional: | Key | Type | Description | CLI env var | |-----|------|-------------|-------------| | `:otlp-endpoint` | string | OTLP HTTP endpoint to export spans to | `OTEL_EXPORTER_OTLP_ENDPOINT` | +| `:otlp-protocol` | string | OTLP wire protocol: `"http/json"` or `"http/protobuf"` | `OTEL_EXPORTER_OTLP_PROTOCOL` | | `:file-path` | string | Write spans to a local file instead of/alongside OTLP | `COPILOT_OTEL_FILE_EXPORTER_PATH` | | `:exporter-type` | string | Exporter selection | `COPILOT_OTEL_EXPORTER_TYPE` | | `:source-name` | string | Service / source name attached to spans | `COPILOT_OTEL_SOURCE_NAME` | | `:capture-content?` | boolean | Capture prompt/response content in spans. **Defaults to off** — only enable in trusted environments, as it records message content | `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | When `:telemetry` is present the SDK sets `COPILOT_OTEL_ENABLED=true` on the CLI process. -(upstream PR #785) +(upstream PR #785, [PR #1648](https://github.com/github/copilot-sdk/pull/1648)) #### Distributed trace propagation (`:on-get-trace-context`) @@ -1736,7 +1753,7 @@ What `:empty` mode enforces (vs `:copilot-cli`): `:skip-embedding-retrieval true`, `:embedding-cache-storage :in-memory`, `:enable-on-demand-instruction-discovery false`, `:enable-file-hooks false`, `:enable-host-git-operations false`, `:enable-session-store false`, - `:enable-skills false`. + `:enable-skills false`, `:memory {:enabled false}`. - **System message normalization**: the SDK strips the `environment_context` section from the system message (or promotes `:append` to `:customize`) so no host-environment context leaks. If the caller already provides their own From ecfb62ac98e1a833656067da405271189bf434e5 Mon Sep 17 00:00:00 2001 From: Karl Krukow Date: Thu, 18 Jun 2026 09:45:00 +0200 Subject: [PATCH 5/6] fix(sync): address Copilot review feedback Resolve four findings from the Copilot Code Review bot on PR #139: - process.clj: preserve the thread interrupt flag in wait-for-exit!. .waitFor throws InterruptedException (a subclass of Exception), so the generic catch was swallowing it and clearing the interrupt flag. Catch it separately and re-set the flag via (.interrupt (Thread/currentThread)) so callers still observe cancellation. - util.clj: correct the mcp-server->wire docstring example. clj->wire produces camelCase keyword keys, not string keys. - specs.clj: fix the memory-config comment to cite upstream PR #1617 (not #1638). - CHANGELOG.md: drop the nonexistent ::memory-enabled spec reference; only ::memory exists (reusing the existing ::enabled spec). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- src/github/copilot_sdk/process.clj | 5 +++++ src/github/copilot_sdk/specs.clj | 2 +- src/github/copilot_sdk/util.clj | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4633a406..0247196b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ All notable changes to this project will be documented in this file. This change forwarded on **both** `session.create` and `session.resume`, omitted entirely when the key is absent (never wire `null`), and wire-encoded as `memory`. In `:mode :empty` it is defaulted to `{:enabled false}` (caller can override). - Added `::memory` / `::memory-enabled` specs. + Added a `::memory` spec (reusing the existing `::enabled`). - **`:otlp-protocol` telemetry option** — port of upstream [PR #1648](https://github.com/github/copilot-sdk/pull/1648). The client `:telemetry` map accepts an optional `:otlp-protocol` (`"http/json"` or diff --git a/src/github/copilot_sdk/process.clj b/src/github/copilot_sdk/process.clj index 7c646cfa..d01ac7ae 100644 --- a/src/github/copilot_sdk/process.clj +++ b/src/github/copilot_sdk/process.clj @@ -204,6 +204,11 @@ (if-let [^Process p (:process mp)] (try (.waitFor p timeout-ms java.util.concurrent.TimeUnit/MILLISECONDS) + (catch InterruptedException _ + ;; Restore the interrupt flag that waitFor cleared so callers' + ;; shutdown hooks / cancellation logic still observe the interrupt. + (.interrupt (Thread/currentThread)) + false) (catch Exception _ false)) true)) diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj index 438671be..52d20e0b 100644 --- a/src/github/copilot_sdk/specs.clj +++ b/src/github/copilot_sdk/specs.clj @@ -486,7 +486,7 @@ (s/def ::infinite-sessions (s/keys :opt-un [::enabled ::background-compaction-threshold ::buffer-exhaustion-threshold])) -;; Memory configuration (upstream PR #1638). Lets the agent persist and recall +;; Memory configuration (upstream PR #1617). Lets the agent persist and recall ;; information across turns. `:enabled` is required by MemoryConfiguration. (s/def ::memory (s/keys :req-un [::enabled])) diff --git a/src/github/copilot_sdk/util.clj b/src/github/copilot_sdk/util.clj index 059c67b3..445e6e30 100644 --- a/src/github/copilot_sdk/util.clj +++ b/src/github/copilot_sdk/util.clj @@ -105,7 +105,8 @@ keys to camelCase. Keyword values for :type and :defer-tools are converted to strings. Example: {:mcp-command \"node\" :mcp-args [\"x\"] :mcp-server-type :http} - becomes {\"command\" \"node\" \"args\" [\"x\"] \"type\" \"http\"}." + becomes {:command \"node\" :args [\"x\"] :type \"http\"} (camelCase keyword + keys, with :type / :defer-tools values stringified)." [m] (let [renamed (reduce-kv (fn [acc k v] (assoc acc (get mcp-key-renames k k) v)) From 0a5958588e44ea957f57f6fc3fb772c60d12817c Mon Sep 17 00:00:00 2001 From: Karl Krukow Date: Thu, 18 Jun 2026 09:52:36 +0200 Subject: [PATCH 6/6] docs(sync): drop non-exposed RPC method reference from todos_changed row The session.todos_changed event reference pointed Clojure users at session.plan.readSqlTodosWithDependencies(), a TS-style method call for an @experimental upstream method this SDK does not expose. Reword the row as signal-only with no payload, consistent with how the rest of the doc names JSON-RPC methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/reference/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/reference/API.md b/doc/reference/API.md index 2a11ce9a..4f718901 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -1485,7 +1485,7 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor | `:copilot/session.plan_changed` | Session plan created/updated/deleted; data: `{:operation "create"/"update"/"delete"}` | | `:copilot/session.workspace_file_changed` | Workspace file created or updated; data: `{:path "...", :operation "create"/"update"}` | | `:copilot/session.task_complete` | Task completed by the session agent; data: `{:summary "..." :aborted? false}` (both optional) | -| `:copilot/session.todos_changed` | Signal-only: the agent's todos / todo-deps table was written. **No payload** — fetch current state via `session.plan.readSqlTodosWithDependencies()`. Events arrive in order; debounce on arrival if needed (upstream schema 1.0.63) | +| `:copilot/session.todos_changed` | Signal-only: the agent's todos / todo-deps table was written. **No payload.** Events arrive in order; debounce on arrival if needed (upstream schema 1.0.63) | | `:copilot/session.schedule_created` | Scheduled prompt registered via `/every`; data: `{:id :interval-ms :prompt "..."}` (upstream schema 1.0.42) | | `:copilot/session.schedule_cancelled` | Scheduled prompt cancelled from the schedule manager dialog; data: `{:id }` (upstream schema 1.0.42) | | `:copilot/session.autopilot_objective_changed` | Autopilot objective lifecycle events; data: `{:operation #{"create" "update" "delete"}}` (required) with optional `:id` (integer) and `:status` (upstream schema 1.0.56). The `:status` enum is widened to include `"active"`, `"paused"`, `"cap_reached"`, `"completed"`. |