From a0d81b0ff25c763bcd06da4169c29f09674be552 Mon Sep 17 00:00:00 2001 From: Touseef Liaqat Date: Wed, 26 Aug 2026 12:40:32 -0700 Subject: [PATCH 1/5] Add shared session watch APIs Expose passive shared-session watch handles for Node and Rust, with generated RPC types, ordered event routing, lifecycle cleanup, and tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 3 + nodejs/README.md | 23 + nodejs/src/client.ts | 89 ++- nodejs/src/generated/rpc.ts | 77 ++- nodejs/src/generated/session-events.ts | 809 ++++++++++++++++++++++++- nodejs/src/index.ts | 8 +- nodejs/src/session.ts | 84 +++ nodejs/src/types.ts | 16 +- nodejs/test/client.test.ts | 128 ++++ rust/README.md | 26 + rust/src/generated/api_types.rs | 101 ++- rust/src/generated/rpc.rs | 31 + rust/src/generated/session_events.rs | 808 +++++++++++++++++++++++- rust/src/handler.rs | 1 + rust/src/lib.rs | 48 +- rust/src/router.rs | 5 +- rust/src/session.rs | 2 + rust/src/types.rs | 4 + rust/src/watch.rs | 194 ++++++ rust/tests/api_types_test.rs | 45 ++ rust/tests/session_test.rs | 92 +++ 21 files changed, 2529 insertions(+), 65 deletions(-) create mode 100644 rust/src/watch.rs diff --git a/.gitattributes b/.gitattributes index 2a92ef0172..4be0f40e95 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,8 @@ # Cross-platform tools rewrite these files, so keep their output deterministic. java/**/*.java text eol=lf +nodejs/**/*.ts text eol=lf +rust/**/*.rs text eol=lf # Generated files — keep LF line endings so codegen output is deterministic across platforms. nodejs/src/generated/* eol=lf linguist-generated=true @@ -11,3 +13,4 @@ go/zsession_events.go eol=lf linguist-generated=true go/zsession_encoding.go eol=lf linguist-generated=true go/rpc/zrpc.go eol=lf linguist-generated=true go/rpc/zrpc_encoding.go eol=lf linguist-generated=true +rust/src/generated/* eol=lf linguist-generated=true diff --git a/nodejs/README.md b/nodejs/README.md index eec674ce4e..7ce2403c62 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -147,6 +147,29 @@ Create a new conversation session. Resume an existing session. Returns the session with `workspacePath` populated if infinite sessions were enabled. +##### `watchSharedSession(sessionId: string): Promise` + +Watch a session another user shared with the authenticated user. The handle is +passive: it exposes `sessionId`, `metadata`, `readOnly`, `on(...)`, and +`close()`, but no send, steer, permission, configuration, or cancellation APIs. +History is delivered first through the ordinary session event stream, followed +by live updates. Terminal connection loss is reported through the client's +existing `session.disconnected` lifecycle event. + +```typescript +const disconnected = client.onLifecycle("session.disconnected", ({ sessionId }) => { + console.log(`Watch ${sessionId} disconnected`); +}); +await using watch = await client.watchSharedSession(sharedSessionId); +watch.on((event) => { + console.log(event.type, event.data); +}); +``` + +Authentication, viewer identity, lane credentials, channel derivation, and +reconnection remain internal to the runtime. Register the lifecycle handler +before opening the watch so an immediate terminal disconnect cannot be missed. + ##### `ping(message?: string): Promise<{ message: string; timestamp: string }>` Ping the server to check connectivity. diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 2dfae099f1..957fa0e800 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -39,7 +39,7 @@ import type { SessionUpdateOptionsParams, } from "./generated/rpc.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; -import { CopilotSession } from "./session.js"; +import { CopilotSession, SharedSessionWatch } from "./session.js"; import type { FfiRuntimeHost } from "./ffiRuntimeHost.js"; import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js"; import { createCopilotRequestAdapter } from "./copilotRequestHandler.js"; @@ -483,6 +483,7 @@ export class CopilotClient { private actualHost: string = "localhost"; private state: "disconnected" | "connecting" | "connected" | "error" = "disconnected"; private sessions: Map = new Map(); + private sharedSessionWatches: Map = new Map(); private stderrBuffer: string = ""; // Captures CLI stderr for error messages /** Resolved connection mode chosen in the constructor. */ private connectionConfig: InternalRuntimeConnection; @@ -966,6 +967,19 @@ export class CopilotClient { async stop(): Promise { const errors: Error[] = []; + const activeWatches = [...this.sharedSessionWatches.values()]; + for (const watch of activeWatches) { + try { + await watch.close(); + } catch (error) { + errors.push( + new Error( + `Failed to close shared-session watch ${watch.sessionId}: ${error instanceof Error ? error.message : String(error)}` + ) + ); + } + } + // Disconnect all active sessions with retry logic const activeSessions = [...this.sessions.values()]; // TEMPORARY: over the in-process (FFI) transport the runtime shares this @@ -1015,6 +1029,7 @@ export class CopilotClient { session._markDisconnected(); } this.sessions.clear(); + this.sharedSessionWatches.clear(); // Ask SDK-owned runtimes to flush and clean up before we tear down // their transport/process. External runtimes may be shared, so only @@ -1197,6 +1212,7 @@ export class CopilotClient { session._markDisconnected(); } this.sessions.clear(); + this.sharedSessionWatches.clear(); // Force close connection. Suppress writer failures first so teardown // write rejections don't surface as unhandled rejections. @@ -1682,6 +1698,54 @@ export class CopilotClient { return session; } + /** + * Watch a session shared with the authenticated user. + * + * The returned handle exposes canonical history and live events but no + * interactive session operations. Authentication and lane routing remain + * entirely inside the runtime. Register a `session.disconnected` lifecycle + * handler before calling this method if terminal connection loss must not + * be missed. + * + * @param sessionId - The owner's shared session ID. + */ + async watchSharedSession(sessionId: string): Promise { + if (!this.connection) { + await this.start(); + } + + const result = await this.rpc.sessions.watch({ sessionId }); + if (result.readOnly !== true) { + await this.rpc.sessions.close({ sessionId: result.sessionId }); + throw new Error("Runtime returned an interactive shared-session watch"); + } + + const routedSession = new CopilotSession( + result.sessionId, + this.connection!, + undefined, + this.onGetTraceContext + ); + const closeWatch = async (): Promise => { + try { + await this.rpc.sessions.close({ sessionId: result.sessionId }); + } finally { + routedSession._markDisconnected(); + this.sessions.delete(result.sessionId); + this.sharedSessionWatches.delete(result.sessionId); + } + }; + const watch = new SharedSessionWatch( + result.sessionId, + result.metadata, + routedSession, + closeWatch + ); + this.sessions.set(result.sessionId, routedSession); + this.sharedSessionWatches.set(result.sessionId, watch); + return watch; + } + /** * Resumes an existing conversation session by its ID. * @@ -2992,11 +3056,18 @@ export class CopilotClient { }; } - const event = { - type: raw.type, - sessionId: raw.sessionId, - metadata, - } as SessionLifecycleEvent; + const event = ( + raw.type === "session.disconnected" + ? { + type: raw.type, + sessionId: raw.sessionId, + } + : { + type: raw.type, + sessionId: raw.sessionId, + metadata, + } + ) as SessionLifecycleEvent; // Dispatch to typed handlers for this specific event type const typedHandlers = this.typedLifecycleHandlers.get(event.type); @@ -3018,6 +3089,12 @@ export class CopilotClient { // Ignore handler errors } } + + if (event.type === "session.disconnected" && this.sharedSessionWatches.has(event.sessionId)) { + this.sessions.get(event.sessionId)?._markDisconnected(); + this.sessions.delete(event.sessionId); + this.sharedSessionWatches.delete(event.sessionId); + } } private async handleUserInputRequest(params: { diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 65b7c3701a..1673288c3e 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -2548,8 +2548,24 @@ export type PermissionDecisionSurface = | "prompt_mode" /** The Copilot App client. */ | "copilot_app" + /** An Agent Client Protocol host. */ + | "acp" /** A generic Copilot SDK client. */ | "sdk"; +/** + * Response capability available to the client when it settled a permission request. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "PermissionResponseCapability". + */ +/** @experimental */ +export type PermissionResponseCapability = + /** The client could ask a user for this decision. */ + | "interactive" + /** The client could return an automated response but could not ask a user. */ + | "headless" + /** The client had no response path available. */ + | "none"; /** * Tool approval to persist and apply * @@ -7252,6 +7268,10 @@ export interface ExternalToolTextResultForLlmContentShellExit { * Whether outputPreview is known to be incomplete or truncated */ outputTruncated?: boolean; + /** + * Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. + */ + outputFilePath?: string; } /** * Image content block with base64-encoded data @@ -8372,6 +8392,14 @@ export interface GitHubTelemetryClientInfo { * Stable machine identifier for the device. */ dev_device_id?: string; + /** + * Distinct CPU model names for the host, comma-separated. + */ + cpu_model?: string; + /** + * Number of logical CPU cores on the host. + */ + cpu_count?: number; } /** * A single telemetry event in the runtime's native GitHub-shaped telemetry format, forwarded verbatim to opted-in hosts. The `restricted` flag on the enclosing GitHubTelemetryNotification distinguishes standard from restricted events; the payload shape is identical for both. @@ -13498,6 +13526,7 @@ export interface PermissionDecisionContext { outcome: PermissionDecisionOutcome; source: PermissionDecisionSource; surface: PermissionDecisionSurface; + responseCapability?: PermissionResponseCapability; } /** * Pending permission request ID and the decision to apply (approve/reject and scope). @@ -20980,10 +21009,6 @@ export interface ToolsExecuteRequest { */ /** @experimental */ export interface ToolsGetBuiltinDescriptorsRequest { - /** - * Whether line numbers should be omitted from the view tool descriptor. - */ - noViewLineNumbers?: boolean; /** * Whether descriptors should favor fewer user-intervention prompts. */ @@ -20997,10 +21022,6 @@ export interface ToolsGetBuiltinDescriptorsRequest { */ skillEmbeddingEnabled?: boolean; shellConfig?: ToolsShellDescriptorConfig; - /** - * Whether shell commands may only run asynchronously. - */ - shellAsyncOnlyEnabled?: boolean; /** * Whether the configured shell supports PowerShell 7 syntax. */ @@ -22119,6 +22140,37 @@ export interface VisibilitySetResult { */ shareUrl?: string; } +/** + * Parameters for watching a session another user has shared with the authenticated user. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WatchSharedSessionParams". + */ +/** @experimental */ +export interface WatchSharedSessionParams { + /** + * Session ID to watch. The session belongs to another user and must already be shared with the authenticated user. The watcher's own identity is deliberately not accepted here: it is resolved from the connection's authenticated credential, so a caller cannot ask to watch as somebody else. + */ + sessionId: string; +} +/** + * Result of attaching to a shared session as a read-only watcher. History replays as ordered `session.event` notifications after this result is delivered, not inside it. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "WatchSharedSessionResult". + */ +/** @experimental */ +export interface WatchSharedSessionResult { + /** + * SDK session ID for the watched session. + */ + sessionId: string; + /** + * Always true. A watched session observes and replays only: it cannot send or queue input, steer, answer prompts, approve tools, change session configuration or cancel turns. Server-side denial remains the authority; this flag lets a client refuse the interaction up front rather than surfacing a late failure. + */ + readOnly: true; + metadata: ConnectedRemoteSessionMetadata; +} /** * A single changed file and its unified diff. * @@ -23215,6 +23267,15 @@ export function createServerRpc(connection: MessageConnection) { */ connect: async (params: ConnectRemoteSessionParams): Promise => connection.sendRequest("sessions.connect", params), + /** + * Attaches to a session another user has shared with the authenticated user, as a read-only watcher, and exposes it as an SDK session. The watched session replays and streams over the ordinary `session.event` notification channel, but cannot be driven: sending, steering, answering prompts, approving tools, changing session configuration and cancelling turns are all refused. The watcher's own identity is resolved from the connection's credential, never from the caller. + * + * @param params Parameters for watching a session another user has shared with the authenticated user. + * + * @returns Result of attaching to a shared session as a read-only watcher. History replays as ordered `session.event` notifications after this result is delivered, not inside it. + */ + watch: async (params: WatchSharedSessionParams): Promise => + connection.sendRequest("sessions.watch", params), /** * Lists sessions, optionally filtered by source and working-directory context. Returned entries are discriminated by `isRemote`: local entries carry only the lightweight `LocalSessionMetadataValue` shape; remote entries carry the full `RemoteSessionMetadataValue` shape (repository, PR number, taskType, etc.). * diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 3ec55aacda..2942405c45 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -40,10 +40,17 @@ export type SessionEvent = | CompactionStartEvent | CompactionCompleteEvent | TaskCompleteEvent + | FusionRouteStartedEvent + | FusionRouteFailedEvent + | FusionResolvedEvent + | FusionCompletedEvent | UserMessageEvent | PendingMessagesModifiedEvent | AssistantTurnStartEvent | AssistantIntentEvent + | AssistantFusionPhaseStartedEvent + | AssistantFusionPhaseCompletedEvent + | AssistantFusionPhaseFailedEvent | AssistantServerToolProgressEvent | AssistantReasoningEvent | AssistantReasoningDeltaEvent @@ -163,6 +170,16 @@ export type Verbosity = | "medium" /** A more detailed response was requested. */ | "high"; +/** + * The session mode the agent is operating in + */ +export type SessionMode = + /** The agent is responding interactively to the user. */ + | "interactive" + /** The agent is preparing a plan before making changes. */ + | "plan" + /** The agent is working autonomously toward task completion. */ + | "autopilot"; /** * Who created the schedule: `user` (an explicit user action such as `/every` or `/after`) or `model` (the agent via the `manage_schedule` tool). Gates whether a scheduled skill that opted out of model invocation may fire: only user-created schedules may. */ @@ -219,16 +236,6 @@ export type ModelChangeSource = | "automatic" /** An SDK or RPC caller selected the model. */ | "sdk"; -/** - * The session mode the agent is operating in - */ -export type SessionMode = - /** The agent is responding interactively to the user. */ - | "interactive" - /** The agent is preparing a plan before making changes. */ - | "plan" - /** The agent is working autonomously toward task completion. */ - | "autopilot"; /** * Permission mode for the session. */ @@ -298,6 +305,35 @@ export type TaskCompletionOutcome = | "continue" /** Completion cannot proceed without intervention; the active objective is paused when one is identified. */ | "blocked"; +/** + * Kind of turn for which HydraFusion routing is running. + */ +/** @experimental */ +export type FusionTurnKind = + /** A user-message turn. */ + | "user" + /** A conversation-compaction turn. */ + | "compaction"; +/** + * Server-recommended routing behavior for a later HydraFusion turn. + */ +/** @experimental */ +export type FusionFollowUpAction = + /** Reuse the durable primary model without routing. */ + | "reuse_primary" + /** Request a new routing decision. */ + | "reroute"; +/** + * Validated HydraFusion execution pattern. + */ +/** @experimental */ +export type FusionPattern = + /** Run one primary solver phase. */ + | "single" + /** Run a primary phase, a judge, and an optional repair. */ + | "cascade" + /** Run a primary draft, a read-only critique, and a revision. */ + | "critique"; /** * The agent mode that was active when this message was sent */ @@ -357,6 +393,57 @@ export type UserMessageDelivery = | "steering" /** Enqueued while the agent was busy; processed as its own run afterward. */ | "queued"; +/** + * Conversation scope in which a HydraFusion phase executes. + */ +/** @experimental */ +export type FusionConversationScope = + /** Canonical root conversation history. */ + | "root" + /** Isolated read-only review history that does not enter the root conversation. */ + | "review"; +/** + * HydraFusion phase kind. + */ +/** @experimental */ +export type FusionPhaseKind = + /** Primary solver phase. */ + | "primary" + /** Read-only cascade judge phase. */ + | "judge" + /** Cascade repair phase. */ + | "repair" + /** Initial critique-pattern draft phase. */ + | "draft" + /** Read-only critique phase. */ + | "critic" + /** Critique-pattern revision phase. */ + | "revision" + /** Follow-up phase continuing from the resolved model. */ + | "follow_up"; +/** + * How a durable phase checkpoint contributes its exact message to canonical root history. + */ +/** @experimental */ +/** @internal */ +export type FusionProjectionMode = + /** Append the exact root message immediately. */ + | "append" + /** Hold a terminal message outside canonical history until the final commit selects it. */ + | "staged" + /** Do not project the checkpoint into root history. */ + | "none"; +/** + * Durable outcome status of a HydraFusion phase. + */ +/** @experimental */ +export type FusionPhaseStatus = + /** The phase completed successfully. */ + | "succeeded" + /** The phase failed. */ + | "failed" + /** The phase was cancelled. */ + | "cancelled"; /** * Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. */ @@ -1362,6 +1449,7 @@ export interface IdleData { * True when the preceding agentic loop was cancelled via abort signal */ aborted?: boolean; + mode?: SessionMode; } /** * Session event "session.title_changed". Session title change payload containing the new display title @@ -2916,6 +3004,365 @@ export interface TaskCompleteData { */ summary?: string; } +/** + * Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. + */ +/** @experimental */ +export interface FusionRouteStartedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionRouteStartedData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.fusion_route_started". + */ + type: "session.fusion_route_started"; +} +/** + * Experimental transient signal that HydraFusion routing has started for an eligible turn. + */ +/** @experimental */ +export interface FusionRouteStartedData { + /** + * Identifier for this routing attempt before a durable Fusion turn exists. + */ + attemptId: string; + /** + * HydraFusion routing policy requested for the turn. + */ + policy?: string; + /** + * Synthetic HydraFusion model selected for the session. + */ + syntheticModel?: string; + turnKind: FusionTurnKind; +} +/** + * Session event "session.fusion_route_failed". Experimental durable HydraFusion routing failure and the deterministic concrete fallback selected for the turn. + */ +/** @experimental */ +export interface FusionRouteFailedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionRouteFailedData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.fusion_route_failed". + */ + type: "session.fusion_route_failed"; +} +/** + * Experimental durable HydraFusion routing failure and the deterministic concrete fallback selected for the turn. + */ +/** @experimental */ +export interface FusionRouteFailedData { + /** + * Identifier of the routing attempt that failed. + */ + attemptId: string; + /** + * Provider or validation error detail, when available. + */ + errorMessage?: string; + /** + * Concrete model selected as the deterministic fallback. + */ + fallbackModel: string; + /** + * HydraFusion routing policy requested for the turn. + */ + policy: string; + /** + * Stable machine-readable reason for the routing failure. + */ + reason: string; + /** + * Elapsed routing time in milliseconds before the failure. + */ + routingLatencyMs?: number; + /** + * Synthetic HydraFusion model selected for the session. + */ + syntheticModel: string; +} +/** + * Session event "session.fusion_resolved". Experimental durable validated HydraFusion route and turn policy. + */ +/** @experimental */ +export interface FusionResolvedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionResolvedData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.fusion_resolved". + */ + type: "session.fusion_resolved"; +} +/** + * Experimental durable validated HydraFusion route and turn policy. + */ +/** @experimental */ +export interface FusionResolvedData { + /** + * Version of the validated HydraFusion event contract. + */ + contractVersion: number; + /** + * Concrete model used when the planned primary model cannot execute. + */ + fallbackModel: string; + followUp?: FusionFollowUpRecommendation; + /** + * Concrete model recommended for eligible follow-up turns. + */ + followUpModel: string; + /** + * Stable identifier for the resolved HydraFusion turn. + */ + fusionId: string; + /** + * Version of the executable model universe used for selection. + */ + modelUniverseVersion?: string; + pattern: FusionPattern; + /** + * Version of the validated execution-plan format. + */ + planVersion?: string; + /** + * HydraFusion routing policy used to resolve the plan. + */ + policy: string; + /** + * Version of the local routing policy. + */ + policyVersion?: string; + /** + * Concrete model selected for the primary solver phase. + */ + primaryModel: string; + /** + * Router implementation that supplied the plan. + */ + routeSource?: string; + /** + * Elapsed time in milliseconds required to resolve and validate the route. + */ + routingLatencyMs?: number; + /** + * Identifier of the local policy rule that matched. + */ + ruleId?: string; + /** + * Zero-based index of the local policy rule that matched. + */ + ruleIndex?: number; + /** + * Human-readable name of the local policy rule that matched. + */ + ruleName?: string; + scores?: FusionScores; + /** + * Concrete model selected for the review or judge phase, when required. + */ + secondaryModel: string | null; + /** + * Synthetic HydraFusion model selected for the session. + */ + syntheticModel: string; + /** + * Identifier of the session turn associated with the route. + */ + turnId: string; +} +/** + * Durable server recommendation for subsequent HydraFusion turns. + */ +/** @experimental */ +export interface FusionFollowUpRecommendation { + compactionTurn: FusionFollowUpAction; + userTurn: FusionFollowUpAction; +} +/** + * Validated HydraFusion routing capability scores. + */ +/** @experimental */ +export interface FusionScores { + /** + * Code-generation capability score returned by the authenticated router. + */ + codeGen: number; + /** + * Debugging capability score returned by the authenticated router. + */ + debugging: number; + /** + * Reasoning capability score returned by the authenticated router. + */ + reasoning: number; + /** + * Tool-use capability score returned by the authenticated router. + */ + toolUse: number; +} +/** + * Session event "session.fusion_completed". Experimental durable aggregate outcome of a HydraFusion turn. + */ +/** @experimental */ +export interface FusionCompletedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionCompletedData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.fusion_completed". + */ + type: "session.fusion_completed"; +} +/** + * Experimental durable aggregate outcome of a HydraFusion turn. + */ +/** @experimental */ +export interface FusionCompletedData { + /** + * Total cached input tokens reported across all phases. + */ + cachedTokens: number; + /** + * Total tokens written to prompt cache across all phases. + */ + cacheWriteTokens?: number; + /** + * Idempotency identifier for the authoritative final commit. + */ + commitId: string; + /** + * Reason the turn used a degraded route, when applicable. + */ + degradedReason: string | null; + /** + * Total elapsed execution time for the HydraFusion turn in milliseconds. + */ + durationMs: number; + /** + * Concrete model that supplied the authoritative final content. + */ + finalSourceModel: string | null; + /** + * Phase whose output supplied the authoritative final content. + */ + finalSourcePhaseId: string | null; + /** + * Concrete model recommended for eligible follow-up turns. + */ + followUpModel: string; + /** + * Stable identifier for the completed HydraFusion turn. + */ + fusionId: string; + /** + * Total input tokens consumed across all phases. + */ + inputTokens: number; + /** + * Stable aggregate outcome of the HydraFusion turn. + */ + outcome: string; + /** + * Total output tokens produced across all phases. + */ + outputTokens: number; + pattern: FusionPattern; + /** + * Number of concrete phases attempted by the turn. + */ + phaseCount: number; + /** + * Total concrete model requests made across all phases. + */ + requestCount: number; + /** + * Synthetic HydraFusion model selected for the session. + */ + syntheticModel: string; + /** + * Total normalized AI-unit cost reported across all phases, in nano-AIU. + */ + totalNanoAiu: number; + /** + * Identifier of the session turn associated with the completion. + */ + turnId: string; +} /** * Session event "user.message". Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. */ @@ -3551,6 +3998,264 @@ export interface AssistantIntentData { */ intent: string; } +/** + * Session event "assistant.fusion_phase_started". Experimental transient HydraFusion phase/model/role signal. + */ +/** @experimental */ +export interface AssistantFusionPhaseStartedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionPhaseStartedData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "assistant.fusion_phase_started". + */ + type: "assistant.fusion_phase_started"; +} +/** + * Experimental transient HydraFusion phase/model/role signal. + */ +/** @experimental */ +export interface FusionPhaseStartedData { + conversationScope: FusionConversationScope; + /** + * Identifier of the HydraFusion turn containing the phase. + */ + fusionId: string; + /** + * Concrete model executing the phase. + */ + model: string; + pattern: FusionPattern; + /** + * Stable identifier for the concrete phase. + */ + phaseId: string; + phaseKind: FusionPhaseKind; + /** + * Semantic role assigned to the phase. + */ + role: string; +} +/** + * Session event "assistant.fusion_phase_completed". Experimental durable HydraFusion phase output and lossless replay checkpoint. + */ +/** @experimental */ +export interface AssistantFusionPhaseCompletedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionPhaseCompletedData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "assistant.fusion_phase_completed". + */ + type: "assistant.fusion_phase_completed"; +} +/** + * Experimental durable HydraFusion phase output and lossless replay checkpoint. + */ +/** @experimental */ +export interface FusionPhaseCompletedData { + /** + * Provider-normalized textual output produced by the phase. + */ + content: string; + conversationScope: FusionConversationScope; + /** + * Elapsed execution time for the phase in milliseconds. + */ + durationMs: number; + /** + * Identifier of the HydraFusion turn containing the phase. + */ + fusionId: string; + /** + * Concrete model that executed the phase. + */ + model: string; + /** + * Stable identifier for the completed phase. + */ + phaseId: string; + phaseKind: FusionPhaseKind; + /** + * Exact provider-normalized message used to reconstruct canonical model history. + * + * @internal + */ + projectionMessage?: JsonValue; + /** + * Projection action for the exact internal message. + * + * @internal + */ + projectionMode?: FusionProjectionMode; + /** + * Semantic role assigned to the completed phase. + */ + role: string; + /** + * Terminal request held outside canonical state until selected by the final commit. + * + * @internal + */ + stagedTerminal?: FusionStagedTerminal; + status: FusionPhaseStatus; + usage: FusionPhaseUsage; + /** + * Structured judge or critic verdict, when the phase produces one. + */ + verdict: string | null; +} +/** + * Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. + */ +/** @experimental */ +/** @internal */ +export interface FusionStagedTerminal { + arguments: string; + assistantMessage: JsonValue; + phaseId: string; + toolCallId: string; + toolName: string; +} +/** + * Aggregate concrete-model usage for one HydraFusion phase. + */ +/** @experimental */ +export interface FusionPhaseUsage { + /** + * Total cached input tokens reported for the phase. + */ + cachedTokens: number; + /** + * Total tokens written to prompt cache during the phase. + */ + cacheWriteTokens?: number; + /** + * Total input tokens consumed by the phase. + */ + inputTokens: number; + /** + * Total output tokens produced by the phase. + */ + outputTokens: number; + /** + * Number of concrete model requests made by the phase. + */ + requestCount: number; + /** + * Total normalized AI-unit cost reported for the phase, in nano-AIU. + */ + totalNanoAiu: number; +} +/** + * Session event "assistant.fusion_phase_failed". Experimental durable typed HydraFusion phase failure and degradation transition. + */ +/** @experimental */ +export interface AssistantFusionPhaseFailedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionPhaseFailedData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "assistant.fusion_phase_failed". + */ + type: "assistant.fusion_phase_failed"; +} +/** + * Experimental durable typed HydraFusion phase failure and degradation transition. + */ +/** @experimental */ +export interface FusionPhaseFailedData { + conversationScope: FusionConversationScope; + /** + * Identifier of the fallback phase used to continue the turn after degradation. + */ + degradedToPhaseId?: string; + /** + * Elapsed execution time before the phase failed, in milliseconds. + */ + durationMs: number; + /** + * Provider or execution error detail, when available. + */ + errorMessage?: string; + /** + * Identifier of the HydraFusion turn containing the phase. + */ + fusionId: string; + /** + * Concrete model that attempted the phase. + */ + model: string; + /** + * Stable identifier for the failed phase. + */ + phaseId: string; + phaseKind: FusionPhaseKind; + /** + * Stable machine-readable reason for the phase failure. + */ + reason: string; + /** + * Semantic role assigned to the failed phase. + */ + role: string; + status: FusionPhaseStatus; + usage: FusionPhaseUsage; +} /** * Session event "assistant.server_tool_progress". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message */ @@ -3839,6 +4544,12 @@ export interface AssistantMessageData { * Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. */ encryptedContent?: string; + /** + * Experimental HydraFusion source attribution for this ordinary authoritative assistant message. + * + * @experimental + */ + fusion?: FusionAttribution; /** * CAPI interaction ID for correlating this message with upstream telemetry */ @@ -4027,6 +4738,56 @@ export interface CitationLocationBlock { */ type: "block"; } +/** + * Experimental attribution linking an ordinary event to the HydraFusion turn, phase, and concrete source that produced it. + */ +/** @experimental */ +export interface FusionAttribution { + /** + * Idempotency identifier for the authoritative commit, when the event belongs to the selected output. + */ + commitId?: string; + /** + * Conversation scope in which the concrete phase executed. + */ + conversationScope?: string; + /** + * Stable identifier for the HydraFusion turn that produced the event. + */ + fusionId: string; + /** + * HydraFusion orchestration pattern selected for the turn. + */ + pattern: string; + /** + * Identifier of the concrete phase that produced the event. + */ + phaseId?: string; + /** + * Kind of concrete phase that produced the event. + */ + phaseKind?: string; + /** + * HydraFusion routing policy used for the turn. + */ + policy: string; + /** + * Semantic role assigned to the concrete phase. + */ + role?: string; + /** + * Concrete model that produced the attributed event. + */ + sourceModel?: string; + /** + * Phase whose output supplied the authoritative content, when different from the executing phase. + */ + sourcePhaseId?: string; + /** + * Synthetic HydraFusion model selected for the session. + */ + syntheticModel: string; +} /** * Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping */ @@ -4374,6 +5135,12 @@ export interface AssistantUsageData { * @internal */ frontierSource?: string; + /** + * Experimental HydraFusion attribution for this concrete model call's usage. + * + * @experimental + */ + fusion?: FusionAttribution; /** * What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ @@ -4647,6 +5414,12 @@ export interface ModelCallFailureData { */ errorType?: string; failureKind?: ModelCallFailureKind; + /** + * Experimental HydraFusion attribution for this failed concrete model call. + * + * @experimental + */ + fusion?: FusionAttribution; /** * What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ @@ -4921,6 +5694,12 @@ export interface ToolExecutionStartData { * When true, the tool output should be displayed expanded (verbatim) in the CLI timeline */ displayVerbatim?: boolean; + /** + * Experimental HydraFusion attribution for this tool execution. + * + * @experimental + */ + fusion?: FusionAttribution; /** * Name of the MCP server hosting this tool, when the tool is an MCP tool */ @@ -5130,6 +5909,12 @@ export interface ToolExecutionCompleteEvent { */ export interface ToolExecutionCompleteData { error?: ToolExecutionCompleteError; + /** + * Experimental HydraFusion attribution for this tool completion. + * + * @experimental + */ + fusion?: FusionAttribution; /** * CAPI interaction ID for correlating this tool execution with upstream telemetry */ @@ -5387,6 +6172,10 @@ export interface ToolExecutionCompleteContentShellExit { * Exit code from the completed shell command */ exitCode: number; + /** + * Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. + */ + outputFilePath?: string; /** * Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output. */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index ae474eefee..613110905b 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -11,7 +11,12 @@ export { CopilotClient } from "./client.js"; export { DisableBypassPermissionsModes, RuntimeConnection } from "./types.js"; export { BuiltInTools, ToolSet } from "./toolSet.js"; -export { CopilotSession, type AssistantMessageEvent } from "./session.js"; +export { + CopilotSession, + SharedSessionWatch, + type AssistantMessageEvent, + type SharedSessionMetadata, +} from "./session.js"; export { defineFactory, FactoryResumeError, isFactoryRunTerminal } from "./factory.js"; export { Canvas, @@ -152,6 +157,7 @@ export type { SessionHooks, SessionCreatedEvent, SessionDeletedEvent, + SessionDisconnectedEvent, SessionUpdatedEvent, SessionForegroundEvent, SessionBackgroundEvent, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index efe9fa3e91..f742d9b760 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -14,6 +14,8 @@ import { createSessionRpc } from "./generated/rpc.js"; import type { ClientSessionApiHandlers, CanvasActionInvokeResult, + ConnectedRemoteSessionMetadata, + ConnectedRemoteSessionMetadataRepository, CurrentToolMetadata, McpOauthPendingRequestResponse, FactoryLogLine, @@ -81,6 +83,88 @@ import { type FactoryStepOptions, } from "./factory.js"; +/** Immutable metadata describing a watched shared session. */ +export type SharedSessionMetadata = Readonly< + Omit & { + repository: Readonly; + } +>; + +/** + * Passive, read-only attachment to a session shared with the authenticated user. + * + * History and live updates are delivered through {@link on}. Interactive session + * operations are intentionally absent from this type. + */ +export class SharedSessionWatch { + /** Always `true`; watched sessions cannot be driven by this client. */ + readonly readOnly = true as const; + /** Immutable metadata returned by the runtime when the watch is attached. */ + readonly metadata: SharedSessionMetadata; + + private readonly handlers = new Set(); + private readonly pendingEvents: SessionEvent[] = []; + private closePromise: Promise | undefined; + + /** @internal */ + constructor( + readonly sessionId: string, + metadata: ConnectedRemoteSessionMetadata, + session: CopilotSession, + private readonly closeWatch: () => Promise + ) { + this.metadata = Object.freeze({ + ...metadata, + repository: Object.freeze({ ...metadata.repository }), + }); + session.on((event) => { + if (this.handlers.size === 0) { + this.pendingEvents.push(event); + return; + } + for (const handler of this.handlers) { + try { + handler(event); + } catch { + // A failing subscriber must not prevent delivery to others. + } + } + }); + } + + /** + * Subscribe to canonical replay and live session events. + * + * Events replayed before the first handler is attached are retained and + * delivered in order when that handler is registered. + */ + on(handler: SessionEventHandler): () => void { + this.handlers.add(handler); + if (this.pendingEvents.length > 0) { + const pending = this.pendingEvents.splice(0); + for (const event of pending) { + handler(event); + } + } + return () => this.handlers.delete(handler); + } + + /** + * Close the watch and release its local event routing. + * + * Repeated calls share the same close operation. + */ + close(): Promise { + this.closePromise ??= this.closeWatch(); + return this.closePromise; + } + + /** Close the watch when used with `await using`. */ + async [Symbol.asyncDispose](): Promise { + await this.close(); + } +} + function isFactoryResumeErrorCode(value: unknown): value is FactoryResumeErrorCode { return ( value === "not_found" || diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 5b5d8da482..b258aaccf9 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -3459,11 +3459,12 @@ export type SessionLifecycleEventType = | "session.deleted" | "session.updated" | "session.foreground" - | "session.background"; + | "session.background" + | "session.disconnected"; /** * Metadata payload for session lifecycle events. Not present on - * `session.deleted` events. + * `session.deleted` or `session.disconnected` events. */ export interface SessionLifecycleEventMetadata { /** Time the session was created. */ @@ -3478,7 +3479,7 @@ export interface SessionLifecycleEventMetadata { interface SessionLifecycleEventBase { /** ID of the session this event relates to. */ sessionId: string; - /** Session metadata (not included for `session.deleted`). */ + /** Session metadata (not included for deleted or disconnected events). */ metadata?: SessionLifecycleEventMetadata; } @@ -3512,6 +3513,12 @@ export interface SessionBackgroundEvent extends SessionLifecycleEventBase { metadata: SessionLifecycleEventMetadata; } +/** Emitted when a session connection is terminally lost. */ +export interface SessionDisconnectedEvent extends SessionLifecycleEventBase { + type: "session.disconnected"; + metadata?: undefined; +} + /** * Discriminated union of all session lifecycle events emitted in TUI+server mode. * Switch on `type` to access the variant-specific metadata. @@ -3521,7 +3528,8 @@ export type SessionLifecycleEvent = | SessionDeletedEvent | SessionUpdatedEvent | SessionForegroundEvent - | SessionBackgroundEvent; + | SessionBackgroundEvent + | SessionDisconnectedEvent; /** * Handler for session lifecycle events. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 3ffda2fa71..5b6d852500 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -17,6 +17,7 @@ import { type ModelInfo, } from "../src/index.js"; import { CopilotSession } from "../src/session.js"; +import type { WatchSharedSessionParams, WatchSharedSessionResult } from "../src/generated/rpc.js"; import { defaultJoinSessionPermissionHandler } from "../src/types.js"; // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead @@ -1186,6 +1187,133 @@ describe("CopilotClient", () => { expect(received).toEqual([notification]); }); + it("watches a shared session without exposing interactive session methods", async () => { + const { createMessageConnection, StreamMessageReader, StreamMessageWriter } = + await import("vscode-jsonrpc/node.js"); + + const clientToServer = new PassThrough(); + const serverToClient = new PassThrough(); + const clientConn = createMessageConnection( + new StreamMessageReader(serverToClient), + new StreamMessageWriter(clientToServer) + ); + const serverConn = createMessageConnection( + new StreamMessageReader(clientToServer), + new StreamMessageWriter(serverToClient) + ); + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("localhost:1234"), + }); + (client as any).connection = clientConn; + (client as any).attachConnectionHandlers(); + + onTestFinished(() => { + clientConn.dispose(); + serverConn.dispose(); + }); + + const closeRequests: unknown[] = []; + serverConn.onRequest("sessions.watch", async (params) => { + expect(params).toEqual({ sessionId: "shared-session" }); + return { + sessionId: "watch-session", + readOnly: true, + metadata: { + sessionId: "watch-session", + startTime: "2025-01-01T00:00:00Z", + modifiedTime: "2025-01-01T00:01:00Z", + repository: { owner: "github", name: "copilot-sdk", branch: "main" }, + kind: "remote-session", + }, + }; + }); + serverConn.onRequest("sessions.close", async (params) => { + closeRequests.push(params); + return {}; + }); + clientConn.listen(); + serverConn.listen(); + + const disconnected = new Promise((resolve) => { + client.onLifecycle("session.disconnected", (event) => { + expect(event).toEqual({ + type: "session.disconnected", + sessionId: "watch-session", + }); + expect(Object.keys(event).sort()).toEqual(["sessionId", "type"]); + resolve(); + }); + }); + const watch = await client.watchSharedSession("shared-session"); + expect((client as any).sessions.has("watch-session")).toBe(true); + const replay = { + type: "assistant.message", + id: "replay-event", + parentId: null, + timestamp: "2025-01-01T00:00:30Z", + data: { content: "history" }, + } as const; + + await serverConn.sendNotification("session.event", { + sessionId: "watch-session", + event: replay, + }); + await serverConn.sendNotification("session.lifecycle", { + type: "session.disconnected", + sessionId: "watch-session", + }); + + const received: string[] = []; + await new Promise((resolve) => { + watch.on((event) => { + received.push(event.type); + resolve(); + }); + }); + await disconnected; + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(watch.sessionId).toBe("watch-session"); + expect(watch.readOnly).toBe(true); + expect(watch.metadata.sessionId).toBe("watch-session"); + expect(Object.isFrozen(watch.metadata)).toBe(true); + expect(Object.isFrozen(watch.metadata.repository)).toBe(true); + expect(received).toEqual(["assistant.message"]); + expect((client as any).sessions.has("watch-session")).toBe(false); + expect((client as any).sharedSessionWatches.has("watch-session")).toBe(false); + expect("send" in watch).toBe(false); + expect("abort" in watch).toBe(false); + if (false) { + // @ts-expect-error A shared-session watch is intentionally passive. + await watch.send({ prompt: "not allowed" }); + } + + await watch.close(); + await watch.close(); + expect(closeRequests).toEqual([{ sessionId: "watch-session" }]); + }); + + it("generates the exact credential-free shared-session watch payloads", () => { + const params: WatchSharedSessionParams = { sessionId: "shared-session" }; + const result: WatchSharedSessionResult = { + sessionId: "watch-session", + readOnly: true, + metadata: { + sessionId: "watch-session", + startTime: "2025-01-01T00:00:00Z", + modifiedTime: "2025-01-01T00:01:00Z", + repository: { owner: "github", name: "copilot-sdk", branch: "main" }, + kind: "remote-session", + }, + }; + + expect(params).toEqual({ sessionId: "shared-session" }); + expect(Object.keys(result).sort()).toEqual(["metadata", "readOnly", "sessionId"]); + expect(JSON.stringify(result)).not.toMatch( + /viewerId|baseUrl|wps|lane|channel|credential|token/i + ); + }); + it("registers no gitHubTelemetry handler when onGitHubTelemetry is omitted", () => { const client = new CopilotClient(); onTestFinished(() => stopClient(client)); diff --git a/rust/README.md b/rust/README.md index 29fe673558..2afa992807 100644 --- a/rust/README.md +++ b/rust/README.md @@ -172,6 +172,32 @@ session session.disconnect().await?; ``` +### Shared Session Watch + +`Client::watch_shared_session` attaches to a session another user shared with +the authenticated user. The returned `SharedSessionWatch` is passive and +exposes metadata, ordered canonical history/live events, and `close()` without +interactive session methods. Terminal connection loss is reported through the +client's existing `SessionLifecycleEventType::Disconnected` subscription. + +```rust,ignore +let mut lifecycle = client.subscribe_lifecycle(); +let mut watch = client.watch_shared_session(shared_session_id).await?; +tokio::select! { + Some(event) = watch.events().recv() => { + println!("{}: {}", event.event_type, event.data); + } + Ok(event) = lifecycle.recv() => { + println!("{:?}: {}", event.event_type, event.session_id); + } +} +watch.close().await?; +``` + +Authentication, viewer identity, lane credentials, channel derivation, and +reconnection remain internal to the runtime. Subscribe to lifecycle events +before opening the watch so an immediate terminal disconnect cannot be missed. + #### Typed RPC namespace High-level helpers are convenience wrappers over a fully-typed diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 43ea119f8a..82222d415e 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -137,6 +137,8 @@ pub mod rpc_methods { pub const SESSIONS_FORK: &str = "sessions.fork"; /// `sessions.connect` pub const SESSIONS_CONNECT: &str = "sessions.connect"; + /// `sessions.watch` + pub const SESSIONS_WATCH: &str = "sessions.watch"; /// `sessions.list` pub const SESSIONS_LIST: &str = "sessions.list"; /// `sessions.getMetadata` @@ -4879,6 +4881,9 @@ pub struct ExternalToolTextResultForLlmContentShellExit { pub cwd: Option, /// Exit code from the completed shell command pub exit_code: i64, + /// Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. + #[serde(skip_serializing_if = "Option::is_none")] + pub output_file_path: Option, /// Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output. #[serde(skip_serializing_if = "Option::is_none")] pub output_preview: Option, @@ -5832,6 +5837,12 @@ pub struct GitHubTelemetryClientInfo { /// Copilot subscription plan, when known. #[serde(rename = "copilot_plan", skip_serializing_if = "Option::is_none")] pub copilot_plan: Option, + /// Number of logical CPU cores on the host. + #[serde(rename = "cpu_count", skip_serializing_if = "Option::is_none")] + pub cpu_count: Option, + /// Distinct CPU model names for the host, comma-separated. + #[serde(rename = "cpu_model", skip_serializing_if = "Option::is_none")] + pub cpu_model: Option, /// Stable machine identifier for the device. #[serde(rename = "dev_device_id", skip_serializing_if = "Option::is_none")] pub dev_device_id: Option, @@ -11415,6 +11426,9 @@ pub struct PermissionDecisionDeniedByPermissionRequestHook { pub struct PermissionDecisionContext { /// Disposition of the permission request as observed by the responding client. pub outcome: PermissionDecisionOutcome, + /// Whether the responding client could ask a user interactively, was running headlessly, or had no response path. Omit when the client cannot determine this authoritatively. + #[serde(skip_serializing_if = "Option::is_none")] + pub response_capability: Option, /// Controlled reason or actor responsible for the response. pub source: PermissionDecisionSource, /// Client surface that submitted the response. @@ -19614,15 +19628,9 @@ pub struct ToolsGetBuiltinDescriptorsRequest { /// Whether tool descriptors should include authoring metadata. #[serde(skip_serializing_if = "Option::is_none")] pub include_author: Option, - /// Whether line numbers should be omitted from the view tool descriptor. - #[serde(skip_serializing_if = "Option::is_none")] - pub no_view_line_numbers: Option, /// Whether descriptors should favor fewer user-intervention prompts. #[serde(skip_serializing_if = "Option::is_none")] pub reduce_user_intervention: Option, - /// Whether shell commands may only run asynchronously. - #[serde(skip_serializing_if = "Option::is_none")] - pub shell_async_only_enabled: Option, /// Shell-specific names and description lines for shell tools. #[serde(skip_serializing_if = "Option::is_none")] pub shell_config: Option, @@ -20746,6 +20754,40 @@ pub struct VisibilitySetResult { pub synced: bool, } +/// Parameters for watching a session another user has shared with the authenticated user. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WatchSharedSessionParams { + /// Session ID to watch. The session belongs to another user and must already be shared with the authenticated user. The watcher's own identity is deliberately not accepted here: it is resolved from the connection's authenticated credential, so a caller cannot ask to watch as somebody else. + pub session_id: SessionId, +} + +/// Result of attaching to a shared session as a read-only watcher. History replays as ordered `session.event` notifications after this result is delivered, not inside it. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WatchSharedSessionResult { + /// Metadata for the watched session. + pub metadata: ConnectedRemoteSessionMetadata, + /// Always true. A watched session observes and replays only: it cannot send or queue input, steer, answer prompts, approve tools, change session configuration or cancel turns. Server-side denial remains the authority; this flag lets a client refuse the interaction up front rather than surfacing a late failure. + pub read_only: bool, + /// SDK session ID for the watched session. + pub session_id: SessionId, +} + /// A single changed file and its unified diff. /// ///
@@ -21650,6 +21692,25 @@ pub struct SessionsConnectResult { pub session_id: SessionId, } +/// Result of attaching to a shared session as a read-only watcher. History replays as ordered `session.event` notifications after this result is delivered, not inside it. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsWatchResult { + /// Metadata for the watched session. + pub metadata: ConnectedRemoteSessionMetadata, + /// Always true. A watched session observes and replays only: it cannot send or queue input, steer, answer prompts, approve tools, change session configuration or cancel turns. Server-side denial remains the authority; this flag lets a client refuse the interaction up front rather than surfacing a late failure. + pub read_only: bool, + /// SDK session ID for the watched session. + pub session_id: SessionId, +} + /// Sessions matching the filter, ordered most-recently-modified first. /// ///
@@ -31285,6 +31346,31 @@ pub enum PermissionDecisionOutcome { Unknown, } +/// Response capability available to the client when it settled a permission request. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionResponseCapability { + /// The client could ask a user for this decision. + #[serde(rename = "interactive")] + Interactive, + /// The client could return an automated response but could not ask a user. + #[serde(rename = "headless")] + Headless, + /// The client had no response path available. + #[serde(rename = "none")] + None, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Controlled reason or actor responsible for a permission response. /// ///
@@ -31332,6 +31418,9 @@ pub enum PermissionDecisionSurface { /// The Copilot App client. #[serde(rename = "copilot_app")] CopilotApp, + /// An Agent Client Protocol host. + #[serde(rename = "acp")] + Acp, /// A generic Copilot SDK client. #[serde(rename = "sdk")] Sdk, diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 60bf9d804f..590f5b192f 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -1800,6 +1800,37 @@ impl<'a> ClientRpcSessions<'a> { Ok(serde_json::from_value(_value)?) } + /// Attaches to a session another user has shared with the authenticated user, as a read-only watcher, and exposes it as an SDK session. The watched session replays and streams over the ordinary `session.event` notification channel, but cannot be driven: sending, steering, answering prompts, approving tools, changing session configuration and cancelling turns are all refused. The watcher's own identity is resolved from the connection's credential, never from the caller. + /// + /// Wire method: `sessions.watch`. + /// + /// # Parameters + /// + /// * `params` - Parameters for watching a session another user has shared with the authenticated user. + /// + /// # Returns + /// + /// Result of attaching to a shared session as a read-only watcher. History replays as ordered `session.event` notifications after this result is delivered, not inside it. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn watch( + &self, + params: WatchSharedSessionParams, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_WATCH, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Lists sessions, optionally filtered by source and working-directory context. Returned entries are discriminated by `isRemote`: local entries carry only the lightweight `LocalSessionMetadataValue` shape; remote entries carry the full `RemoteSessionMetadataValue` shape (repository, PR number, taskType, etc.). /// /// Wire method: `sessions.list`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index f0508660b4..c2336aaf98 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -78,6 +78,42 @@ pub enum SessionEventType { SessionCompactionComplete, #[serde(rename = "session.task_complete")] SessionTaskComplete, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "session.fusion_route_started")] + SessionFusionRouteStarted, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "session.fusion_route_failed")] + SessionFusionRouteFailed, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "session.fusion_resolved")] + SessionFusionResolved, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "session.fusion_completed")] + SessionFusionCompleted, #[serde(rename = "user.message")] UserMessage, #[serde(rename = "pending_messages.modified")] @@ -90,6 +126,33 @@ pub enum SessionEventType { AgentInterrupted, #[serde(rename = "assistant.intent")] AssistantIntent, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "assistant.fusion_phase_started")] + AssistantFusionPhaseStarted, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "assistant.fusion_phase_completed")] + AssistantFusionPhaseCompleted, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "assistant.fusion_phase_failed")] + AssistantFusionPhaseFailed, #[serde(rename = "assistant.server_tool_progress")] AssistantServerToolProgress, #[serde(rename = "assistant.reasoning")] @@ -439,6 +502,42 @@ pub enum SessionEventData { SessionCompactionComplete(SessionCompactionCompleteData), #[serde(rename = "session.task_complete")] SessionTaskComplete(SessionTaskCompleteData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "session.fusion_route_started")] + SessionFusionRouteStarted(SessionFusionRouteStartedData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "session.fusion_route_failed")] + SessionFusionRouteFailed(SessionFusionRouteFailedData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "session.fusion_resolved")] + SessionFusionResolved(SessionFusionResolvedData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "session.fusion_completed")] + SessionFusionCompleted(SessionFusionCompletedData), #[serde(rename = "user.message")] UserMessage(UserMessageData), #[serde(rename = "pending_messages.modified")] @@ -451,6 +550,33 @@ pub enum SessionEventData { AgentInterrupted(AgentInterruptedData), #[serde(rename = "assistant.intent")] AssistantIntent(AssistantIntentData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "assistant.fusion_phase_started")] + AssistantFusionPhaseStarted(AssistantFusionPhaseStartedData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "assistant.fusion_phase_completed")] + AssistantFusionPhaseCompleted(AssistantFusionPhaseCompletedData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(rename = "assistant.fusion_phase_failed")] + AssistantFusionPhaseFailed(AssistantFusionPhaseFailedData), #[serde(rename = "assistant.server_tool_progress")] AssistantServerToolProgress(AssistantServerToolProgressData), #[serde(rename = "assistant.reasoning")] @@ -943,6 +1069,9 @@ pub struct SessionIdleData { /// True when the preceding agentic loop was cancelled via abort signal #[serde(skip_serializing_if = "Option::is_none")] pub aborted: Option, + /// The session mode the agent was operating in when it went idle, when the mode is known. Lets turn-scoped consumers distinguish an autopilot continuation boundary (where the agent keeps working after this idle) from a genuine turn completion. + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, } /// Session event "session.title_changed". Session title change payload containing the new display title @@ -1675,6 +1804,209 @@ pub struct SessionTaskCompleteData { pub summary: Option, } +/// Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFusionRouteStartedData { + /// Identifier for this routing attempt before a durable Fusion turn exists. + pub attempt_id: String, + /// HydraFusion routing policy requested for the turn. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy: Option, + /// Synthetic HydraFusion model selected for the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub synthetic_model: Option, + /// Kind of turn being routed. + pub turn_kind: FusionTurnKind, +} + +/// Session event "session.fusion_route_failed". Experimental durable HydraFusion routing failure and the deterministic concrete fallback selected for the turn. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFusionRouteFailedData { + /// Identifier of the routing attempt that failed. + pub attempt_id: String, + /// Provider or validation error detail, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub error_message: Option, + /// Concrete model selected as the deterministic fallback. + pub fallback_model: String, + /// HydraFusion routing policy requested for the turn. + pub policy: String, + /// Stable machine-readable reason for the routing failure. + pub reason: String, + /// Elapsed routing time in milliseconds before the failure. + #[serde(skip_serializing_if = "Option::is_none")] + pub routing_latency_ms: Option, + /// Synthetic HydraFusion model selected for the session. + pub synthetic_model: String, +} + +/// Durable server recommendation for subsequent HydraFusion turns. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FusionFollowUpRecommendation { + /// Recommended routing action for the next compaction turn. + pub compaction_turn: FusionFollowUpAction, + /// Recommended routing action for the next user-message turn. + pub user_turn: FusionFollowUpAction, +} + +/// Validated HydraFusion routing capability scores. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FusionScores { + /// Code-generation capability score returned by the authenticated router. + pub code_gen: f64, + /// Debugging capability score returned by the authenticated router. + pub debugging: f64, + /// Reasoning capability score returned by the authenticated router. + pub reasoning: f64, + /// Tool-use capability score returned by the authenticated router. + pub tool_use: f64, +} + +/// Session event "session.fusion_resolved". Experimental durable validated HydraFusion route and turn policy. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFusionResolvedData { + /// Version of the validated HydraFusion event contract. + pub contract_version: i64, + /// Concrete model used when the planned primary model cannot execute. + pub fallback_model: String, + /// Router recommendation controlling reuse or rerouting on later turns. + #[serde(skip_serializing_if = "Option::is_none")] + pub follow_up: Option, + /// Concrete model recommended for eligible follow-up turns. + pub follow_up_model: String, + /// Stable identifier for the resolved HydraFusion turn. + pub fusion_id: String, + /// Version of the executable model universe used for selection. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_universe_version: Option, + /// Validated orchestration pattern selected for the turn. + pub pattern: FusionPattern, + /// Version of the validated execution-plan format. + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_version: Option, + /// HydraFusion routing policy used to resolve the plan. + pub policy: String, + /// Version of the local routing policy. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_version: Option, + /// Concrete model selected for the primary solver phase. + pub primary_model: String, + /// Router implementation that supplied the plan. + #[serde(skip_serializing_if = "Option::is_none")] + pub route_source: Option, + /// Elapsed time in milliseconds required to resolve and validate the route. + #[serde(skip_serializing_if = "Option::is_none")] + pub routing_latency_ms: Option, + /// Identifier of the local policy rule that matched. + #[serde(skip_serializing_if = "Option::is_none")] + pub rule_id: Option, + /// Zero-based index of the local policy rule that matched. + #[serde(skip_serializing_if = "Option::is_none")] + pub rule_index: Option, + /// Human-readable name of the local policy rule that matched. + #[serde(skip_serializing_if = "Option::is_none")] + pub rule_name: Option, + /// Validated capability scores used to select the route. + #[serde(skip_serializing_if = "Option::is_none")] + pub scores: Option, + /// Concrete model selected for the review or judge phase, when required. + pub secondary_model: Option, + /// Synthetic HydraFusion model selected for the session. + pub synthetic_model: String, + /// Identifier of the session turn associated with the route. + pub turn_id: String, +} + +/// Session event "session.fusion_completed". Experimental durable aggregate outcome of a HydraFusion turn. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionFusionCompletedData { + /// Total cached input tokens reported across all phases. + pub cached_tokens: i64, + /// Total tokens written to prompt cache across all phases. + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_write_tokens: Option, + /// Idempotency identifier for the authoritative final commit. + pub commit_id: String, + /// Reason the turn used a degraded route, when applicable. + pub degraded_reason: Option, + /// Total elapsed execution time for the HydraFusion turn in milliseconds. + pub duration_ms: f64, + /// Concrete model that supplied the authoritative final content. + pub final_source_model: Option, + /// Phase whose output supplied the authoritative final content. + pub final_source_phase_id: Option, + /// Concrete model recommended for eligible follow-up turns. + pub follow_up_model: String, + /// Stable identifier for the completed HydraFusion turn. + pub fusion_id: String, + /// Total input tokens consumed across all phases. + pub input_tokens: i64, + /// Stable aggregate outcome of the HydraFusion turn. + pub outcome: String, + /// Total output tokens produced across all phases. + pub output_tokens: i64, + /// HydraFusion orchestration pattern executed for the turn. + pub pattern: FusionPattern, + /// Number of concrete phases attempted by the turn. + pub phase_count: i64, + /// Total concrete model requests made across all phases. + pub request_count: i64, + /// Synthetic HydraFusion model selected for the session. + pub synthetic_model: String, + /// Total normalized AI-unit cost reported across all phases, in nano-AIU. + pub total_nano_aiu: f64, + /// Identifier of the session turn associated with the completion. + pub turn_id: String, +} + /// Session event "user.message". Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1799,6 +2131,163 @@ pub struct AssistantIntentData { pub intent: String, } +/// Session event "assistant.fusion_phase_started". Experimental transient HydraFusion phase/model/role signal. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssistantFusionPhaseStartedData { + /// Conversation scope in which the phase executes. + pub conversation_scope: FusionConversationScope, + /// Identifier of the HydraFusion turn containing the phase. + pub fusion_id: String, + /// Concrete model executing the phase. + pub model: String, + /// HydraFusion orchestration pattern containing the phase. + pub pattern: FusionPattern, + /// Stable identifier for the concrete phase. + pub phase_id: String, + /// Kind of phase being executed. + pub phase_kind: FusionPhaseKind, + /// Semantic role assigned to the phase. + pub role: String, +} + +/// Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct FusionStagedTerminal { + pub arguments: String, + pub assistant_message: serde_json::Value, + pub phase_id: String, + pub tool_call_id: String, + pub tool_name: String, +} + +/// Aggregate concrete-model usage for one HydraFusion phase. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FusionPhaseUsage { + /// Total cached input tokens reported for the phase. + pub cached_tokens: i64, + /// Total tokens written to prompt cache during the phase. + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_write_tokens: Option, + /// Total input tokens consumed by the phase. + pub input_tokens: i64, + /// Total output tokens produced by the phase. + pub output_tokens: i64, + /// Number of concrete model requests made by the phase. + pub request_count: i64, + /// Total normalized AI-unit cost reported for the phase, in nano-AIU. + pub total_nano_aiu: f64, +} + +/// Session event "assistant.fusion_phase_completed". Experimental durable HydraFusion phase output and lossless replay checkpoint. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssistantFusionPhaseCompletedData { + /// Provider-normalized textual output produced by the phase. + pub content: String, + /// Conversation scope in which the phase executed. + pub conversation_scope: FusionConversationScope, + /// Elapsed execution time for the phase in milliseconds. + pub duration_ms: f64, + /// Identifier of the HydraFusion turn containing the phase. + pub fusion_id: String, + /// Concrete model that executed the phase. + pub model: String, + /// Stable identifier for the completed phase. + pub phase_id: String, + /// Kind of phase that completed. + pub phase_kind: FusionPhaseKind, + /// Exact provider-normalized message used to reconstruct canonical model history. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) projection_message: Option, + /// Projection action for the exact internal message. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) projection_mode: Option, + /// Semantic role assigned to the completed phase. + pub role: String, + /// Terminal request held outside canonical state until selected by the final commit. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) staged_terminal: Option, + /// Durable outcome status of the phase. + pub status: FusionPhaseStatus, + /// Aggregate concrete-model usage consumed by the phase. + pub usage: FusionPhaseUsage, + /// Structured judge or critic verdict, when the phase produces one. + pub verdict: Option, +} + +/// Session event "assistant.fusion_phase_failed". Experimental durable typed HydraFusion phase failure and degradation transition. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssistantFusionPhaseFailedData { + /// Conversation scope in which the phase executed. + pub conversation_scope: FusionConversationScope, + /// Identifier of the fallback phase used to continue the turn after degradation. + #[serde(skip_serializing_if = "Option::is_none")] + pub degraded_to_phase_id: Option, + /// Elapsed execution time before the phase failed, in milliseconds. + pub duration_ms: f64, + /// Provider or execution error detail, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub error_message: Option, + /// Identifier of the HydraFusion turn containing the phase. + pub fusion_id: String, + /// Concrete model that attempted the phase. + pub model: String, + /// Stable identifier for the failed phase. + pub phase_id: String, + /// Kind of phase that failed. + pub phase_kind: FusionPhaseKind, + /// Stable machine-readable reason for the phase failure. + pub reason: String, + /// Semantic role assigned to the failed phase. + pub role: String, + /// Durable outcome status of the phase. + pub status: FusionPhaseStatus, + /// Aggregate concrete-model usage consumed before the failure. + pub usage: FusionPhaseUsage, +} + /// Session event "assistant.server_tool_progress". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1944,6 +2433,48 @@ pub struct Citations { pub spans: Vec, } +/// Experimental attribution linking an ordinary event to the HydraFusion turn, phase, and concrete source that produced it. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FusionAttribution { + /// Idempotency identifier for the authoritative commit, when the event belongs to the selected output. + #[serde(skip_serializing_if = "Option::is_none")] + pub commit_id: Option, + /// Conversation scope in which the concrete phase executed. + #[serde(skip_serializing_if = "Option::is_none")] + pub conversation_scope: Option, + /// Stable identifier for the HydraFusion turn that produced the event. + pub fusion_id: String, + /// HydraFusion orchestration pattern selected for the turn. + pub pattern: String, + /// Identifier of the concrete phase that produced the event. + #[serde(skip_serializing_if = "Option::is_none")] + pub phase_id: Option, + /// Kind of concrete phase that produced the event. + #[serde(skip_serializing_if = "Option::is_none")] + pub phase_kind: Option, + /// HydraFusion routing policy used for the turn. + pub policy: String, + /// Semantic role assigned to the concrete phase. + #[serde(skip_serializing_if = "Option::is_none")] + pub role: Option, + /// Concrete model that produced the attributed event. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_model: Option, + /// Phase whose output supplied the authoritative content, when different from the executing phase. + #[serde(skip_serializing_if = "Option::is_none")] + pub source_phase_id: Option, + /// Synthetic HydraFusion model selected for the session. + pub synthetic_model: String, +} + /// Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping /// ///
@@ -2048,6 +2579,16 @@ pub struct AssistantMessageData { /// Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. #[serde(skip_serializing_if = "Option::is_none")] pub encrypted_content: Option, + /// Experimental HydraFusion source attribution for this ordinary authoritative assistant message. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub fusion: Option, /// CAPI interaction ID for correlating this message with upstream telemetry #[serde(skip_serializing_if = "Option::is_none")] pub interaction_id: Option, @@ -2274,6 +2815,16 @@ pub struct AssistantUsageData { #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) frontier_source: Option, + /// Experimental HydraFusion attribution for this concrete model call's usage. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub fusion: Option, /// What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls #[serde(skip_serializing_if = "Option::is_none")] pub initiator: Option, @@ -2491,6 +3042,16 @@ pub struct ModelCallFailureData { /// Whether the failure originated from an API response or the request transport #[serde(skip_serializing_if = "Option::is_none")] pub failure_kind: Option, + /// Experimental HydraFusion attribution for this failed concrete model call. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub fusion: Option, /// What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls #[serde(skip_serializing_if = "Option::is_none")] pub initiator: Option, @@ -2565,6 +3126,16 @@ pub struct ModelCallFinishedData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModelCallStartData { + /// Experimental HydraFusion attribution for this concrete model call. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub fusion: Option, /// Model identifier used for this API call, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, @@ -2662,6 +3233,16 @@ pub struct ToolExecutionStartData { /// When true, the tool output should be displayed expanded (verbatim) in the CLI timeline #[serde(skip_serializing_if = "Option::is_none")] pub display_verbatim: Option, + /// Experimental HydraFusion attribution for this tool execution. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub fusion: Option, /// Name of the MCP server hosting this tool, when the tool is an MCP tool #[serde(skip_serializing_if = "Option::is_none")] pub mcp_server_name: Option, @@ -2860,6 +3441,9 @@ pub struct ToolExecutionCompleteContentShellExit { pub cwd: Option, /// Exit code from the completed shell command pub exit_code: i64, + /// Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. + #[serde(skip_serializing_if = "Option::is_none")] + pub output_file_path: Option, /// Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output. #[serde(skip_serializing_if = "Option::is_none")] pub output_preview: Option, @@ -3170,6 +3754,16 @@ pub struct ToolExecutionCompleteData { /// Error details when the tool execution failed #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, + /// Experimental HydraFusion attribution for this tool completion. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub fusion: Option, /// CAPI interaction ID for correlating this tool execution with upstream telemetry #[serde(skip_serializing_if = "Option::is_none")] pub interaction_id: Option, @@ -5752,6 +6346,24 @@ pub enum Verbosity { Unknown, } +/// The session mode the agent is operating in +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SessionMode { + /// The agent is responding interactively to the user. + #[serde(rename = "interactive")] + Interactive, + /// The agent is preparing a plan before making changes. + #[serde(rename = "plan")] + Plan, + /// The agent is working autonomously toward task completion. + #[serde(rename = "autopilot")] + Autopilot, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Who created the schedule: `user` (an explicit user action such as `/every` or `/after`) or `model` (the agent via the `manage_schedule` tool). Gates whether a scheduled skill that opted out of model invocation may fire: only user-created schedules may. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ScheduleOrigin { @@ -5848,24 +6460,6 @@ pub enum ModelChangeSource { Unknown, } -/// The session mode the agent is operating in -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum SessionMode { - /// The agent is responding interactively to the user. - #[serde(rename = "interactive")] - Interactive, - /// The agent is preparing a plan before making changes. - #[serde(rename = "plan")] - Plan, - /// The agent is working autonomously toward task completion. - #[serde(rename = "autopilot")] - Autopilot, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Permission mode for the session. /// ///
@@ -5996,6 +6590,75 @@ pub enum TaskCompletionOutcome { Unknown, } +/// Kind of turn for which HydraFusion routing is running. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionTurnKind { + /// A user-message turn. + #[serde(rename = "user")] + User, + /// A conversation-compaction turn. + #[serde(rename = "compaction")] + Compaction, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Server-recommended routing behavior for a later HydraFusion turn. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionFollowUpAction { + /// Reuse the durable primary model without routing. + #[serde(rename = "reuse_primary")] + ReusePrimary, + /// Request a new routing decision. + #[serde(rename = "reroute")] + Reroute, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Validated HydraFusion execution pattern. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionPattern { + /// Run one primary solver phase. + #[serde(rename = "single")] + Single, + /// Run a primary phase, a judge, and an optional repair. + #[serde(rename = "cascade")] + Cascade, + /// Run a primary draft, a read-only critique, and a revision. + #[serde(rename = "critique")] + Critique, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The agent mode that was active when this message was sent #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum UserMessageAgentMode { @@ -6086,6 +6749,115 @@ pub enum ModelCallFailureTransport { Unknown, } +/// Conversation scope in which a HydraFusion phase executes. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionConversationScope { + /// Canonical root conversation history. + #[serde(rename = "root")] + Root, + /// Isolated read-only review history that does not enter the root conversation. + #[serde(rename = "review")] + Review, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// HydraFusion phase kind. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionPhaseKind { + /// Primary solver phase. + #[serde(rename = "primary")] + Primary, + /// Read-only cascade judge phase. + #[serde(rename = "judge")] + Judge, + /// Cascade repair phase. + #[serde(rename = "repair")] + Repair, + /// Initial critique-pattern draft phase. + #[serde(rename = "draft")] + Draft, + /// Read-only critique phase. + #[serde(rename = "critic")] + Critic, + /// Critique-pattern revision phase. + #[serde(rename = "revision")] + Revision, + /// Follow-up phase continuing from the resolved model. + #[serde(rename = "follow_up")] + FollowUp, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// How a durable phase checkpoint contributes its exact message to canonical root history. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionProjectionMode { + /// Append the exact root message immediately. + #[serde(rename = "append")] + Append, + /// Hold a terminal message outside canonical history until the final commit selects it. + #[serde(rename = "staged")] + Staged, + /// Do not project the checkpoint into root history. + #[serde(rename = "none")] + None, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Durable outcome status of a HydraFusion phase. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionPhaseStatus { + /// The phase completed successfully. + #[serde(rename = "succeeded")] + Succeeded, + /// The phase failed. + #[serde(rename = "failed")] + Failed, + /// The phase was cancelled. + #[serde(rename = "cancelled")] + Cancelled, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AssistantMessageToolRequestType { diff --git a/rust/src/handler.rs b/rust/src/handler.rs index f1f0d9566d..e036b75a10 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -114,6 +114,7 @@ impl PermissionResult { /// /// let result = PermissionResult::approve_once().with_context(PermissionDecisionContext { /// outcome: PermissionDecisionOutcome::AutoApproved, + /// response_capability: None, /// source: PermissionDecisionSource::HostPolicy, /// surface: PermissionDecisionSurface::Sdk, /// }); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index bf50adfd48..e61028e2ac 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -52,6 +52,7 @@ pub mod trace_context; pub mod transforms; /// Protocol types shared between the SDK and the GitHub Copilot CLI. pub mod types; +mod watch; mod wire; /// Session event payload types — auto-generated from the protocol schema. @@ -71,6 +72,7 @@ pub(crate) mod generated; /// source-qualified tool filter patterns. pub mod mode; +use std::collections::HashSet; use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Stdio; @@ -89,6 +91,7 @@ pub(crate) use jsonrpc::{ }; pub use mode::{BUILTIN_TOOLS_ISOLATED, ClientMode, ToolSet}; pub use provider_token::{BearerTokenError, BearerTokenProvider, ProviderTokenArgs}; +pub use watch::{SharedSessionWatch, SharedSessionWatchEvents}; /// Re-exported JSON-RPC internals for integration tests (requires `test-support` feature). #[cfg(feature = "test-support")] @@ -1020,6 +1023,7 @@ struct ClientInner { request_rx: parking_lot::Mutex>>, notification_tx: broadcast::Sender, router: router::SessionRouter, + watch_sessions: Arc>>, negotiated_protocol_version: OnceLock, state: parking_lot::Mutex, lifecycle_tx: broadcast::Sender, @@ -1613,6 +1617,7 @@ impl Client { request_rx: parking_lot::Mutex::new(Some(request_rx)), notification_tx: notification_broadcast_tx, router: router::SessionRouter::new(), + watch_sessions: Arc::new(parking_lot::Mutex::new(HashSet::new())), negotiated_protocol_version: OnceLock::new(), state: parking_lot::Mutex::new(ConnectionState::Connected), lifecycle_tx: broadcast::channel(256).0, @@ -1643,6 +1648,8 @@ impl Client { fn spawn_lifecycle_dispatcher(&self) { let mut notif_rx = self.inner.notification_tx.subscribe(); let lifecycle_tx = self.inner.lifecycle_tx.clone(); + let router = self.inner.router.clone(); + let watch_sessions = self.inner.watch_sessions.clone(); tokio::spawn(async move { loop { match notif_rx.recv().await { @@ -1666,7 +1673,12 @@ impl Client { }; // `send` only errors when there are no subscribers — that's // the normal case before any consumer calls subscribe_lifecycle. - let _ = lifecycle_tx.send(event); + let _ = lifecycle_tx.send(event.clone()); + if event.event_type == SessionLifecycleEventType::Disconnected + && watch_sessions.lock().remove(&event.session_id) + { + router.unregister(&event.session_id); + } } Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { warn!(missed = n, "lifecycle dispatcher lagged"); @@ -2055,6 +2067,15 @@ impl Client { self.inner.router.unregister(session_id); } + pub(crate) fn register_watch_session(&self, session_id: &SessionId) { + self.inner.watch_sessions.lock().insert(session_id.clone()); + } + + pub(crate) fn unregister_watch_session(&self, session_id: &SessionId) { + self.inner.watch_sessions.lock().remove(session_id); + self.inner.router.unregister(session_id); + } + /// Returns the protocol version negotiated with the CLI server, if any. /// /// Set during [`start`](Self::start). Returns `None` if the server didn't @@ -2274,11 +2295,13 @@ impl Client { let mut first_error = None; for session_id in self.inner.router.session_ids() { + let method = if self.inner.watch_sessions.lock().remove(&session_id) { + generated::api_types::rpc_methods::SESSIONS_CLOSE + } else { + "session.destroy" + }; if let Err(error) = self - .call( - "session.destroy", - Some(serde_json::json!({ "sessionId": session_id })), - ) + .call(method, Some(serde_json::json!({ "sessionId": session_id }))) .await && first_error.is_none() { @@ -2436,11 +2459,13 @@ impl Client { // Snapshot the registered session IDs without holding the router // lock across the destroy RPCs. for session_id in self.inner.router.session_ids() { + let method = if self.inner.watch_sessions.lock().remove(&session_id) { + generated::api_types::rpc_methods::SESSIONS_CLOSE + } else { + "session.destroy" + }; match self - .call( - "session.destroy", - Some(serde_json::json!({ "sessionId": session_id })), - ) + .call(method, Some(serde_json::json!({ "sessionId": session_id }))) .await { Ok(_) => {} @@ -2448,7 +2473,8 @@ impl Client { warn!( session_id = %session_id, error = %e, - "session.destroy failed during Client::stop", + method, + "session cleanup failed during Client::stop", ); errors.push(e); } @@ -2581,6 +2607,7 @@ impl Client { // Drop all session channels so any awaiters see a closed channel // instead of waiting for responses that will never arrive. self.inner.router.clear(); + self.inner.watch_sessions.lock().clear(); *self.inner.state.lock() = ConnectionState::Disconnected; *self.inner.models_cache.lock() = Arc::new(tokio::sync::OnceCell::new()); } @@ -3388,6 +3415,7 @@ mod tests { request_rx: parking_lot::Mutex::new(None), notification_tx: broadcast::channel(16).0, router: router::SessionRouter::new(), + watch_sessions: Arc::new(parking_lot::Mutex::new(HashSet::new())), negotiated_protocol_version: OnceLock::new(), state: parking_lot::Mutex::new(ConnectionState::Connected), lifecycle_tx: broadcast::channel(16).0, diff --git a/rust/src/router.rs b/rust/src/router.rs index adc1923824..be104bd7c1 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -24,16 +24,17 @@ struct SessionSenders { /// Routes notifications and requests by sessionId to per-session channels. /// /// Internal to the SDK — consumers interact via `Client::register_session()`. +#[derive(Clone)] pub(crate) struct SessionRouter { sessions: Arc>>, - started: Mutex, + started: Arc>, } impl SessionRouter { pub(crate) fn new() -> Self { Self { sessions: Arc::new(Mutex::new(HashMap::new())), - started: Mutex::new(false), + started: Arc::new(Mutex::new(false)), } } diff --git a/rust/src/session.rs b/rust/src/session.rs index 3e5ae13dee..ebf588973a 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -2718,6 +2718,7 @@ mod tests { fn attribution_context() -> PermissionDecisionContext { PermissionDecisionContext { outcome: PermissionDecisionOutcome::AutoApproved, + response_capability: None, source: PermissionDecisionSource::AssistedApproval, surface: PermissionDecisionSurface::CopilotApp, } @@ -2806,6 +2807,7 @@ mod tests { .with_context(attribution_context()) .with_context(PermissionDecisionContext { outcome: PermissionDecisionOutcome::PromptedUser, + response_capability: None, source: PermissionDecisionSource::HumanResponse, surface: PermissionDecisionSurface::Sdk, }); diff --git a/rust/src/types.rs b/rust/src/types.rs index 2db631db3c..1c17ae0dc0 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -77,6 +77,9 @@ pub enum SessionLifecycleEventType { /// A session moved into the background. #[serde(rename = "session.background")] Background, + /// A session connection was terminally lost. + #[serde(rename = "session.disconnected")] + Disconnected, } /// Optional metadata attached to a [`SessionLifecycleEvent`]. @@ -104,6 +107,7 @@ pub struct SessionLifecycleEvent { #[serde(rename = "sessionId")] pub session_id: SessionId, /// Optional metadata describing the session at the time of the event. + /// Absent for deleted and disconnected events. #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, } diff --git a/rust/src/watch.rs b/rust/src/watch.rs new file mode 100644 index 0000000000..cf759f53d3 --- /dev/null +++ b/rust/src/watch.rs @@ -0,0 +1,194 @@ +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use parking_lot::Mutex as ParkingLotMutex; +use tokio::sync::mpsc; +use tokio_stream::Stream; + +use crate::generated::api_types::{ + ConnectedRemoteSessionMetadata, SessionsCloseRequest, WatchSharedSessionParams, + WatchSharedSessionResult, rpc_methods, +}; +use crate::router::SessionChannels; +use crate::types::{SessionEvent, SessionEventNotification, SessionId}; +use crate::{Client, Error, ErrorKind}; + +/// Passive, read-only attachment to a session shared with the authenticated user. +/// +/// History and live updates are available through [`events`](Self::events). +/// Interactive session operations are intentionally absent from this type. +/// Call [`close`](Self::close) before dropping the handle; client shutdown also +/// closes any watch that remains registered. +pub struct SharedSessionWatch { + session_id: SessionId, + metadata: ConnectedRemoteSessionMetadata, + client: Client, + events: SharedSessionWatchEvents, + closed: tokio::sync::Mutex, +} + +impl SharedSessionWatch { + pub(crate) fn new( + client: Client, + session_id: SessionId, + metadata: ConnectedRemoteSessionMetadata, + channels: SessionChannels, + ) -> Self { + let SessionChannels { + notifications, + requests: _, + } = channels; + Self { + session_id, + metadata, + client, + events: SharedSessionWatchEvents { notifications }, + closed: tokio::sync::Mutex::new(false), + } + } + + /// SDK session ID assigned to this watch. + pub fn session_id(&self) -> &SessionId { + &self.session_id + } + + /// Metadata for the watched shared session. + pub fn metadata(&self) -> &ConnectedRemoteSessionMetadata { + &self.metadata + } + + /// Whether this attachment is read-only. + pub const fn is_read_only(&self) -> bool { + true + } + + /// Ordered canonical history and live events for the watched session. + /// + /// The receiver is registered before the watch response is delivered, so + /// replay events remain buffered until the caller begins consuming them. + pub fn events(&mut self) -> &mut SharedSessionWatchEvents { + &mut self.events + } + + /// Close the watch and release local event routing. + /// + /// Repeated calls are idempotent. + pub async fn close(&self) -> Result<(), Error> { + let mut closed = self.closed.lock().await; + if *closed { + return Ok(()); + } + + let result = self + .client + .rpc() + .sessions() + .close(SessionsCloseRequest { + session_id: self.session_id.clone(), + }) + .await + .map(|_| ()); + self.client.unregister_watch_session(&self.session_id); + *closed = true; + result + } +} + +/// Event stream retained by a [`SharedSessionWatch`]. +pub struct SharedSessionWatchEvents { + notifications: mpsc::UnboundedReceiver, +} + +impl SharedSessionWatchEvents { + /// Receive the next canonical session event. + /// + /// Returns `None` after the watch is closed or the client disconnects. + pub async fn recv(&mut self) -> Option { + self.notifications + .recv() + .await + .map(|notification| notification.event) + } +} + +impl Stream for SharedSessionWatchEvents { + type Item = SessionEvent; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.notifications) + .poll_recv(cx) + .map(|notification| notification.map(|notification| notification.event)) + } +} + +impl Client { + /// Watch a session shared with the authenticated user. + /// + /// The runtime derives viewer identity, authorization and lane routing from + /// the existing authenticated connection. The returned handle exposes no + /// send, steer, prompt, permission, configuration or cancellation methods. + /// Subscribe with [`Client::subscribe_lifecycle`] before calling this method + /// if terminal connection loss must not be missed. + pub async fn watch_shared_session( + &self, + session_id: impl Into, + ) -> Result { + let params = WatchSharedSessionParams { + session_id: session_id.into(), + }; + let wire_params = serde_json::to_value(params)?; + let registration = Arc::new(ParkingLotMutex::new(None)); + let registration_for_callback = registration.clone(); + let client = self.clone(); + + let value = self + .call_with_inline_callback( + rpc_methods::SESSIONS_WATCH, + Some(wire_params), + Some(Box::new(move |response| { + let value = response.result.as_ref().ok_or_else(|| { + Error::with_message( + ErrorKind::Rpc { code: -32603 }, + "sessions.watch response did not include a result", + ) + })?; + let result: WatchSharedSessionResult = serde_json::from_value(value.clone())?; + let channels = client.register_session(&result.session_id); + client.register_watch_session(&result.session_id); + *registration_for_callback.lock() = Some(channels); + Ok(()) + })), + ) + .await?; + let result: WatchSharedSessionResult = serde_json::from_value(value)?; + + if !result.read_only { + let _ = self + .rpc() + .sessions() + .close(SessionsCloseRequest { + session_id: result.session_id.clone(), + }) + .await; + self.unregister_watch_session(&result.session_id); + return Err(Error::with_message( + ErrorKind::Rpc { code: -32603 }, + "runtime returned an interactive shared-session watch", + )); + } + + let channels = registration.lock().take().ok_or_else(|| { + Error::with_message( + ErrorKind::Rpc { code: -32603 }, + "sessions.watch response was not registered for event routing", + ) + })?; + Ok(SharedSessionWatch::new( + self.clone(), + result.session_id, + result.metadata, + channels, + )) + } +} diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index 9b86b1367a..d204f8ca88 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -6,6 +6,7 @@ use github_copilot_sdk::rpc::{ Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest, + WatchSharedSessionParams, WatchSharedSessionResult, }; use github_copilot_sdk::session_events::{PermissionRequest, PermissionRequestedData}; @@ -104,6 +105,50 @@ fn permission_event_exposes_managed_approval_required() { assert_eq!(request.managed_approval_required, Some(true)); } +#[test] +fn shared_session_watch_payloads_are_generated_without_credentials() { + let params = WatchSharedSessionParams { + session_id: "shared-session".into(), + }; + assert_eq!( + serde_json::to_value(params).unwrap(), + serde_json::json!({ "sessionId": "shared-session" }) + ); + + let result: WatchSharedSessionResult = serde_json::from_value(serde_json::json!({ + "sessionId": "watch-session", + "readOnly": true, + "metadata": { + "sessionId": "watch-session", + "startTime": "2025-01-01T00:00:00Z", + "modifiedTime": "2025-01-01T00:01:00Z", + "repository": { + "owner": "github", + "name": "copilot-sdk", + "branch": "main" + }, + "kind": "remote-session" + } + })) + .unwrap(); + let serialized = serde_json::to_value(result).unwrap(); + + assert_eq!(serialized["readOnly"], true); + assert_eq!(serialized["sessionId"], "watch-session"); + let debug = serialized.to_string().to_ascii_lowercase(); + for forbidden in [ + "viewerid", + "baseurl", + "wps", + "lane", + "channel", + "credential", + "token", + ] { + assert!(!debug.contains(forbidden), "unexpected field: {forbidden}"); + } +} + fn running_extension(id: &str, name: &str) -> Extension { Extension { id: id.to_string(), diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index e20d9d0885..a495fadbc2 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -51,6 +51,7 @@ impl PermissionHandler for ContextualApproveHandler { ) -> PermissionResult { PermissionResult::approve_once().with_context(PermissionDecisionContext { outcome: PermissionDecisionOutcome::PromptedUser, + response_capability: None, source: PermissionDecisionSource::HumanResponse, surface: PermissionDecisionSurface::CopilotApp, }) @@ -254,6 +255,97 @@ where (session, server) } +#[tokio::test] +async fn shared_session_watch_retains_replay_and_closes_once() { + let (client, server_read, server_write) = make_client(); + let mut lifecycle = client.subscribe_lifecycle(); + let mut server = FakeServer { + read: server_read, + write: server_write, + session_id: "watch-session".to_string(), + }; + + let watch_handle = tokio::spawn({ + let client = client.clone(); + async move { client.watch_shared_session("shared-session").await.unwrap() } + }); + let watch_request = server.read_request().await; + assert_eq!(watch_request["method"], "sessions.watch"); + assert_eq!( + watch_request["params"], + serde_json::json!({ "sessionId": "shared-session" }) + ); + server + .respond( + &watch_request, + serde_json::json!({ + "sessionId": "watch-session", + "readOnly": true, + "metadata": { + "sessionId": "watch-session", + "startTime": "2025-01-01T00:00:00Z", + "modifiedTime": "2025-01-01T00:01:00Z", + "repository": { + "owner": "github", + "name": "copilot-sdk", + "branch": "main" + }, + "kind": "remote-session" + } + }), + ) + .await; + server + .send_event( + "assistant.message", + serde_json::json!({ "content": "history" }), + ) + .await; + server + .send_notification( + "session.lifecycle", + serde_json::json!({ + "type": "session.disconnected", + "sessionId": "watch-session" + }), + ) + .await; + + let mut watch = timeout(TIMEOUT, watch_handle).await.unwrap().unwrap(); + assert_eq!(watch.session_id(), "watch-session"); + assert!(watch.is_read_only()); + assert_eq!(watch.metadata().session_id, "watch-session"); + + let replay = timeout(TIMEOUT, watch.events().recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(replay.event_type, "assistant.message"); + let terminal = timeout(TIMEOUT, lifecycle.recv()).await.unwrap().unwrap(); + assert_eq!( + terminal.event_type, + github_copilot_sdk::SessionLifecycleEventType::Disconnected + ); + assert_eq!(terminal.session_id, "watch-session"); + assert!(terminal.metadata.is_none()); + assert!(watch.events().recv().await.is_none()); + + let close_handle = tokio::spawn({ + async move { + watch.close().await.unwrap(); + watch.close().await.unwrap(); + } + }); + let close_request = server.read_request().await; + assert_eq!(close_request["method"], "sessions.close"); + assert_eq!( + close_request["params"], + serde_json::json!({ "sessionId": "watch-session" }) + ); + server.respond(&close_request, serde_json::json!({})).await; + timeout(TIMEOUT, close_handle).await.unwrap().unwrap(); +} + fn rand_id() -> u64 { static COUNTER: AtomicUsize = AtomicUsize::new(0); COUNTER.fetch_add(1, Ordering::Relaxed) as u64 From ab59d44923f52ccd63807c7709a8629f13e4d8b2 Mon Sep 17 00:00:00 2001 From: Touseef Liaqat Date: Wed, 26 Aug 2026 12:50:06 -0700 Subject: [PATCH 2/5] Pin shared watch lifecycle variants Guard the hand-authored Node and Rust lifecycle unions against silent drift from the Runtime notification contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- nodejs/test/client.test.ts | 21 +++++++++++++++++++++ rust/src/types.rs | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 5b6d852500..28907ed693 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -15,6 +15,7 @@ import { type GitHubTelemetryNotification, type ManagedSettings, type ModelInfo, + type SessionLifecycleEventType, } from "../src/index.js"; import { CopilotSession } from "../src/session.js"; import type { WatchSharedSessionParams, WatchSharedSessionResult } from "../src/generated/rpc.js"; @@ -1314,6 +1315,26 @@ describe("CopilotClient", () => { ); }); + it("pins the complete runtime lifecycle event type set", () => { + const eventTypes = { + "session.created": true, + "session.deleted": true, + "session.updated": true, + "session.foreground": true, + "session.background": true, + "session.disconnected": true, + } satisfies Record; + + expect(Object.keys(eventTypes)).toEqual([ + "session.created", + "session.deleted", + "session.updated", + "session.foreground", + "session.background", + "session.disconnected", + ]); + }); + it("registers no gitHubTelemetry handler when onGitHubTelemetry is omitted", () => { const client = new CopilotClient(); onTestFinished(() => stopClient(client)); diff --git a/rust/src/types.rs b/rust/src/types.rs index 1c17ae0dc0..469e2deb02 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -5917,8 +5917,8 @@ mod tests { InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, - SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, - ToolResultResponse, ensure_attachment_display_names, + SessionLifecycleEventType, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, + ToolResultExpanded, ToolResultResponse, ensure_attachment_display_names, }; use crate::generated::session_events::TypedSessionEvent; @@ -7308,6 +7308,38 @@ mod tests { let _ = ConnectionState::Error; } + #[test] + fn session_lifecycle_event_types_match_runtime_contract() { + let event_types = [ + SessionLifecycleEventType::Created, + SessionLifecycleEventType::Deleted, + SessionLifecycleEventType::Updated, + SessionLifecycleEventType::Foreground, + SessionLifecycleEventType::Background, + SessionLifecycleEventType::Disconnected, + ]; + let wire_names = event_types.map(|event_type| match event_type { + SessionLifecycleEventType::Created => "session.created", + SessionLifecycleEventType::Deleted => "session.deleted", + SessionLifecycleEventType::Updated => "session.updated", + SessionLifecycleEventType::Foreground => "session.foreground", + SessionLifecycleEventType::Background => "session.background", + SessionLifecycleEventType::Disconnected => "session.disconnected", + }); + + assert_eq!( + wire_names, + [ + "session.created", + "session.deleted", + "session.updated", + "session.foreground", + "session.background", + "session.disconnected", + ] + ); + } + #[test] fn deserializes_runtime_attachment_variants() { let attachments: Vec = serde_json::from_value(json!([ From 7093118d811059c5e6cb540c682f8e4d358d32fb Mon Sep 17 00:00:00 2001 From: Touseef Liaqat Date: Wed, 26 Aug 2026 12:56:21 -0700 Subject: [PATCH 3/5] Keep shared watch generation pin-compatible Regenerate the watch RPC from the accepted contract while preserving the SDK's published CLI schema surface and removing unrelated newer Runtime event/type drift. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: be0a255d-04f8-4830-888a-59f54ad1e607 --- nodejs/src/generated/rpc.ts | 53 +- nodejs/src/generated/session-events.ts | 809 +------------------------ rust/src/generated/api_types.rs | 46 +- rust/src/generated/session_events.rs | 808 +----------------------- rust/src/handler.rs | 1 - rust/src/session.rs | 2 - rust/tests/session_test.rs | 1 - 7 files changed, 50 insertions(+), 1670 deletions(-) diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 1673288c3e..193ecf602f 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -2548,24 +2548,8 @@ export type PermissionDecisionSurface = | "prompt_mode" /** The Copilot App client. */ | "copilot_app" - /** An Agent Client Protocol host. */ - | "acp" /** A generic Copilot SDK client. */ | "sdk"; -/** - * Response capability available to the client when it settled a permission request. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "PermissionResponseCapability". - */ -/** @experimental */ -export type PermissionResponseCapability = - /** The client could ask a user for this decision. */ - | "interactive" - /** The client could return an automated response but could not ask a user. */ - | "headless" - /** The client had no response path available. */ - | "none"; /** * Tool approval to persist and apply * @@ -7268,10 +7252,6 @@ export interface ExternalToolTextResultForLlmContentShellExit { * Whether outputPreview is known to be incomplete or truncated */ outputTruncated?: boolean; - /** - * Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. - */ - outputFilePath?: string; } /** * Image content block with base64-encoded data @@ -8392,14 +8372,6 @@ export interface GitHubTelemetryClientInfo { * Stable machine identifier for the device. */ dev_device_id?: string; - /** - * Distinct CPU model names for the host, comma-separated. - */ - cpu_model?: string; - /** - * Number of logical CPU cores on the host. - */ - cpu_count?: number; } /** * A single telemetry event in the runtime's native GitHub-shaped telemetry format, forwarded verbatim to opted-in hosts. The `restricted` flag on the enclosing GitHubTelemetryNotification distinguishes standard from restricted events; the payload shape is identical for both. @@ -13526,7 +13498,6 @@ export interface PermissionDecisionContext { outcome: PermissionDecisionOutcome; source: PermissionDecisionSource; surface: PermissionDecisionSurface; - responseCapability?: PermissionResponseCapability; } /** * Pending permission request ID and the decision to apply (approve/reject and scope). @@ -18160,6 +18131,10 @@ export interface SessionOpenOptions { * Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the same name remain available. */ includedBuiltinAgents?: string[]; + /** + * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. + */ + includedBuiltinSkills?: string[]; /** * Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. */ @@ -18202,10 +18177,6 @@ export interface SessionOpenOptions { * Additional directories to search for skills. */ skillDirectories?: string[]; - /** - * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. - */ - includedBuiltinSkills?: string[]; /** * Skill IDs disabled for this session. */ @@ -19581,6 +19552,10 @@ export interface SessionUpdateOptionsParams { * Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the same name remain available. Set to null to remove the allowlist restriction. */ includedBuiltinAgents?: string[] | null; + /** + * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. Set to null to remove the allowlist restriction. + */ + includedBuiltinSkills?: string[] | null; /** * Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. */ @@ -19620,10 +19595,6 @@ export interface SessionUpdateOptionsParams { * Additional directories to search for skills. */ skillDirectories?: string[]; - /** - * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. Set to null to remove the allowlist restriction. - */ - includedBuiltinSkills?: string[] | null; /** * Skill IDs that should be excluded from this session. */ @@ -21009,6 +20980,10 @@ export interface ToolsExecuteRequest { */ /** @experimental */ export interface ToolsGetBuiltinDescriptorsRequest { + /** + * Whether line numbers should be omitted from the view tool descriptor. + */ + noViewLineNumbers?: boolean; /** * Whether descriptors should favor fewer user-intervention prompts. */ @@ -21022,6 +20997,10 @@ export interface ToolsGetBuiltinDescriptorsRequest { */ skillEmbeddingEnabled?: boolean; shellConfig?: ToolsShellDescriptorConfig; + /** + * Whether shell commands may only run asynchronously. + */ + shellAsyncOnlyEnabled?: boolean; /** * Whether the configured shell supports PowerShell 7 syntax. */ diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 2942405c45..3ec55aacda 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -40,17 +40,10 @@ export type SessionEvent = | CompactionStartEvent | CompactionCompleteEvent | TaskCompleteEvent - | FusionRouteStartedEvent - | FusionRouteFailedEvent - | FusionResolvedEvent - | FusionCompletedEvent | UserMessageEvent | PendingMessagesModifiedEvent | AssistantTurnStartEvent | AssistantIntentEvent - | AssistantFusionPhaseStartedEvent - | AssistantFusionPhaseCompletedEvent - | AssistantFusionPhaseFailedEvent | AssistantServerToolProgressEvent | AssistantReasoningEvent | AssistantReasoningDeltaEvent @@ -170,16 +163,6 @@ export type Verbosity = | "medium" /** A more detailed response was requested. */ | "high"; -/** - * The session mode the agent is operating in - */ -export type SessionMode = - /** The agent is responding interactively to the user. */ - | "interactive" - /** The agent is preparing a plan before making changes. */ - | "plan" - /** The agent is working autonomously toward task completion. */ - | "autopilot"; /** * Who created the schedule: `user` (an explicit user action such as `/every` or `/after`) or `model` (the agent via the `manage_schedule` tool). Gates whether a scheduled skill that opted out of model invocation may fire: only user-created schedules may. */ @@ -236,6 +219,16 @@ export type ModelChangeSource = | "automatic" /** An SDK or RPC caller selected the model. */ | "sdk"; +/** + * The session mode the agent is operating in + */ +export type SessionMode = + /** The agent is responding interactively to the user. */ + | "interactive" + /** The agent is preparing a plan before making changes. */ + | "plan" + /** The agent is working autonomously toward task completion. */ + | "autopilot"; /** * Permission mode for the session. */ @@ -305,35 +298,6 @@ export type TaskCompletionOutcome = | "continue" /** Completion cannot proceed without intervention; the active objective is paused when one is identified. */ | "blocked"; -/** - * Kind of turn for which HydraFusion routing is running. - */ -/** @experimental */ -export type FusionTurnKind = - /** A user-message turn. */ - | "user" - /** A conversation-compaction turn. */ - | "compaction"; -/** - * Server-recommended routing behavior for a later HydraFusion turn. - */ -/** @experimental */ -export type FusionFollowUpAction = - /** Reuse the durable primary model without routing. */ - | "reuse_primary" - /** Request a new routing decision. */ - | "reroute"; -/** - * Validated HydraFusion execution pattern. - */ -/** @experimental */ -export type FusionPattern = - /** Run one primary solver phase. */ - | "single" - /** Run a primary phase, a judge, and an optional repair. */ - | "cascade" - /** Run a primary draft, a read-only critique, and a revision. */ - | "critique"; /** * The agent mode that was active when this message was sent */ @@ -393,57 +357,6 @@ export type UserMessageDelivery = | "steering" /** Enqueued while the agent was busy; processed as its own run afterward. */ | "queued"; -/** - * Conversation scope in which a HydraFusion phase executes. - */ -/** @experimental */ -export type FusionConversationScope = - /** Canonical root conversation history. */ - | "root" - /** Isolated read-only review history that does not enter the root conversation. */ - | "review"; -/** - * HydraFusion phase kind. - */ -/** @experimental */ -export type FusionPhaseKind = - /** Primary solver phase. */ - | "primary" - /** Read-only cascade judge phase. */ - | "judge" - /** Cascade repair phase. */ - | "repair" - /** Initial critique-pattern draft phase. */ - | "draft" - /** Read-only critique phase. */ - | "critic" - /** Critique-pattern revision phase. */ - | "revision" - /** Follow-up phase continuing from the resolved model. */ - | "follow_up"; -/** - * How a durable phase checkpoint contributes its exact message to canonical root history. - */ -/** @experimental */ -/** @internal */ -export type FusionProjectionMode = - /** Append the exact root message immediately. */ - | "append" - /** Hold a terminal message outside canonical history until the final commit selects it. */ - | "staged" - /** Do not project the checkpoint into root history. */ - | "none"; -/** - * Durable outcome status of a HydraFusion phase. - */ -/** @experimental */ -export type FusionPhaseStatus = - /** The phase completed successfully. */ - | "succeeded" - /** The phase failed. */ - | "failed" - /** The phase was cancelled. */ - | "cancelled"; /** * Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. */ @@ -1449,7 +1362,6 @@ export interface IdleData { * True when the preceding agentic loop was cancelled via abort signal */ aborted?: boolean; - mode?: SessionMode; } /** * Session event "session.title_changed". Session title change payload containing the new display title @@ -3004,365 +2916,6 @@ export interface TaskCompleteData { */ summary?: string; } -/** - * Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. - */ -/** @experimental */ -export interface FusionRouteStartedEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: FusionRouteStartedData; - /** - * Always true for events that are transient and not persisted to the session event log on disk. - */ - ephemeral: true; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "session.fusion_route_started". - */ - type: "session.fusion_route_started"; -} -/** - * Experimental transient signal that HydraFusion routing has started for an eligible turn. - */ -/** @experimental */ -export interface FusionRouteStartedData { - /** - * Identifier for this routing attempt before a durable Fusion turn exists. - */ - attemptId: string; - /** - * HydraFusion routing policy requested for the turn. - */ - policy?: string; - /** - * Synthetic HydraFusion model selected for the session. - */ - syntheticModel?: string; - turnKind: FusionTurnKind; -} -/** - * Session event "session.fusion_route_failed". Experimental durable HydraFusion routing failure and the deterministic concrete fallback selected for the turn. - */ -/** @experimental */ -export interface FusionRouteFailedEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: FusionRouteFailedData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "session.fusion_route_failed". - */ - type: "session.fusion_route_failed"; -} -/** - * Experimental durable HydraFusion routing failure and the deterministic concrete fallback selected for the turn. - */ -/** @experimental */ -export interface FusionRouteFailedData { - /** - * Identifier of the routing attempt that failed. - */ - attemptId: string; - /** - * Provider or validation error detail, when available. - */ - errorMessage?: string; - /** - * Concrete model selected as the deterministic fallback. - */ - fallbackModel: string; - /** - * HydraFusion routing policy requested for the turn. - */ - policy: string; - /** - * Stable machine-readable reason for the routing failure. - */ - reason: string; - /** - * Elapsed routing time in milliseconds before the failure. - */ - routingLatencyMs?: number; - /** - * Synthetic HydraFusion model selected for the session. - */ - syntheticModel: string; -} -/** - * Session event "session.fusion_resolved". Experimental durable validated HydraFusion route and turn policy. - */ -/** @experimental */ -export interface FusionResolvedEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: FusionResolvedData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "session.fusion_resolved". - */ - type: "session.fusion_resolved"; -} -/** - * Experimental durable validated HydraFusion route and turn policy. - */ -/** @experimental */ -export interface FusionResolvedData { - /** - * Version of the validated HydraFusion event contract. - */ - contractVersion: number; - /** - * Concrete model used when the planned primary model cannot execute. - */ - fallbackModel: string; - followUp?: FusionFollowUpRecommendation; - /** - * Concrete model recommended for eligible follow-up turns. - */ - followUpModel: string; - /** - * Stable identifier for the resolved HydraFusion turn. - */ - fusionId: string; - /** - * Version of the executable model universe used for selection. - */ - modelUniverseVersion?: string; - pattern: FusionPattern; - /** - * Version of the validated execution-plan format. - */ - planVersion?: string; - /** - * HydraFusion routing policy used to resolve the plan. - */ - policy: string; - /** - * Version of the local routing policy. - */ - policyVersion?: string; - /** - * Concrete model selected for the primary solver phase. - */ - primaryModel: string; - /** - * Router implementation that supplied the plan. - */ - routeSource?: string; - /** - * Elapsed time in milliseconds required to resolve and validate the route. - */ - routingLatencyMs?: number; - /** - * Identifier of the local policy rule that matched. - */ - ruleId?: string; - /** - * Zero-based index of the local policy rule that matched. - */ - ruleIndex?: number; - /** - * Human-readable name of the local policy rule that matched. - */ - ruleName?: string; - scores?: FusionScores; - /** - * Concrete model selected for the review or judge phase, when required. - */ - secondaryModel: string | null; - /** - * Synthetic HydraFusion model selected for the session. - */ - syntheticModel: string; - /** - * Identifier of the session turn associated with the route. - */ - turnId: string; -} -/** - * Durable server recommendation for subsequent HydraFusion turns. - */ -/** @experimental */ -export interface FusionFollowUpRecommendation { - compactionTurn: FusionFollowUpAction; - userTurn: FusionFollowUpAction; -} -/** - * Validated HydraFusion routing capability scores. - */ -/** @experimental */ -export interface FusionScores { - /** - * Code-generation capability score returned by the authenticated router. - */ - codeGen: number; - /** - * Debugging capability score returned by the authenticated router. - */ - debugging: number; - /** - * Reasoning capability score returned by the authenticated router. - */ - reasoning: number; - /** - * Tool-use capability score returned by the authenticated router. - */ - toolUse: number; -} -/** - * Session event "session.fusion_completed". Experimental durable aggregate outcome of a HydraFusion turn. - */ -/** @experimental */ -export interface FusionCompletedEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: FusionCompletedData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "session.fusion_completed". - */ - type: "session.fusion_completed"; -} -/** - * Experimental durable aggregate outcome of a HydraFusion turn. - */ -/** @experimental */ -export interface FusionCompletedData { - /** - * Total cached input tokens reported across all phases. - */ - cachedTokens: number; - /** - * Total tokens written to prompt cache across all phases. - */ - cacheWriteTokens?: number; - /** - * Idempotency identifier for the authoritative final commit. - */ - commitId: string; - /** - * Reason the turn used a degraded route, when applicable. - */ - degradedReason: string | null; - /** - * Total elapsed execution time for the HydraFusion turn in milliseconds. - */ - durationMs: number; - /** - * Concrete model that supplied the authoritative final content. - */ - finalSourceModel: string | null; - /** - * Phase whose output supplied the authoritative final content. - */ - finalSourcePhaseId: string | null; - /** - * Concrete model recommended for eligible follow-up turns. - */ - followUpModel: string; - /** - * Stable identifier for the completed HydraFusion turn. - */ - fusionId: string; - /** - * Total input tokens consumed across all phases. - */ - inputTokens: number; - /** - * Stable aggregate outcome of the HydraFusion turn. - */ - outcome: string; - /** - * Total output tokens produced across all phases. - */ - outputTokens: number; - pattern: FusionPattern; - /** - * Number of concrete phases attempted by the turn. - */ - phaseCount: number; - /** - * Total concrete model requests made across all phases. - */ - requestCount: number; - /** - * Synthetic HydraFusion model selected for the session. - */ - syntheticModel: string; - /** - * Total normalized AI-unit cost reported across all phases, in nano-AIU. - */ - totalNanoAiu: number; - /** - * Identifier of the session turn associated with the completion. - */ - turnId: string; -} /** * Session event "user.message". Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. */ @@ -3998,264 +3551,6 @@ export interface AssistantIntentData { */ intent: string; } -/** - * Session event "assistant.fusion_phase_started". Experimental transient HydraFusion phase/model/role signal. - */ -/** @experimental */ -export interface AssistantFusionPhaseStartedEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: FusionPhaseStartedData; - /** - * Always true for events that are transient and not persisted to the session event log on disk. - */ - ephemeral: true; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "assistant.fusion_phase_started". - */ - type: "assistant.fusion_phase_started"; -} -/** - * Experimental transient HydraFusion phase/model/role signal. - */ -/** @experimental */ -export interface FusionPhaseStartedData { - conversationScope: FusionConversationScope; - /** - * Identifier of the HydraFusion turn containing the phase. - */ - fusionId: string; - /** - * Concrete model executing the phase. - */ - model: string; - pattern: FusionPattern; - /** - * Stable identifier for the concrete phase. - */ - phaseId: string; - phaseKind: FusionPhaseKind; - /** - * Semantic role assigned to the phase. - */ - role: string; -} -/** - * Session event "assistant.fusion_phase_completed". Experimental durable HydraFusion phase output and lossless replay checkpoint. - */ -/** @experimental */ -export interface AssistantFusionPhaseCompletedEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: FusionPhaseCompletedData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "assistant.fusion_phase_completed". - */ - type: "assistant.fusion_phase_completed"; -} -/** - * Experimental durable HydraFusion phase output and lossless replay checkpoint. - */ -/** @experimental */ -export interface FusionPhaseCompletedData { - /** - * Provider-normalized textual output produced by the phase. - */ - content: string; - conversationScope: FusionConversationScope; - /** - * Elapsed execution time for the phase in milliseconds. - */ - durationMs: number; - /** - * Identifier of the HydraFusion turn containing the phase. - */ - fusionId: string; - /** - * Concrete model that executed the phase. - */ - model: string; - /** - * Stable identifier for the completed phase. - */ - phaseId: string; - phaseKind: FusionPhaseKind; - /** - * Exact provider-normalized message used to reconstruct canonical model history. - * - * @internal - */ - projectionMessage?: JsonValue; - /** - * Projection action for the exact internal message. - * - * @internal - */ - projectionMode?: FusionProjectionMode; - /** - * Semantic role assigned to the completed phase. - */ - role: string; - /** - * Terminal request held outside canonical state until selected by the final commit. - * - * @internal - */ - stagedTerminal?: FusionStagedTerminal; - status: FusionPhaseStatus; - usage: FusionPhaseUsage; - /** - * Structured judge or critic verdict, when the phase produces one. - */ - verdict: string | null; -} -/** - * Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. - */ -/** @experimental */ -/** @internal */ -export interface FusionStagedTerminal { - arguments: string; - assistantMessage: JsonValue; - phaseId: string; - toolCallId: string; - toolName: string; -} -/** - * Aggregate concrete-model usage for one HydraFusion phase. - */ -/** @experimental */ -export interface FusionPhaseUsage { - /** - * Total cached input tokens reported for the phase. - */ - cachedTokens: number; - /** - * Total tokens written to prompt cache during the phase. - */ - cacheWriteTokens?: number; - /** - * Total input tokens consumed by the phase. - */ - inputTokens: number; - /** - * Total output tokens produced by the phase. - */ - outputTokens: number; - /** - * Number of concrete model requests made by the phase. - */ - requestCount: number; - /** - * Total normalized AI-unit cost reported for the phase, in nano-AIU. - */ - totalNanoAiu: number; -} -/** - * Session event "assistant.fusion_phase_failed". Experimental durable typed HydraFusion phase failure and degradation transition. - */ -/** @experimental */ -export interface AssistantFusionPhaseFailedEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: FusionPhaseFailedData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "assistant.fusion_phase_failed". - */ - type: "assistant.fusion_phase_failed"; -} -/** - * Experimental durable typed HydraFusion phase failure and degradation transition. - */ -/** @experimental */ -export interface FusionPhaseFailedData { - conversationScope: FusionConversationScope; - /** - * Identifier of the fallback phase used to continue the turn after degradation. - */ - degradedToPhaseId?: string; - /** - * Elapsed execution time before the phase failed, in milliseconds. - */ - durationMs: number; - /** - * Provider or execution error detail, when available. - */ - errorMessage?: string; - /** - * Identifier of the HydraFusion turn containing the phase. - */ - fusionId: string; - /** - * Concrete model that attempted the phase. - */ - model: string; - /** - * Stable identifier for the failed phase. - */ - phaseId: string; - phaseKind: FusionPhaseKind; - /** - * Stable machine-readable reason for the phase failure. - */ - reason: string; - /** - * Semantic role assigned to the failed phase. - */ - role: string; - status: FusionPhaseStatus; - usage: FusionPhaseUsage; -} /** * Session event "assistant.server_tool_progress". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message */ @@ -4544,12 +3839,6 @@ export interface AssistantMessageData { * Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. */ encryptedContent?: string; - /** - * Experimental HydraFusion source attribution for this ordinary authoritative assistant message. - * - * @experimental - */ - fusion?: FusionAttribution; /** * CAPI interaction ID for correlating this message with upstream telemetry */ @@ -4738,56 +4027,6 @@ export interface CitationLocationBlock { */ type: "block"; } -/** - * Experimental attribution linking an ordinary event to the HydraFusion turn, phase, and concrete source that produced it. - */ -/** @experimental */ -export interface FusionAttribution { - /** - * Idempotency identifier for the authoritative commit, when the event belongs to the selected output. - */ - commitId?: string; - /** - * Conversation scope in which the concrete phase executed. - */ - conversationScope?: string; - /** - * Stable identifier for the HydraFusion turn that produced the event. - */ - fusionId: string; - /** - * HydraFusion orchestration pattern selected for the turn. - */ - pattern: string; - /** - * Identifier of the concrete phase that produced the event. - */ - phaseId?: string; - /** - * Kind of concrete phase that produced the event. - */ - phaseKind?: string; - /** - * HydraFusion routing policy used for the turn. - */ - policy: string; - /** - * Semantic role assigned to the concrete phase. - */ - role?: string; - /** - * Concrete model that produced the attributed event. - */ - sourceModel?: string; - /** - * Phase whose output supplied the authoritative content, when different from the executing phase. - */ - sourcePhaseId?: string; - /** - * Synthetic HydraFusion model selected for the session. - */ - syntheticModel: string; -} /** * Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping */ @@ -5135,12 +4374,6 @@ export interface AssistantUsageData { * @internal */ frontierSource?: string; - /** - * Experimental HydraFusion attribution for this concrete model call's usage. - * - * @experimental - */ - fusion?: FusionAttribution; /** * What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ @@ -5414,12 +4647,6 @@ export interface ModelCallFailureData { */ errorType?: string; failureKind?: ModelCallFailureKind; - /** - * Experimental HydraFusion attribution for this failed concrete model call. - * - * @experimental - */ - fusion?: FusionAttribution; /** * What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ @@ -5694,12 +4921,6 @@ export interface ToolExecutionStartData { * When true, the tool output should be displayed expanded (verbatim) in the CLI timeline */ displayVerbatim?: boolean; - /** - * Experimental HydraFusion attribution for this tool execution. - * - * @experimental - */ - fusion?: FusionAttribution; /** * Name of the MCP server hosting this tool, when the tool is an MCP tool */ @@ -5909,12 +5130,6 @@ export interface ToolExecutionCompleteEvent { */ export interface ToolExecutionCompleteData { error?: ToolExecutionCompleteError; - /** - * Experimental HydraFusion attribution for this tool completion. - * - * @experimental - */ - fusion?: FusionAttribution; /** * CAPI interaction ID for correlating this tool execution with upstream telemetry */ @@ -6172,10 +5387,6 @@ export interface ToolExecutionCompleteContentShellExit { * Exit code from the completed shell command */ exitCode: number; - /** - * Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. - */ - outputFilePath?: string; /** * Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output. */ diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 82222d415e..ba339aff4e 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -4881,9 +4881,6 @@ pub struct ExternalToolTextResultForLlmContentShellExit { pub cwd: Option, /// Exit code from the completed shell command pub exit_code: i64, - /// Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. - #[serde(skip_serializing_if = "Option::is_none")] - pub output_file_path: Option, /// Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output. #[serde(skip_serializing_if = "Option::is_none")] pub output_preview: Option, @@ -5837,12 +5834,6 @@ pub struct GitHubTelemetryClientInfo { /// Copilot subscription plan, when known. #[serde(rename = "copilot_plan", skip_serializing_if = "Option::is_none")] pub copilot_plan: Option, - /// Number of logical CPU cores on the host. - #[serde(rename = "cpu_count", skip_serializing_if = "Option::is_none")] - pub cpu_count: Option, - /// Distinct CPU model names for the host, comma-separated. - #[serde(rename = "cpu_model", skip_serializing_if = "Option::is_none")] - pub cpu_model: Option, /// Stable machine identifier for the device. #[serde(rename = "dev_device_id", skip_serializing_if = "Option::is_none")] pub dev_device_id: Option, @@ -11426,9 +11417,6 @@ pub struct PermissionDecisionDeniedByPermissionRequestHook { pub struct PermissionDecisionContext { /// Disposition of the permission request as observed by the responding client. pub outcome: PermissionDecisionOutcome, - /// Whether the responding client could ask a user interactively, was running headlessly, or had no response path. Omit when the client cannot determine this authoritatively. - #[serde(skip_serializing_if = "Option::is_none")] - pub response_capability: Option, /// Controlled reason or actor responsible for the response. pub source: PermissionDecisionSource, /// Client surface that submitted the response. @@ -19628,9 +19616,15 @@ pub struct ToolsGetBuiltinDescriptorsRequest { /// Whether tool descriptors should include authoring metadata. #[serde(skip_serializing_if = "Option::is_none")] pub include_author: Option, + /// Whether line numbers should be omitted from the view tool descriptor. + #[serde(skip_serializing_if = "Option::is_none")] + pub no_view_line_numbers: Option, /// Whether descriptors should favor fewer user-intervention prompts. #[serde(skip_serializing_if = "Option::is_none")] pub reduce_user_intervention: Option, + /// Whether shell commands may only run asynchronously. + #[serde(skip_serializing_if = "Option::is_none")] + pub shell_async_only_enabled: Option, /// Shell-specific names and description lines for shell tools. #[serde(skip_serializing_if = "Option::is_none")] pub shell_config: Option, @@ -31346,31 +31340,6 @@ pub enum PermissionDecisionOutcome { Unknown, } -/// Response capability available to the client when it settled a permission request. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum PermissionResponseCapability { - /// The client could ask a user for this decision. - #[serde(rename = "interactive")] - Interactive, - /// The client could return an automated response but could not ask a user. - #[serde(rename = "headless")] - Headless, - /// The client had no response path available. - #[serde(rename = "none")] - None, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Controlled reason or actor responsible for a permission response. /// ///
@@ -31418,9 +31387,6 @@ pub enum PermissionDecisionSurface { /// The Copilot App client. #[serde(rename = "copilot_app")] CopilotApp, - /// An Agent Client Protocol host. - #[serde(rename = "acp")] - Acp, /// A generic Copilot SDK client. #[serde(rename = "sdk")] Sdk, diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index c2336aaf98..f0508660b4 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -78,42 +78,6 @@ pub enum SessionEventType { SessionCompactionComplete, #[serde(rename = "session.task_complete")] SessionTaskComplete, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "session.fusion_route_started")] - SessionFusionRouteStarted, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "session.fusion_route_failed")] - SessionFusionRouteFailed, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "session.fusion_resolved")] - SessionFusionResolved, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "session.fusion_completed")] - SessionFusionCompleted, #[serde(rename = "user.message")] UserMessage, #[serde(rename = "pending_messages.modified")] @@ -126,33 +90,6 @@ pub enum SessionEventType { AgentInterrupted, #[serde(rename = "assistant.intent")] AssistantIntent, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "assistant.fusion_phase_started")] - AssistantFusionPhaseStarted, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "assistant.fusion_phase_completed")] - AssistantFusionPhaseCompleted, - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "assistant.fusion_phase_failed")] - AssistantFusionPhaseFailed, #[serde(rename = "assistant.server_tool_progress")] AssistantServerToolProgress, #[serde(rename = "assistant.reasoning")] @@ -502,42 +439,6 @@ pub enum SessionEventData { SessionCompactionComplete(SessionCompactionCompleteData), #[serde(rename = "session.task_complete")] SessionTaskComplete(SessionTaskCompleteData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "session.fusion_route_started")] - SessionFusionRouteStarted(SessionFusionRouteStartedData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "session.fusion_route_failed")] - SessionFusionRouteFailed(SessionFusionRouteFailedData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "session.fusion_resolved")] - SessionFusionResolved(SessionFusionResolvedData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "session.fusion_completed")] - SessionFusionCompleted(SessionFusionCompletedData), #[serde(rename = "user.message")] UserMessage(UserMessageData), #[serde(rename = "pending_messages.modified")] @@ -550,33 +451,6 @@ pub enum SessionEventData { AgentInterrupted(AgentInterruptedData), #[serde(rename = "assistant.intent")] AssistantIntent(AssistantIntentData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "assistant.fusion_phase_started")] - AssistantFusionPhaseStarted(AssistantFusionPhaseStartedData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "assistant.fusion_phase_completed")] - AssistantFusionPhaseCompleted(AssistantFusionPhaseCompletedData), - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(rename = "assistant.fusion_phase_failed")] - AssistantFusionPhaseFailed(AssistantFusionPhaseFailedData), #[serde(rename = "assistant.server_tool_progress")] AssistantServerToolProgress(AssistantServerToolProgressData), #[serde(rename = "assistant.reasoning")] @@ -1069,9 +943,6 @@ pub struct SessionIdleData { /// True when the preceding agentic loop was cancelled via abort signal #[serde(skip_serializing_if = "Option::is_none")] pub aborted: Option, - /// The session mode the agent was operating in when it went idle, when the mode is known. Lets turn-scoped consumers distinguish an autopilot continuation boundary (where the agent keeps working after this idle) from a genuine turn completion. - #[serde(skip_serializing_if = "Option::is_none")] - pub mode: Option, } /// Session event "session.title_changed". Session title change payload containing the new display title @@ -1804,209 +1675,6 @@ pub struct SessionTaskCompleteData { pub summary: Option, } -/// Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionFusionRouteStartedData { - /// Identifier for this routing attempt before a durable Fusion turn exists. - pub attempt_id: String, - /// HydraFusion routing policy requested for the turn. - #[serde(skip_serializing_if = "Option::is_none")] - pub policy: Option, - /// Synthetic HydraFusion model selected for the session. - #[serde(skip_serializing_if = "Option::is_none")] - pub synthetic_model: Option, - /// Kind of turn being routed. - pub turn_kind: FusionTurnKind, -} - -/// Session event "session.fusion_route_failed". Experimental durable HydraFusion routing failure and the deterministic concrete fallback selected for the turn. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionFusionRouteFailedData { - /// Identifier of the routing attempt that failed. - pub attempt_id: String, - /// Provider or validation error detail, when available. - #[serde(skip_serializing_if = "Option::is_none")] - pub error_message: Option, - /// Concrete model selected as the deterministic fallback. - pub fallback_model: String, - /// HydraFusion routing policy requested for the turn. - pub policy: String, - /// Stable machine-readable reason for the routing failure. - pub reason: String, - /// Elapsed routing time in milliseconds before the failure. - #[serde(skip_serializing_if = "Option::is_none")] - pub routing_latency_ms: Option, - /// Synthetic HydraFusion model selected for the session. - pub synthetic_model: String, -} - -/// Durable server recommendation for subsequent HydraFusion turns. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FusionFollowUpRecommendation { - /// Recommended routing action for the next compaction turn. - pub compaction_turn: FusionFollowUpAction, - /// Recommended routing action for the next user-message turn. - pub user_turn: FusionFollowUpAction, -} - -/// Validated HydraFusion routing capability scores. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FusionScores { - /// Code-generation capability score returned by the authenticated router. - pub code_gen: f64, - /// Debugging capability score returned by the authenticated router. - pub debugging: f64, - /// Reasoning capability score returned by the authenticated router. - pub reasoning: f64, - /// Tool-use capability score returned by the authenticated router. - pub tool_use: f64, -} - -/// Session event "session.fusion_resolved". Experimental durable validated HydraFusion route and turn policy. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionFusionResolvedData { - /// Version of the validated HydraFusion event contract. - pub contract_version: i64, - /// Concrete model used when the planned primary model cannot execute. - pub fallback_model: String, - /// Router recommendation controlling reuse or rerouting on later turns. - #[serde(skip_serializing_if = "Option::is_none")] - pub follow_up: Option, - /// Concrete model recommended for eligible follow-up turns. - pub follow_up_model: String, - /// Stable identifier for the resolved HydraFusion turn. - pub fusion_id: String, - /// Version of the executable model universe used for selection. - #[serde(skip_serializing_if = "Option::is_none")] - pub model_universe_version: Option, - /// Validated orchestration pattern selected for the turn. - pub pattern: FusionPattern, - /// Version of the validated execution-plan format. - #[serde(skip_serializing_if = "Option::is_none")] - pub plan_version: Option, - /// HydraFusion routing policy used to resolve the plan. - pub policy: String, - /// Version of the local routing policy. - #[serde(skip_serializing_if = "Option::is_none")] - pub policy_version: Option, - /// Concrete model selected for the primary solver phase. - pub primary_model: String, - /// Router implementation that supplied the plan. - #[serde(skip_serializing_if = "Option::is_none")] - pub route_source: Option, - /// Elapsed time in milliseconds required to resolve and validate the route. - #[serde(skip_serializing_if = "Option::is_none")] - pub routing_latency_ms: Option, - /// Identifier of the local policy rule that matched. - #[serde(skip_serializing_if = "Option::is_none")] - pub rule_id: Option, - /// Zero-based index of the local policy rule that matched. - #[serde(skip_serializing_if = "Option::is_none")] - pub rule_index: Option, - /// Human-readable name of the local policy rule that matched. - #[serde(skip_serializing_if = "Option::is_none")] - pub rule_name: Option, - /// Validated capability scores used to select the route. - #[serde(skip_serializing_if = "Option::is_none")] - pub scores: Option, - /// Concrete model selected for the review or judge phase, when required. - pub secondary_model: Option, - /// Synthetic HydraFusion model selected for the session. - pub synthetic_model: String, - /// Identifier of the session turn associated with the route. - pub turn_id: String, -} - -/// Session event "session.fusion_completed". Experimental durable aggregate outcome of a HydraFusion turn. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionFusionCompletedData { - /// Total cached input tokens reported across all phases. - pub cached_tokens: i64, - /// Total tokens written to prompt cache across all phases. - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_write_tokens: Option, - /// Idempotency identifier for the authoritative final commit. - pub commit_id: String, - /// Reason the turn used a degraded route, when applicable. - pub degraded_reason: Option, - /// Total elapsed execution time for the HydraFusion turn in milliseconds. - pub duration_ms: f64, - /// Concrete model that supplied the authoritative final content. - pub final_source_model: Option, - /// Phase whose output supplied the authoritative final content. - pub final_source_phase_id: Option, - /// Concrete model recommended for eligible follow-up turns. - pub follow_up_model: String, - /// Stable identifier for the completed HydraFusion turn. - pub fusion_id: String, - /// Total input tokens consumed across all phases. - pub input_tokens: i64, - /// Stable aggregate outcome of the HydraFusion turn. - pub outcome: String, - /// Total output tokens produced across all phases. - pub output_tokens: i64, - /// HydraFusion orchestration pattern executed for the turn. - pub pattern: FusionPattern, - /// Number of concrete phases attempted by the turn. - pub phase_count: i64, - /// Total concrete model requests made across all phases. - pub request_count: i64, - /// Synthetic HydraFusion model selected for the session. - pub synthetic_model: String, - /// Total normalized AI-unit cost reported across all phases, in nano-AIU. - pub total_nano_aiu: f64, - /// Identifier of the session turn associated with the completion. - pub turn_id: String, -} - /// Session event "user.message". Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -2131,163 +1799,6 @@ pub struct AssistantIntentData { pub intent: String, } -/// Session event "assistant.fusion_phase_started". Experimental transient HydraFusion phase/model/role signal. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AssistantFusionPhaseStartedData { - /// Conversation scope in which the phase executes. - pub conversation_scope: FusionConversationScope, - /// Identifier of the HydraFusion turn containing the phase. - pub fusion_id: String, - /// Concrete model executing the phase. - pub model: String, - /// HydraFusion orchestration pattern containing the phase. - pub pattern: FusionPattern, - /// Stable identifier for the concrete phase. - pub phase_id: String, - /// Kind of phase being executed. - pub phase_kind: FusionPhaseKind, - /// Semantic role assigned to the phase. - pub role: String, -} - -/// Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct FusionStagedTerminal { - pub arguments: String, - pub assistant_message: serde_json::Value, - pub phase_id: String, - pub tool_call_id: String, - pub tool_name: String, -} - -/// Aggregate concrete-model usage for one HydraFusion phase. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FusionPhaseUsage { - /// Total cached input tokens reported for the phase. - pub cached_tokens: i64, - /// Total tokens written to prompt cache during the phase. - #[serde(skip_serializing_if = "Option::is_none")] - pub cache_write_tokens: Option, - /// Total input tokens consumed by the phase. - pub input_tokens: i64, - /// Total output tokens produced by the phase. - pub output_tokens: i64, - /// Number of concrete model requests made by the phase. - pub request_count: i64, - /// Total normalized AI-unit cost reported for the phase, in nano-AIU. - pub total_nano_aiu: f64, -} - -/// Session event "assistant.fusion_phase_completed". Experimental durable HydraFusion phase output and lossless replay checkpoint. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AssistantFusionPhaseCompletedData { - /// Provider-normalized textual output produced by the phase. - pub content: String, - /// Conversation scope in which the phase executed. - pub conversation_scope: FusionConversationScope, - /// Elapsed execution time for the phase in milliseconds. - pub duration_ms: f64, - /// Identifier of the HydraFusion turn containing the phase. - pub fusion_id: String, - /// Concrete model that executed the phase. - pub model: String, - /// Stable identifier for the completed phase. - pub phase_id: String, - /// Kind of phase that completed. - pub phase_kind: FusionPhaseKind, - /// Exact provider-normalized message used to reconstruct canonical model history. - #[doc(hidden)] - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) projection_message: Option, - /// Projection action for the exact internal message. - #[doc(hidden)] - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) projection_mode: Option, - /// Semantic role assigned to the completed phase. - pub role: String, - /// Terminal request held outside canonical state until selected by the final commit. - #[doc(hidden)] - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) staged_terminal: Option, - /// Durable outcome status of the phase. - pub status: FusionPhaseStatus, - /// Aggregate concrete-model usage consumed by the phase. - pub usage: FusionPhaseUsage, - /// Structured judge or critic verdict, when the phase produces one. - pub verdict: Option, -} - -/// Session event "assistant.fusion_phase_failed". Experimental durable typed HydraFusion phase failure and degradation transition. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AssistantFusionPhaseFailedData { - /// Conversation scope in which the phase executed. - pub conversation_scope: FusionConversationScope, - /// Identifier of the fallback phase used to continue the turn after degradation. - #[serde(skip_serializing_if = "Option::is_none")] - pub degraded_to_phase_id: Option, - /// Elapsed execution time before the phase failed, in milliseconds. - pub duration_ms: f64, - /// Provider or execution error detail, when available. - #[serde(skip_serializing_if = "Option::is_none")] - pub error_message: Option, - /// Identifier of the HydraFusion turn containing the phase. - pub fusion_id: String, - /// Concrete model that attempted the phase. - pub model: String, - /// Stable identifier for the failed phase. - pub phase_id: String, - /// Kind of phase that failed. - pub phase_kind: FusionPhaseKind, - /// Stable machine-readable reason for the phase failure. - pub reason: String, - /// Semantic role assigned to the failed phase. - pub role: String, - /// Durable outcome status of the phase. - pub status: FusionPhaseStatus, - /// Aggregate concrete-model usage consumed before the failure. - pub usage: FusionPhaseUsage, -} - /// Session event "assistant.server_tool_progress". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -2433,48 +1944,6 @@ pub struct Citations { pub spans: Vec, } -/// Experimental attribution linking an ordinary event to the HydraFusion turn, phase, and concrete source that produced it. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FusionAttribution { - /// Idempotency identifier for the authoritative commit, when the event belongs to the selected output. - #[serde(skip_serializing_if = "Option::is_none")] - pub commit_id: Option, - /// Conversation scope in which the concrete phase executed. - #[serde(skip_serializing_if = "Option::is_none")] - pub conversation_scope: Option, - /// Stable identifier for the HydraFusion turn that produced the event. - pub fusion_id: String, - /// HydraFusion orchestration pattern selected for the turn. - pub pattern: String, - /// Identifier of the concrete phase that produced the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub phase_id: Option, - /// Kind of concrete phase that produced the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub phase_kind: Option, - /// HydraFusion routing policy used for the turn. - pub policy: String, - /// Semantic role assigned to the concrete phase. - #[serde(skip_serializing_if = "Option::is_none")] - pub role: Option, - /// Concrete model that produced the attributed event. - #[serde(skip_serializing_if = "Option::is_none")] - pub source_model: Option, - /// Phase whose output supplied the authoritative content, when different from the executing phase. - #[serde(skip_serializing_if = "Option::is_none")] - pub source_phase_id: Option, - /// Synthetic HydraFusion model selected for the session. - pub synthetic_model: String, -} - /// Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping /// ///
@@ -2579,16 +2048,6 @@ pub struct AssistantMessageData { /// Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. #[serde(skip_serializing_if = "Option::is_none")] pub encrypted_content: Option, - /// Experimental HydraFusion source attribution for this ordinary authoritative assistant message. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub fusion: Option, /// CAPI interaction ID for correlating this message with upstream telemetry #[serde(skip_serializing_if = "Option::is_none")] pub interaction_id: Option, @@ -2815,16 +2274,6 @@ pub struct AssistantUsageData { #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) frontier_source: Option, - /// Experimental HydraFusion attribution for this concrete model call's usage. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub fusion: Option, /// What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls #[serde(skip_serializing_if = "Option::is_none")] pub initiator: Option, @@ -3042,16 +2491,6 @@ pub struct ModelCallFailureData { /// Whether the failure originated from an API response or the request transport #[serde(skip_serializing_if = "Option::is_none")] pub failure_kind: Option, - /// Experimental HydraFusion attribution for this failed concrete model call. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub fusion: Option, /// What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls #[serde(skip_serializing_if = "Option::is_none")] pub initiator: Option, @@ -3126,16 +2565,6 @@ pub struct ModelCallFinishedData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModelCallStartData { - /// Experimental HydraFusion attribution for this concrete model call. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub fusion: Option, /// Model identifier used for this API call, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, @@ -3233,16 +2662,6 @@ pub struct ToolExecutionStartData { /// When true, the tool output should be displayed expanded (verbatim) in the CLI timeline #[serde(skip_serializing_if = "Option::is_none")] pub display_verbatim: Option, - /// Experimental HydraFusion attribution for this tool execution. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub fusion: Option, /// Name of the MCP server hosting this tool, when the tool is an MCP tool #[serde(skip_serializing_if = "Option::is_none")] pub mcp_server_name: Option, @@ -3441,9 +2860,6 @@ pub struct ToolExecutionCompleteContentShellExit { pub cwd: Option, /// Exit code from the completed shell command pub exit_code: i64, - /// Path reported in the shell session's filesystem namespace when shell output exceeded the configured large-output threshold. - #[serde(skip_serializing_if = "Option::is_none")] - pub output_file_path: Option, /// Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output. #[serde(skip_serializing_if = "Option::is_none")] pub output_preview: Option, @@ -3754,16 +3170,6 @@ pub struct ToolExecutionCompleteData { /// Error details when the tool execution failed #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Experimental HydraFusion attribution for this tool completion. - /// - ///
- /// - /// **Experimental.** This type is part of an experimental wire-protocol surface - /// and may change or be removed in future SDK or CLI releases. - /// - ///
- #[serde(skip_serializing_if = "Option::is_none")] - pub fusion: Option, /// CAPI interaction ID for correlating this tool execution with upstream telemetry #[serde(skip_serializing_if = "Option::is_none")] pub interaction_id: Option, @@ -6346,24 +5752,6 @@ pub enum Verbosity { Unknown, } -/// The session mode the agent is operating in -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum SessionMode { - /// The agent is responding interactively to the user. - #[serde(rename = "interactive")] - Interactive, - /// The agent is preparing a plan before making changes. - #[serde(rename = "plan")] - Plan, - /// The agent is working autonomously toward task completion. - #[serde(rename = "autopilot")] - Autopilot, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Who created the schedule: `user` (an explicit user action such as `/every` or `/after`) or `model` (the agent via the `manage_schedule` tool). Gates whether a scheduled skill that opted out of model invocation may fire: only user-created schedules may. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ScheduleOrigin { @@ -6460,6 +5848,24 @@ pub enum ModelChangeSource { Unknown, } +/// The session mode the agent is operating in +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SessionMode { + /// The agent is responding interactively to the user. + #[serde(rename = "interactive")] + Interactive, + /// The agent is preparing a plan before making changes. + #[serde(rename = "plan")] + Plan, + /// The agent is working autonomously toward task completion. + #[serde(rename = "autopilot")] + Autopilot, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Permission mode for the session. /// ///
@@ -6590,75 +5996,6 @@ pub enum TaskCompletionOutcome { Unknown, } -/// Kind of turn for which HydraFusion routing is running. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionTurnKind { - /// A user-message turn. - #[serde(rename = "user")] - User, - /// A conversation-compaction turn. - #[serde(rename = "compaction")] - Compaction, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Server-recommended routing behavior for a later HydraFusion turn. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionFollowUpAction { - /// Reuse the durable primary model without routing. - #[serde(rename = "reuse_primary")] - ReusePrimary, - /// Request a new routing decision. - #[serde(rename = "reroute")] - Reroute, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Validated HydraFusion execution pattern. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionPattern { - /// Run one primary solver phase. - #[serde(rename = "single")] - Single, - /// Run a primary phase, a judge, and an optional repair. - #[serde(rename = "cascade")] - Cascade, - /// Run a primary draft, a read-only critique, and a revision. - #[serde(rename = "critique")] - Critique, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// The agent mode that was active when this message was sent #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum UserMessageAgentMode { @@ -6749,115 +6086,6 @@ pub enum ModelCallFailureTransport { Unknown, } -/// Conversation scope in which a HydraFusion phase executes. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionConversationScope { - /// Canonical root conversation history. - #[serde(rename = "root")] - Root, - /// Isolated read-only review history that does not enter the root conversation. - #[serde(rename = "review")] - Review, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// HydraFusion phase kind. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionPhaseKind { - /// Primary solver phase. - #[serde(rename = "primary")] - Primary, - /// Read-only cascade judge phase. - #[serde(rename = "judge")] - Judge, - /// Cascade repair phase. - #[serde(rename = "repair")] - Repair, - /// Initial critique-pattern draft phase. - #[serde(rename = "draft")] - Draft, - /// Read-only critique phase. - #[serde(rename = "critic")] - Critic, - /// Critique-pattern revision phase. - #[serde(rename = "revision")] - Revision, - /// Follow-up phase continuing from the resolved model. - #[serde(rename = "follow_up")] - FollowUp, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// How a durable phase checkpoint contributes its exact message to canonical root history. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionProjectionMode { - /// Append the exact root message immediately. - #[serde(rename = "append")] - Append, - /// Hold a terminal message outside canonical history until the final commit selects it. - #[serde(rename = "staged")] - Staged, - /// Do not project the checkpoint into root history. - #[serde(rename = "none")] - None, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// Durable outcome status of a HydraFusion phase. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionPhaseStatus { - /// The phase completed successfully. - #[serde(rename = "succeeded")] - Succeeded, - /// The phase failed. - #[serde(rename = "failed")] - Failed, - /// The phase was cancelled. - #[serde(rename = "cancelled")] - Cancelled, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AssistantMessageToolRequestType { diff --git a/rust/src/handler.rs b/rust/src/handler.rs index e036b75a10..f1f0d9566d 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -114,7 +114,6 @@ impl PermissionResult { /// /// let result = PermissionResult::approve_once().with_context(PermissionDecisionContext { /// outcome: PermissionDecisionOutcome::AutoApproved, - /// response_capability: None, /// source: PermissionDecisionSource::HostPolicy, /// surface: PermissionDecisionSurface::Sdk, /// }); diff --git a/rust/src/session.rs b/rust/src/session.rs index ebf588973a..3e5ae13dee 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -2718,7 +2718,6 @@ mod tests { fn attribution_context() -> PermissionDecisionContext { PermissionDecisionContext { outcome: PermissionDecisionOutcome::AutoApproved, - response_capability: None, source: PermissionDecisionSource::AssistedApproval, surface: PermissionDecisionSurface::CopilotApp, } @@ -2807,7 +2806,6 @@ mod tests { .with_context(attribution_context()) .with_context(PermissionDecisionContext { outcome: PermissionDecisionOutcome::PromptedUser, - response_capability: None, source: PermissionDecisionSource::HumanResponse, surface: PermissionDecisionSurface::Sdk, }); diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index a495fadbc2..83274c1084 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -51,7 +51,6 @@ impl PermissionHandler for ContextualApproveHandler { ) -> PermissionResult { PermissionResult::approve_once().with_context(PermissionDecisionContext { outcome: PermissionDecisionOutcome::PromptedUser, - response_capability: None, source: PermissionDecisionSource::HumanResponse, surface: PermissionDecisionSurface::CopilotApp, }) From 2bc1df46341e099922c76689e18dd75b47de5052 Mon Sep 17 00:00:00 2001 From: Touseef Liaqat Date: Wed, 26 Aug 2026 13:01:16 -0700 Subject: [PATCH 4/5] Remove unrelated generator normalization Keep the generated watch API additive against the SDK's pinned CLI schema and leave repository-wide line-ending policy unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: be0a255d-04f8-4830-888a-59f54ad1e607 --- .gitattributes | 3 --- nodejs/src/generated/rpc.ts | 16 ++++++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.gitattributes b/.gitattributes index 4be0f40e95..2a92ef0172 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,8 +2,6 @@ # Cross-platform tools rewrite these files, so keep their output deterministic. java/**/*.java text eol=lf -nodejs/**/*.ts text eol=lf -rust/**/*.rs text eol=lf # Generated files — keep LF line endings so codegen output is deterministic across platforms. nodejs/src/generated/* eol=lf linguist-generated=true @@ -13,4 +11,3 @@ go/zsession_events.go eol=lf linguist-generated=true go/zsession_encoding.go eol=lf linguist-generated=true go/rpc/zrpc.go eol=lf linguist-generated=true go/rpc/zrpc_encoding.go eol=lf linguist-generated=true -rust/src/generated/* eol=lf linguist-generated=true diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 193ecf602f..0d3de53f95 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -18131,10 +18131,6 @@ export interface SessionOpenOptions { * Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the same name remain available. */ includedBuiltinAgents?: string[]; - /** - * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. - */ - includedBuiltinSkills?: string[]; /** * Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. */ @@ -18177,6 +18173,10 @@ export interface SessionOpenOptions { * Additional directories to search for skills. */ skillDirectories?: string[]; + /** + * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. + */ + includedBuiltinSkills?: string[]; /** * Skill IDs disabled for this session. */ @@ -19552,10 +19552,6 @@ export interface SessionUpdateOptionsParams { * Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the same name remain available. Set to null to remove the allowlist restriction. */ includedBuiltinAgents?: string[] | null; - /** - * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. Set to null to remove the allowlist restriction. - */ - includedBuiltinSkills?: string[] | null; /** * Built-in subagent names to exclude from this session. Excluded built-ins are hidden from agent discovery and cannot be dispatched unless a custom agent with the same name is available. */ @@ -19595,6 +19591,10 @@ export interface SessionUpdateOptionsParams { * Additional directories to search for skills. */ skillDirectories?: string[]; + /** + * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. Set to null to remove the allowlist restriction. + */ + includedBuiltinSkills?: string[] | null; /** * Skill IDs that should be excluded from this session. */ From 2ec3886f4691bc24dff8a1bac2beabb1c45124ac Mon Sep 17 00:00:00 2001 From: Touseef Liaqat Date: Wed, 26 Aug 2026 14:07:30 -0700 Subject: [PATCH 5/5] Make watch teardown ordering atomic Route disconnect cleanup in the ordered session notification task and store watch classification with router entries so shutdown always uses sessions.close without racing terminal cleanup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: be0a255d-04f8-4830-888a-59f54ad1e607 --- rust/src/lib.rs | 41 +++++++++++++--------------- rust/src/router.rs | 56 ++++++++++++++++++++++++++++++++------ rust/src/watch.rs | 3 +- rust/tests/session_test.rs | 50 ++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 32 deletions(-) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index e61028e2ac..d19eb5d022 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -72,7 +72,6 @@ pub(crate) mod generated; /// source-qualified tool filter patterns. pub mod mode; -use std::collections::HashSet; use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Stdio; @@ -1023,7 +1022,6 @@ struct ClientInner { request_rx: parking_lot::Mutex>>, notification_tx: broadcast::Sender, router: router::SessionRouter, - watch_sessions: Arc>>, negotiated_protocol_version: OnceLock, state: parking_lot::Mutex, lifecycle_tx: broadcast::Sender, @@ -1617,7 +1615,6 @@ impl Client { request_rx: parking_lot::Mutex::new(Some(request_rx)), notification_tx: notification_broadcast_tx, router: router::SessionRouter::new(), - watch_sessions: Arc::new(parking_lot::Mutex::new(HashSet::new())), negotiated_protocol_version: OnceLock::new(), state: parking_lot::Mutex::new(ConnectionState::Connected), lifecycle_tx: broadcast::channel(256).0, @@ -1648,8 +1645,6 @@ impl Client { fn spawn_lifecycle_dispatcher(&self) { let mut notif_rx = self.inner.notification_tx.subscribe(); let lifecycle_tx = self.inner.lifecycle_tx.clone(); - let router = self.inner.router.clone(); - let watch_sessions = self.inner.watch_sessions.clone(); tokio::spawn(async move { loop { match notif_rx.recv().await { @@ -1673,12 +1668,7 @@ impl Client { }; // `send` only errors when there are no subscribers — that's // the normal case before any consumer calls subscribe_lifecycle. - let _ = lifecycle_tx.send(event.clone()); - if event.event_type == SessionLifecycleEventType::Disconnected - && watch_sessions.lock().remove(&event.session_id) - { - router.unregister(&event.session_id); - } + let _ = lifecycle_tx.send(event); } Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { warn!(missed = n, "lifecycle dispatcher lagged"); @@ -2067,12 +2057,20 @@ impl Client { self.inner.router.unregister(session_id); } - pub(crate) fn register_watch_session(&self, session_id: &SessionId) { - self.inner.watch_sessions.lock().insert(session_id.clone()); + pub(crate) fn register_watch_session( + &self, + session_id: &SessionId, + ) -> crate::router::SessionChannels { + self.inner.router.ensure_started( + &self.inner.notification_tx, + &self.inner.request_rx, + self.inner.llm_inference.get().cloned(), + self.inner.on_github_telemetry.clone(), + ); + self.inner.router.register_watch(session_id) } pub(crate) fn unregister_watch_session(&self, session_id: &SessionId) { - self.inner.watch_sessions.lock().remove(session_id); self.inner.router.unregister(session_id); } @@ -2294,8 +2292,8 @@ impl Client { pub async fn cleanup_sessions_for_test(&self) -> Result<()> { let mut first_error = None; - for session_id in self.inner.router.session_ids() { - let method = if self.inner.watch_sessions.lock().remove(&session_id) { + for (session_id, is_watch) in self.inner.router.session_entries() { + let method = if is_watch { generated::api_types::rpc_methods::SESSIONS_CLOSE } else { "session.destroy" @@ -2458,8 +2456,8 @@ impl Client { // Snapshot the registered session IDs without holding the router // lock across the destroy RPCs. - for session_id in self.inner.router.session_ids() { - let method = if self.inner.watch_sessions.lock().remove(&session_id) { + for (session_id, is_watch) in self.inner.router.session_entries() { + let method = if is_watch { generated::api_types::rpc_methods::SESSIONS_CLOSE } else { "session.destroy" @@ -2607,7 +2605,7 @@ impl Client { // Drop all session channels so any awaiters see a closed channel // instead of waiting for responses that will never arrive. self.inner.router.clear(); - self.inner.watch_sessions.lock().clear(); + *self.inner.state.lock() = ConnectionState::Disconnected; *self.inner.models_cache.lock() = Arc::new(tokio::sync::OnceCell::new()); } @@ -3295,7 +3293,7 @@ mod tests { handle.abort(); let _ = handle.await; - assert!(client.inner.router.session_ids().is_empty()); + assert!(client.inner.router.session_entries().is_empty()); client.force_stop(); } @@ -3415,7 +3413,6 @@ mod tests { request_rx: parking_lot::Mutex::new(None), notification_tx: broadcast::channel(16).0, router: router::SessionRouter::new(), - watch_sessions: Arc::new(parking_lot::Mutex::new(HashSet::new())), negotiated_protocol_version: OnceLock::new(), state: parking_lot::Mutex::new(ConnectionState::Connected), lifecycle_tx: broadcast::channel(16).0, @@ -3435,7 +3432,7 @@ mod tests { async fn wait_for_pending_session_registration(client: &Client) { let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1); - while client.inner.router.session_ids().is_empty() { + while client.inner.router.session_entries().is_empty() { assert!( tokio::time::Instant::now() < deadline, "session was not registered" diff --git a/rust/src/router.rs b/rust/src/router.rs index be104bd7c1..7588fb2628 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -6,7 +6,9 @@ use tokio::sync::{broadcast, mpsc}; use tracing::warn; use crate::jsonrpc::{JsonRpcNotification, JsonRpcRequest}; -use crate::types::{SessionEventNotification, SessionId}; +use crate::types::{ + SessionEventNotification, SessionId, SessionLifecycleEvent, SessionLifecycleEventType, +}; /// Per-session channels created by the router during session registration. pub(crate) struct SessionChannels { @@ -19,6 +21,7 @@ pub(crate) struct SessionChannels { struct SessionSenders { notifications: mpsc::UnboundedSender, requests: mpsc::UnboundedSender, + is_watch: bool, } /// Routes notifications and requests by sessionId to per-session channels. @@ -40,6 +43,15 @@ impl SessionRouter { /// Register a session to receive filtered events and requests. pub(crate) fn register(&self, session_id: &SessionId) -> SessionChannels { + self.register_with_kind(session_id, false) + } + + /// Register a passive shared-session watch. + pub(crate) fn register_watch(&self, session_id: &SessionId) -> SessionChannels { + self.register_with_kind(session_id, true) + } + + fn register_with_kind(&self, session_id: &SessionId, is_watch: bool) -> SessionChannels { let (notif_tx, notif_rx) = mpsc::unbounded_channel(); let (req_tx, req_rx) = mpsc::unbounded_channel(); self.sessions.lock().insert( @@ -47,6 +59,7 @@ impl SessionRouter { SessionSenders { notifications: notif_tx, requests: req_tx, + is_watch, }, ); SessionChannels { @@ -60,13 +73,13 @@ impl SessionRouter { self.sessions.lock().remove(session_id.as_str()); } - /// Snapshot every currently-registered session ID. - /// - /// Used by [`Client::stop`](crate::Client::stop) to iterate active - /// sessions for cooperative shutdown without holding the router lock - /// across `.await`. - pub(crate) fn session_ids(&self) -> Vec { - self.sessions.lock().keys().cloned().collect() + /// Snapshot registered session IDs with their cleanup classification. + pub(crate) fn session_entries(&self) -> Vec<(SessionId, bool)> { + self.sessions + .lock() + .iter() + .map(|(session_id, senders)| (session_id.clone(), senders.is_watch)) + .collect() } /// Drop all registered session channels. @@ -136,6 +149,33 @@ impl SessionRouter { } continue; } + if notification.method == "session.lifecycle" { + let Some(ref params) = notification.params else { + continue; + }; + match serde_json::from_value::(params.clone()) { + Ok(event) + if event.event_type + == SessionLifecycleEventType::Disconnected => + { + let mut guard = sessions.lock(); + if guard + .get(&event.session_id) + .is_some_and(|senders| senders.is_watch) + { + guard.remove(&event.session_id); + } + } + Ok(_) => {} + Err(e) => { + warn!( + error = %e, + "failed to deserialize session.lifecycle notification" + ); + } + } + continue; + } if notification.method != "session.event" { continue; } diff --git a/rust/src/watch.rs b/rust/src/watch.rs index cf759f53d3..6ab01190a2 100644 --- a/rust/src/watch.rs +++ b/rust/src/watch.rs @@ -154,8 +154,7 @@ impl Client { ) })?; let result: WatchSharedSessionResult = serde_json::from_value(value.clone())?; - let channels = client.register_session(&result.session_id); - client.register_watch_session(&result.session_id); + let channels = client.register_watch_session(&result.session_id); *registration_for_callback.lock() = Some(channels); Ok(()) })), diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 83274c1084..9e38bd4de5 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -345,6 +345,56 @@ async fn shared_session_watch_retains_replay_and_closes_once() { timeout(TIMEOUT, close_handle).await.unwrap().unwrap(); } +#[tokio::test] +async fn client_stop_closes_shared_session_watches() { + let (client, server_read, server_write) = make_client(); + let mut server = FakeServer { + read: server_read, + write: server_write, + session_id: "watch-session".to_string(), + }; + + let watch_handle = tokio::spawn({ + let client = client.clone(); + async move { client.watch_shared_session("shared-session").await.unwrap() } + }); + let watch_request = server.read_request().await; + server + .respond( + &watch_request, + serde_json::json!({ + "sessionId": "watch-session", + "readOnly": true, + "metadata": { + "sessionId": "watch-session", + "startTime": "2025-01-01T00:00:00Z", + "modifiedTime": "2025-01-01T00:01:00Z", + "repository": { + "owner": "github", + "name": "copilot-sdk", + "branch": "main" + }, + "kind": "remote-session" + } + }), + ) + .await; + let _watch = timeout(TIMEOUT, watch_handle).await.unwrap().unwrap(); + + let stop_handle = tokio::spawn({ + let client = client.clone(); + async move { client.stop().await.unwrap() } + }); + let close_request = server.read_request().await; + assert_eq!(close_request["method"], "sessions.close"); + assert_eq!( + close_request["params"], + serde_json::json!({ "sessionId": "watch-session" }) + ); + server.respond(&close_request, serde_json::json!({})).await; + timeout(TIMEOUT, stop_handle).await.unwrap().unwrap(); +} + fn rand_id() -> u64 { static COUNTER: AtomicUsize = AtomicUsize::new(0); COUNTER.fetch_add(1, Ordering::Relaxed) as u64