diff --git a/nodejs/README.md b/nodejs/README.md index a6e577be93..b3a1b28ee5 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -2,6 +2,9 @@ TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC. +For experimental native AHP 0.9 endpoints and an SDK-owned WebSocket sample, see +[Native AHP endpoints](docs/ahp-endpoints.md). + ## Prerequisites To use the SDK, you'll need: diff --git a/nodejs/docs/ahp-endpoints.md b/nodejs/docs/ahp-endpoints.md new file mode 100644 index 0000000000..2c01583506 --- /dev/null +++ b/nodejs/docs/ahp-endpoints.md @@ -0,0 +1,207 @@ + + +# Native AHP endpoints (experimental) + +`CopilotClient.createAhpEndpoint()` registers a native AHP 0.9 endpoint on the +client's **existing runtime JSON-RPC connection**. It does not start a second +runtime, an HTTP server, or a WebSocket listener. The application owns its physical +transport. Only the native runtime parses AHP, translates actions, and tracks AHP +request IDs and session state; the Node SDK transports opaque strings. + +This requires a matching locally built runtime that implements the `ahp.*` RPC +methods. Published runtimes without those methods fail with an explicit +"does not support native AHP endpoints" error. Older AHP versions are not supported +by this API. Mock-wire tests do **not** establish real native runtime acceptance. + +The [Rust SDK](../../rust/README.md#native-ahp-endpoints-experimental) exposes the +same transport-neutral feature. Its recorded E2E suite hosts a Rust WebSocket +listener and drives real agent/tool turns with the standard AHP client, using the +shared CapiProxy record-replay harness. + +## API + +```ts +const endpoint = await client.createAhpEndpoint({ + onCreateSession: (request, { signal }) => { + signal.throwIfAborted(); + return client.createSession({ + ...applicationSessionConfig, // prompt, local tools, permission callbacks + sessionId: request.requestedSessionId, + }); + }, + onResumeSession: ({ sessionId }) => client.resumeSession(sessionId, applicationSessionConfig), + onListSessions: () => applicationVisibleSessions, +}); +const connection = await endpoint.openConnection({ + onMessage: (text) => applicationTransport.write(text), + onClose: (error) => applicationTransport.close(error), +}); +await connection.send(incomingText); +await endpoint.refreshExposure(); +await endpoint.setCapabilities(applicationCapabilities); +await connection.close(); +await endpoint.dispose(); +``` + +Create/resume callbacks return a `CopilotSession` or `{ sessionId }`; list returns +an array of those identities. The runtime attaches to the returned session, not a +second newly created one. `onSessionControl(request, { signal })` optionally +returns `{ applied, reason?, result? }`. Application functions never cross the +wire. Rejection or cancellation fails the native operation; no fallback creation +occurs. Ordinary SDK methods are safe inside callbacks; recursive endpoint +operations are rejected. Respect `signal` when doing application work. Cancellation +does not roll back an ordinary session creation that has already completed. + +Omitting callbacks delegates policy to native runtime defaults, rather than +installing SDK policy. Omitted `allowSessionCreation` and `capabilities` are not +sent, preserving native defaults. Explicit `allowSessionCreation: false` disables +creation at the endpoint. **`onListSessions` is endpoint-wide authorization.** The +native endpoint enforces the returned session set for subscriptions, history, +actions, disposal, and root notifications, including attempts to use an excluded +session's URI directly. Applications do not need to duplicate those access checks +in resume or control callbacks. Authenticate the physical listener in production. +`refreshExposure()` asks the runtime to refresh the authorized session set after +application policy changes. + +Attaching an already-live session does not invoke `onResumeSession`. A cold +authorized session invokes that callback so the application can restore its +ordinary SDK configuration, including local tools and permission handlers. + +Persistence follows ordinary SDK lifecycle rules. An unnamed session with no user activity +is not saved simply by disconnecting. `createSession({ name: "..." })` opts into persistence +before the first turn. The sample also sets the workspace name and calls `sessions.save` +before disconnecting, so its cold-resume case has a durable journal and working-directory +metadata rather than relying on an unused in-memory session. + +`send()` resolves when the runtime admits the message, not when the AHP request +finishes. Output arrives via `onMessage`, with IDs and text unchanged. Output +callbacks run serially per logical connection but never block another connection +or the SDK reader. Resolve `onMessage` only after the physical transport has +delivered the message, not merely queued it in an unbounded application buffer. +The WebSocket sample awaits the `ws.send` completion callback. + +Internally, `ahp.message` is a **server-to-SDK request**, returning `null` only after +`onMessage` resolves. This is a transport-delivery acknowledgment, not an AHP +response; the opaque payload's request IDs remain untouched. Waiting for the +acknowledgment preserves native output backpressure across the runtime's ordinary +SDK writer. Failed delivery, cancellation, and logical connection closure reject +pending acknowledgments, including queued messages. `ahp.connectionClosed` and +`ahp.endpointClosed` remain notifications; `ahp.send` still acknowledges admission. + +Default per-direction limits (including in-flight delivery): +1 MiB UTF-8 per message, 128 outstanding messages, 8 MiB total buffered bytes. +`limits` can tighten local bounds; it does not change the runtime's own limits. +Values above these hard ceilings are rejected before endpoint registration. +Overflow, admission failure, or a rejected output callback closes the affected +logical connection and reports an error through `onClose`, without disconnecting +or cancelling its owning session. + +Close/dispose are idempotent and clean up locally even if runtime cleanup fails. +Explicit cleanup calls still reject on remote errors; repeat calls return the same +result. Runtime EOF and SDK stop close endpoints and abort application callbacks. +As with ordinary SDK sessions, stopping the client itself also disconnects its +session handles. + +## Run the SDK-owned WebSocket sample + +Requires Node 22.12+ (the standard AHP client's WebSocket transport uses Node's +global WebSocket), a runtime checkout with this slice's native changes, and normal +Copilot authentication if using model-driven turns. + +Build the matching runtime in its checkout: + +```sh +cd /workspace/copilot-agent-runtime +node_modules/.bin/tsx script/build-addons-bazel.ts --profile debug +# Export the COPILOT_NAPI_ADDONS_PREBUILT, COPILOT_BUILD_RUNTIME_BIN and +# COPILOT_REPO_ROOT values printed by the staging command, then: +corepack pnpm run build +``` + +Then run the **SDK application**, which owns the localhost-only WebSocket listener: + +```sh +cd /workspace/copilot-sdk/nodejs +npm ci +npm run build +COPILOT_RUNTIME_PATH=/workspace/copilot-agent-runtime/dist-cli/index.js \ + npx tsx examples/ahp-websocket-server.ts +``` + +The path must point to the matching runtime executable/JavaScript entry point, not +an installed older CLI. This is the ordinary `RuntimeConnection.forStdio({ path })` +choice, not an additional host server. + +To use the staged native executable directly instead of `dist-cli/index.js`, set +both `COPILOT_RUNTIME_PATH` to `src/native/runtime/copilot-runtime.linux-x64-gnu` +and `COPILOT_RUNTIME_PROVIDER_LIB` to the absolute path of the adjacent +`runtime.linux-x64-gnu.node` (adjust platform names as appropriate). The staging +filename differs from the packaged `runtime.node` name the executable otherwise +looks for. + +The server prints `COLD_SESSION_ID` and `EXCLUDED_SESSION_ID`. Copy their values +into the command in a second terminal: + +```sh +cd /workspace/copilot-sdk/nodejs +COLD_SESSION_ID= EXCLUDED_SESSION_ID= \ + npx tsx examples/ahp-websocket-client.ts +``` + +The standard `@microsoft/agent-host-protocol` 0.9 client initializes, lists sessions, +tries subscribing directly to the excluded URI, subscribes to the authorized +persisted session, creates through the application override, and attaches the +new live session. The excluded-URI probe requires a native authorization/not-found +error; transport failures or unexpected success fail the sample. + +The SDK app creates two configured sessions. It disconnects the ordinary SDK owner +of the authorized session **before** registering the endpoint, while retaining +that session ID in `onListSessions`. The first authorized subscription exercises +the cold-resume callback; its invocation is logged in the server terminal. Later +subscriptions while the session is live attach without invoking resume. The other +session is omitted from `onListSessions`; native authorization rejects it, with no +duplicate access check in the application's resume callback. + +Create and cold-resume callbacks both supply the distinct application prompt and +local, harmless `demo_label` tool. Permission and ask-user callbacks prompt in the +server terminal; the sample does not use `approveAll`. + +The default client run makes no model calls. To opt into two model-driven turns +(one on the resumed session, one on the newly created session): + +```sh +COLD_SESSION_ID= EXCLUDED_SESSION_ID= \ + AHP_RUN_MODEL=1 npx tsx examples/ahp-websocket-client.ts +``` + +This optional exercise uses the standard AHP client, state mirror, chat +subscription, and typed actions. It requests `demo_label` and checks for its +completed tool event and the `SDK demo:` response prefix. Model/authentication +failures, missing evidence, and a 120-second timeout fail the exercise rather than +claiming success. It is not run in deterministic CI. Neither the listener nor the +SDK adds any AHP-to-runtime protocol mapping. + +`PORT` changes the listener port; `AHP_URL` changes the client URL. Press Ctrl+C in +the server terminal to dispose the endpoint and stop the SDK. Do not expose this +unauthenticated demonstration listener beyond loopback. + +## Deterministic validation + +From `nodejs`: + +```sh +npm test -- test/ahp.test.ts +npm run typecheck +npm run build +``` + +These test the actual JSON-RPC duplex reader with a mocked runtime, including +reentrant ordinary SDK calls, cancellation, local tool retention, opaque frames, +queue limits, independent delivery, cleanup, and unsupported-runtime errors. +Real runtime and model-backed integration acceptance is a separate requirement. + +The runtime's native integration suite exercises real SDK transport, session +participation and pending-request settlement. Its offline CLI scenarios also +cross the physical sharing listener and the same endpoint API. Use the sample's +optional real-model mode separately to exercise application-owned tools and +permissions against the matching runtime. diff --git a/nodejs/examples/ahp-websocket-client.ts b/nodejs/examples/ahp-websocket-client.ts new file mode 100644 index 0000000000..3026318c78 --- /dev/null +++ b/nodejs/examples/ahp-websocket-client.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { + ActionType, + AhpErrorCodes, + MessageKind, + ResponsePartKind, + type Snapshot, +} from "@microsoft/agent-host-protocol"; +import { AhpClient, AhpStateMirror, RpcError } from "@microsoft/agent-host-protocol/client"; +import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws"; + +async function exerciseModel(client: AhpClient, snapshot: Snapshot): Promise { + const mirror = new AhpStateMirror(); + mirror.applySnapshot(snapshot); + const session = mirror.getSession(snapshot.resource); + const chat = session?.defaultChat ?? session?.chats[0]?.resource; + if (!chat) throw new Error("Native session snapshot did not advertise a chat"); + const { subscription } = await client.subscribe(chat); + const turnId = randomUUID(); + const parts = new Map(); + const labelCalls = new Set(); + let labelCompleted = false; + let timeout: ReturnType | undefined; + try { + client.dispatch(chat, { + type: ActionType.ChatTurnStarted, + turnId, + startedAt: new Date().toISOString(), + message: { + text: "Call demo_label to get your application label, then report its result. Follow your application's response-prefix instructions.", + origin: { kind: MessageKind.User }, + }, + }); + const completion = (async () => { + for await (const event of subscription) { + if (event.type !== "action") continue; + const action = event.params.action; + if (!("turnId" in action) || action.turnId !== turnId) continue; + switch (action.type) { + case ActionType.ChatResponsePart: + if (action.part.kind === ResponsePartKind.Markdown) { + parts.set(action.part.id, action.part.content); + } + break; + case ActionType.ChatDelta: + parts.set(action.partId, (parts.get(action.partId) ?? "") + action.content); + process.stdout.write(action.content); + break; + case ActionType.ChatToolCallStart: + console.log("Native tool call:", action.toolName); + if (action.toolName === "demo_label") labelCalls.add(action.toolCallId); + break; + case ActionType.ChatToolCallComplete: + if (labelCalls.has(action.toolCallId)) { + if (!action.result.success) + throw new Error("Local demo_label tool failed"); + labelCompleted = true; + console.log("Local demo_label result:", action.result); + } + break; + case ActionType.ChatError: + throw new Error(`Native model turn failed: ${JSON.stringify(action.part)}`); + case ActionType.ChatTurnComplete: { + const text = [...parts.values()].join(""); + console.log("\nCompleted model response:", text); + if (!text.includes("SDK demo:") || !labelCompleted) { + throw new Error( + "Did not observe both the distinctive prompt and demo_label execution" + ); + } + return; + } + } + } + throw new Error("AHP subscription closed before the model turn completed"); + })(); + const timedOut = new Promise((_, reject) => { + timeout = setTimeout( + () => reject(new Error("Model exercise timed out after 120 seconds")), + 120_000 + ); + }); + await Promise.race([completion, timedOut]); + } finally { + clearTimeout(timeout); + await subscription.close(); + await client.unsubscribe(chat); + } +} + +const transport = await WebSocketTransport.connect(process.env.AHP_URL ?? "ws://127.0.0.1:8765"); +const client = new AhpClient(transport); +client.connect(); +try { + console.log( + "initialize:", + await client.initialize({ + clientId: `sdk-demo-${randomUUID()}`, + protocolVersions: ["0.9.0"], + initialSubscriptions: ["ahp-root://"], + }) + ); + const before = await client.request("listSessions", { channel: "ahp-root://" }); + console.log("Visible sessions (private SDK session is excluded):", before.items); + const excludedId = process.env.EXCLUDED_SESSION_ID; + if (excludedId) { + const excludedUri = `ahp-session:/${excludedId}`; + if (before.items.some((item) => item.resource === excludedUri)) { + throw new Error("Native endpoint listed the excluded session"); + } + try { + const unexpected = await client.subscribe(excludedUri); + await unexpected.subscription.close(); + throw new Error("Native endpoint unexpectedly authorized the excluded session URI"); + } catch (error) { + if ( + !(error instanceof RpcError) || + ![ + AhpErrorCodes.PermissionDenied, + AhpErrorCodes.SessionNotFound, + AhpErrorCodes.NotFound, + ].some((code) => code === error.code) + ) { + throw error; + } + console.log( + "Direct excluded-URI subscription rejected by native authorization:", + error.message + ); + } + } else { + console.log("EXCLUDED_SESSION_ID not set; direct excluded-URI probe skipped."); + } + const coldId = process.env.COLD_SESSION_ID; + const existingUri = coldId ? `ahp-session:/${coldId}` : before.items[0]?.resource; + if (existingUri) { + const existing = await client.subscribe(existingUri); + console.log("Authorized persisted-session snapshot:", existing.result); + console.log( + "The first subscription after owner disconnect exercises cold resume; a live attachment does not invoke that override. Check the SDK server log." + ); + if (process.env.AHP_RUN_MODEL === "1") { + if (!existing.result.snapshot) throw new Error("Missing persisted-session snapshot"); + await exerciseModel(client, existing.result.snapshot); + } + await existing.subscription.close(); + await client.unsubscribe(existingUri); + } + const channel = `ahp-session:/${randomUUID()}`; + await client.request("createSession", { channel, provider: "copilot" }); + console.log("Created through the SDK callback:", channel); + const { result, subscription } = await client.subscribe(channel); + console.log("New live session snapshot (attachment, not a resume override):", result); + if (process.env.AHP_RUN_MODEL === "1") { + if (!result.snapshot) throw new Error("Missing new-session snapshot"); + await exerciseModel(client, result.snapshot); + } + console.log( + "Visible sessions after create:", + await client.request("listSessions", { channel: "ahp-root://" }) + ); + await subscription.close(); + await client.unsubscribe(channel); +} finally { + await client.shutdown(); +} diff --git a/nodejs/examples/ahp-websocket-server.ts b/nodejs/examples/ahp-websocket-server.ts new file mode 100644 index 0000000000..8f58058ca7 --- /dev/null +++ b/nodejs/examples/ahp-websocket-server.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { createInterface } from "node:readline/promises"; +import { WebSocket, WebSocketServer } from "ws"; +import { + CopilotClient, + RuntimeConnection, + type AhpConnection, + type SessionConfig, +} from "../src/index.js"; + +const runtimePath = process.env.COPILOT_RUNTIME_PATH; +if (!runtimePath) { + throw new Error("Set COPILOT_RUNTIME_PATH to the matching locally built runtime executable"); +} + +const terminal = createInterface({ input: process.stdin, output: process.stdout }); +const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ path: runtimePath }), +}); +const authorizedSessionIds = new Set(); +let questions = Promise.resolve(); +function question(prompt: string): Promise { + const answer = questions.then(() => terminal.question(prompt)); + questions = answer.then( + () => undefined, + () => undefined + ); + return answer; +} + +// All executable configuration stays in this SDK process, not in ahp.registerEndpoint. +const config: SessionConfig = { + systemMessage: { + mode: "append", + content: + "You are the SDK-owned AHP demo. Begin replies with 'SDK demo:'. Use demo_label when asked for your label.", + }, + tools: [ + { + name: "demo_label", + description: + "Return this application's harmless demo label without accessing files or network.", + parameters: { type: "object", properties: {}, additionalProperties: false }, + handler: () => "SDK-owned native AHP example", + }, + ], + onPermissionRequest: async (request, invocation) => { + console.log("Permission request:", invocation.sessionId, request); + const answer = await question("Allow once? [y/N] "); + return answer.trim().toLowerCase() === "y" + ? { kind: "approve-once" } + : { kind: "denied-interactively-by-user" }; + }, + onUserInputRequest: async (request) => { + console.log("Agent question:", request.question, request.choices ?? []); + return { answer: await question("Your answer: "), wasFreeform: true }; + }, +}; + +try { + const shared = await client.createSession({ + ...config, + sessionId: randomUUID(), + name: "SDK-owned AHP cold-resume example", + }); + const privateSession = await client.createSession({ ...config, sessionId: randomUUID() }); + authorizedSessionIds.add(shared.sessionId); + // Naming opts into persistence before the first turn. Drop its ordinary SDK + // owner before registering the endpoint, retaining authorization to resume it. + await shared.rpc.name.set({ name: "SDK-owned AHP cold-resume example" }); + await client.rpc.sessions.save({ sessionId: shared.sessionId }); + await shared.disconnect(); + const endpoint = await client.createAhpEndpoint({ + onListSessions: () => [...authorizedSessionIds].map((sessionId) => ({ sessionId })), + onCreateSession: async (request, { signal }) => { + signal.throwIfAborted(); + console.log("AHP create override:", request.requestedSessionId); + const session = await client.createSession({ + ...config, + sessionId: request.requestedSessionId, + }); + signal.throwIfAborted(); + authorizedSessionIds.add(session.sessionId); + return session; + }, + onResumeSession: async ({ sessionId }, { signal }) => { + signal.throwIfAborted(); + console.log("AHP cold-resume override:", sessionId); + const session = await client.resumeSession(sessionId, config); + signal.throwIfAborted(); + return session; + }, + }); + const server = new WebSocketServer({ + host: "127.0.0.1", + port: Number(process.env.PORT ?? 8765), + maxPayload: 1024 * 1024, + }); + server.on("connection", (socket) => { + // Do not accumulate application messages while openConnection is pending. + socket.pause(); + let logical: AhpConnection | undefined; + const fail = (error: unknown) => { + console.error("AHP transport:", error); + socket.close(1011, "AHP transport failure"); + }; + socket.on("error", fail); + socket.on("close", () => { + void logical?.close().catch(fail); + }); + void endpoint + .openConnection({ + onMessage: (message) => + new Promise((resolve, reject) => { + if (socket.readyState !== WebSocket.OPEN) { + reject(new Error("WebSocket is not open")); + return; + } + socket.send(message, (error) => (error ? reject(error) : resolve())); + }), + onClose: (error) => { + if (error) console.error("Native AHP connection:", error); + socket.close( + error ? 1011 : 1000, + error ? "AHP connection failed" : "AHP connection closed" + ); + }, + }) + .then(async (connection) => { + logical = connection; + if (socket.readyState !== WebSocket.OPEN) { + await connection.close(); + return; + } + socket.on("message", (data, binary) => { + if (binary) { + socket.close(1003, "Text messages required"); + void connection.close().catch(fail); + return; + } + void connection.send(data.toString()).catch(fail); + }); + socket.resume(); + }) + .catch(fail); + }); + await new Promise((resolve, reject) => { + server.once("listening", resolve); + server.once("error", reject); + }); + console.log("SDK-owned WebSocket listener:", server.address()); + console.log( + "Authorized persisted session (ordinary SDK owner disconnected):", + shared.sessionId + ); + console.log("Excluded by native onListSessions authorization:", privateSession.sessionId); + console.log("Copy these variables into the AHP client command:"); + console.log( + `COLD_SESSION_ID=${shared.sessionId} EXCLUDED_SESSION_ID=${privateSession.sessionId}` + ); + console.log( + "Permission and ask-user requests are answered in this terminal, not auto-approved." + ); + await new Promise((resolve) => { + process.once("SIGINT", resolve); + process.once("SIGTERM", resolve); + }); + try { + await endpoint.dispose(); + } finally { + for (const socket of server.clients) socket.terminate(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + } +} finally { + terminal.close(); + const errors = await client.stop(); + if (errors.length) throw new AggregateError(errors, "SDK shutdown failed"); +} diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 4214cc8cd7..a6562826f8 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -14,6 +14,7 @@ "zod": "4.3.6" }, "devDependencies": { + "@microsoft/agent-host-protocol": "0.9.0", "@platformatic/vfs": "^0.3.0", "@types/node": "^25.2.0", "@types/semver": "7.8.0", @@ -1006,6 +1007,12 @@ "url": "https://liberapay.com/Koromix" } }, + "node_modules/@microsoft/agent-host-protocol": { + "version": "0.9.0", + "integrity": "sha512-XY6zOqLSqlNianiWbEpZkzKRB2AKwcDPk9yAbp+afxyxhWzTfpJKy+03T5rIipJDnqf45rXiJ5JV/dzUA8JHLw==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.3", "integrity": "sha1-l+PUXXQk3F2h1OMvO/OykvbBtEw=", diff --git a/nodejs/package.json b/nodejs/package.json index 440c533d7d..9ad9321d05 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -70,6 +70,7 @@ "zod": "4.3.6" }, "devDependencies": { + "@microsoft/agent-host-protocol": "0.9.0", "@platformatic/vfs": "^0.3.0", "@types/node": "^25.2.0", "@types/semver": "7.8.0", diff --git a/nodejs/src/ahp.ts b/nodejs/src/ahp.ts new file mode 100644 index 0000000000..31f53da3dd --- /dev/null +++ b/nodejs/src/ahp.ts @@ -0,0 +1,563 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { AsyncLocalStorage } from "node:async_hooks"; +import { + type CancellationToken, + type Disposable, + type MessageConnection, + ResponseError, +} from "vscode-jsonrpc/node.js"; +import type { + AhpCreateSessionRequest as WireCreateSessionRequest, + AhpResumeSessionRequest as WireResumeSessionRequest, + AhpListSessionsRequest as WireListSessionsRequest, + AhpMessageRequest as WireMessageRequest, + AhpSessionControlRequest as WireSessionControlRequest, + AhpConnectionClosedNotification, + AhpEndpointClosedNotification, + AhpEndpointRef, + AhpRegisterEndpointResult, + AhpOpenConnectionResult, +} from "./generated/rpc.js"; + +/** A session returned by application policy; a CopilotSession also satisfies this interface. */ +export interface AhpSessionIdentity { + sessionId: string; +} + +/** Application callback context. Cancellation never disconnects the owning SDK session. */ +export interface AhpCallbackContext { + signal: AbortSignal; +} + +export interface AhpCreateSessionRequest { + connectionId: string; + requestedSessionId: string; + workingDirectory?: string; + model?: string; + config?: unknown; +} + +export interface AhpResumeSessionRequest { + connectionId: string; + sessionId: string; +} + +export interface AhpSessionControlRequest { + sessionId: string; + kind: string; + payload: unknown; +} + +export interface AhpSessionControlResult { + applied: boolean; + reason?: string; + result?: unknown; +} + +/** Local policy functions are never serialized. Omitted callbacks use native runtime defaults. */ +export interface AhpEndpointOptions { + allowSessionCreation?: boolean; + capabilities?: unknown; + onCreateSession?: ( + request: AhpCreateSessionRequest, + context: AhpCallbackContext + ) => AhpSessionIdentity | Promise; + onResumeSession?: ( + request: AhpResumeSessionRequest, + context: AhpCallbackContext + ) => AhpSessionIdentity | Promise; + /** + * Authorizes sessions for this endpoint. The native endpoint enforces this + * set for subscriptions, history, actions, disposal, and root notifications. + */ + onListSessions?: ( + context: AhpCallbackContext + ) => readonly AhpSessionIdentity[] | Promise; + onSessionControl?: ( + request: AhpSessionControlRequest, + context: AhpCallbackContext + ) => AhpSessionControlResult | Promise; + /** + * Tighter local limits per direction, including the currently in-flight message. + * Values cannot exceed 1 MiB per message, 128 messages, or 8 MiB buffered. + */ + limits?: { + maxMessageBytes?: number; + maxQueuedMessages?: number; + maxBufferedBytes?: number; + }; +} + +export interface AhpConnectionOptions { + /** + * Deliver one opaque AHP message. Calls are serialized for this connection + * only. Resolve after transport delivery; the native delivery ACK waits for it. + */ + onMessage: (message: string) => void | Promise; + onClose?: (error?: Error) => void; +} + +export interface AhpConnection { + readonly id: string; + /** Resolves on runtime admission, not on the eventual AHP response. */ + send(message: string): Promise; + close(): Promise; +} + +export interface AhpEndpoint { + readonly id: string; + openConnection(options: AhpConnectionOptions): Promise; + refreshExposure(): Promise; + setCapabilities(capabilities: unknown): Promise; + dispose(): Promise; +} + +const callbackScope = new AsyncLocalStorage(); +const closedError = () => new Error("AHP endpoint or connection is closed"); +const asError = (error: unknown) => (error instanceof Error ? error : new Error(String(error))); +const MAX_LIMITS = Object.freeze({ + maxMessageBytes: 1024 * 1024, + maxQueuedMessages: 128, + maxBufferedBytes: 8 * 1024 * 1024, +}); + +function assertNotCallback(): void { + if (callbackScope.getStore()) { + throw new Error("Recursive AHP endpoint operations from an AHP callback are not supported"); + } +} + +type CallbackRequest = + | { method: "createSession"; params: WireCreateSessionRequest } + | { method: "resumeSession"; params: WireResumeSessionRequest } + | { method: "listSessions"; params: WireListSessionsRequest } + | { method: "sessionControl"; params: WireSessionControlRequest }; + +/** @internal Opaque bridge over the client's existing runtime connection. */ +export class AhpEndpointRegistry { + private endpoints = new Map(); + private subscriptions: Disposable[] = []; + private lifetime = new AbortController(); + + constructor(private readonly rpc: MessageConnection) { + this.registerCallback("createSession", (params) => ({ + method: "createSession", + params, + })); + this.registerCallback("resumeSession", (params) => ({ + method: "resumeSession", + params, + })); + this.registerCallback("listSessions", (params) => ({ + method: "listSessions", + params, + })); + this.registerCallback("sessionControl", (params) => ({ + method: "sessionControl", + params, + })); + this.subscriptions.push( + rpc.onRequest("ahp.message", (params: WireMessageRequest, token: CancellationToken) => { + const connection = this.endpoints + .get(params.endpointId) + ?.connections.get(params.connectionId); + if (!connection) throw closedError(); + return connection.receive(params.message, token); + }), + rpc.onNotification( + "ahp.connectionClosed", + (params: AhpConnectionClosedNotification) => { + this.endpoints + .get(params.endpointId) + ?.connections.get(params.connectionId) + ?.finish(params.error === undefined ? undefined : new Error(params.error)); + } + ), + rpc.onNotification("ahp.endpointClosed", (params: AhpEndpointClosedNotification) => { + this.endpoints + .get(params.endpointId) + ?.finish(params.error === undefined ? undefined : new Error(params.error)); + }), + rpc.onClose(() => this.close(new Error("Runtime connection closed"))), + rpc.onDispose(() => this.close(new Error("Runtime connection disposed"))) + ); + } + + private registerCallback( + method: CallbackRequest["method"], + request: (params: T) => CallbackRequest + ): void { + this.subscriptions.push( + this.rpc.onRequest(`ahp.${method}`, (params: T, token: CancellationToken) => { + const endpoint = this.endpoints.get(params.endpointId); + if (!endpoint) throw closedError(); + return endpoint.invoke(request(params), token); + }) + ); + } + + get closed(): boolean { + return this.lifetime.signal.aborted; + } + + async request(method: string, params: unknown): Promise { + if (this.closed) throw closedError(); + try { + return await this.rpc.sendRequest(`ahp.${method}`, params); + } catch (error) { + if (error instanceof ResponseError && error.code === -32601) { + throw new Error( + "This runtime does not support native AHP endpoints. Use a matching runtime with ahp.* support.", + { cause: error } + ); + } + throw error; + } + } + + async create(options: AhpEndpointOptions): Promise { + assertNotCallback(); + const limits = { + maxMessageBytes: options.limits?.maxMessageBytes ?? MAX_LIMITS.maxMessageBytes, + maxQueuedMessages: options.limits?.maxQueuedMessages ?? MAX_LIMITS.maxQueuedMessages, + maxBufferedBytes: options.limits?.maxBufferedBytes ?? MAX_LIMITS.maxBufferedBytes, + }; + for (const key of ["maxMessageBytes", "maxQueuedMessages", "maxBufferedBytes"] as const) { + const value = limits[key]; + if (!Number.isSafeInteger(value) || value <= 0) { + throw new Error("AHP limits must be positive safe integers"); + } + if (value > MAX_LIMITS[key]) { + throw new Error(`AHP ${key} cannot exceed ${MAX_LIMITS[key]}`); + } + } + const { endpointId } = await this.request("registerEndpoint", { + callbacks: { + createSession: !!options.onCreateSession, + resumeSession: !!options.onResumeSession, + listSessions: !!options.onListSessions, + sessionControl: !!options.onSessionControl, + }, + ...(options.allowSessionCreation === undefined + ? {} + : { allowSessionCreation: options.allowSessionCreation }), + ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }), + }); + if (this.closed) throw closedError(); + const endpoint = new Endpoint(endpointId, this, options, limits, () => + this.endpoints.delete(endpointId) + ); + this.endpoints.set(endpointId, endpoint); + return endpoint; + } + + close(error: Error): void { + if (this.closed) return; + this.lifetime.abort(error); + for (const endpoint of this.endpoints.values()) endpoint.finish(error); + for (const subscription of this.subscriptions) subscription.dispose(); + this.subscriptions = []; + } +} + +type Limits = Required>; + +class Endpoint implements AhpEndpoint { + readonly connections = new Map(); + private lifetime = new AbortController(); + private callbacks = new Map(); + private disposing?: Promise; + + constructor( + readonly id: string, + private readonly registry: AhpEndpointRegistry, + private readonly options: AhpEndpointOptions, + readonly limits: Limits, + private readonly remove: () => void + ) {} + + get closed(): boolean { + return this.lifetime.signal.aborted; + } + + request(method: string, params: object = {}): Promise { + assertNotCallback(); + if (this.closed) return Promise.reject(closedError()); + return this.registry.request(method, { endpointId: this.id, ...params }); + } + + async openConnection(options: AhpConnectionOptions): Promise { + const { connectionId } = await this.request("openConnection"); + if (this.closed) throw closedError(); + const connection = new LogicalConnection(connectionId, this, options); + this.connections.set(connectionId, connection); + return connection; + } + + refreshExposure(): Promise { + return this.request("refreshExposure"); + } + + setCapabilities(capabilities: unknown): Promise { + return this.request("setCapabilities", { capabilities }); + } + + dispose(): Promise { + assertNotCallback(); + if (this.disposing) return this.disposing; + if (this.closed) return Promise.resolve(); + const disposal = this.registry.request("disposeEndpoint", { endpointId: this.id }); + this.finish(); + this.disposing = disposal; + return disposal; + } + + finish(error?: Error): void { + if (this.closed) return; + this.lifetime.abort(error ?? closedError()); + for (const controller of this.callbacks.keys()) controller.abort(error ?? closedError()); + for (const connection of this.connections.values()) connection.finish(error); + this.remove(); + } + + connectionFinished(id: string): void { + this.connections.delete(id); + for (const [controller, connectionId] of this.callbacks) { + if (connectionId === id) controller.abort(closedError()); + } + } + + async invoke(request: CallbackRequest, token: CancellationToken): Promise { + if (this.closed) throw closedError(); + if ( + (request.method === "createSession" || request.method === "resumeSession") && + !this.connections.has(request.params.connectionId) + ) { + throw closedError(); + } + const controller = new AbortController(); + this.callbacks.set( + controller, + "connectionId" in request.params ? request.params.connectionId : undefined + ); + const subscription = token.onCancellationRequested(() => controller.abort()); + if (token.isCancellationRequested) controller.abort(); + const context = { signal: controller.signal }; + let abortListener: (() => void) | undefined; + try { + const cancelled = new Promise((_, reject) => { + abortListener = () => reject(new ResponseError(-32800, "AHP callback cancelled")); + controller.signal.addEventListener("abort", abortListener, { once: true }); + if (controller.signal.aborted) abortListener(); + }); + const result = callbackScope.run(true, async () => { + controller.signal.throwIfAborted(); + switch (request.method) { + case "createSession": { + if (!this.options.onCreateSession) throw new Error("No create callback"); + const { endpointId: _endpointId, ...params } = request.params; + const session = await this.options.onCreateSession(params, context); + return { sessionId: session.sessionId }; + } + case "resumeSession": { + if (!this.options.onResumeSession) throw new Error("No resume callback"); + const { endpointId: _endpointId, ...params } = request.params; + const session = await this.options.onResumeSession(params, context); + return { sessionId: session.sessionId }; + } + case "listSessions": { + if (!this.options.onListSessions) throw new Error("No list callback"); + const sessions = await this.options.onListSessions(context); + return { sessionIds: sessions.map((session) => session.sessionId) }; + } + case "sessionControl": { + if (!this.options.onSessionControl) throw new Error("No control callback"); + const { endpointId: _endpointId, ...params } = request.params; + return this.options.onSessionControl(params, context); + } + } + }); + return await Promise.race([cancelled, result]); + } finally { + subscription.dispose(); + if (abortListener) controller.signal.removeEventListener("abort", abortListener); + this.callbacks.delete(controller); + } + } +} + +interface InputMessage { + message: string; + bytes: number; + resolve: () => void; + reject: (error: Error) => void; +} + +interface OutputMessage { + message: string; + bytes: number; + resolve: (value: null) => void; + reject: (error: Error) => void; + cancellation?: Disposable; +} + +class LogicalConnection implements AhpConnection { + private closed = false; + private closing?: Promise; + private input: InputMessage[] = []; + private output: OutputMessage[] = []; + private inputBytes = 0; + private outputBytes = 0; + private sending = false; + private delivering = false; + + constructor( + readonly id: string, + private readonly endpoint: Endpoint, + private readonly options: AhpConnectionOptions + ) {} + + private check(message: string, count: number, bytes: number): number { + if (typeof message !== "string") throw new Error("AHP messages must be strings"); + const size = Buffer.byteLength(message, "utf8"); + const limits = this.endpoint.limits; + if (size > limits.maxMessageBytes) throw new Error("AHP message size limit exceeded"); + if (count >= limits.maxQueuedMessages || bytes + size > limits.maxBufferedBytes) { + throw new Error("AHP connection buffer limit exceeded"); + } + return size; + } + + async send(message: string): Promise { + assertNotCallback(); + if (this.closed) throw closedError(); + let bytes: number; + try { + bytes = this.check(message, this.input.length, this.inputBytes); + } catch (error) { + this.fail(asError(error)); + throw error; + } + return new Promise((resolve, reject) => { + this.input.push({ message, bytes, resolve, reject }); + this.inputBytes += bytes; + void this.drainInput(); + }); + } + + private async drainInput(): Promise { + if (this.sending) return; + this.sending = true; + try { + while (!this.closed && this.input.length) { + const item = this.input[0]; + try { + await this.endpoint.request("send", { + connectionId: this.id, + message: item.message, + }); + } catch (error) { + this.fail(asError(error)); + return; + } + if (this.closed) return; + this.input.shift(); + this.inputBytes -= item.bytes; + item.resolve(); + } + } finally { + this.sending = false; + } + } + + async receive(message: string, token: CancellationToken): Promise { + if (this.closed) throw closedError(); + let bytes: number; + try { + bytes = this.check(message, this.output.length, this.outputBytes); + } catch (error) { + this.fail(asError(error)); + throw error; + } + return new Promise((resolve, reject) => { + const item: OutputMessage = { message, bytes, resolve, reject }; + this.output.push(item); + this.outputBytes += bytes; + const cancel = () => { + this.fail(new ResponseError(-32800, "AHP output delivery cancelled")); + }; + item.cancellation = token.onCancellationRequested(cancel); + if (token.isCancellationRequested) cancel(); + void this.drainOutput(); + }); + } + + private async drainOutput(): Promise { + if (this.delivering) return; + this.delivering = true; + try { + while (!this.closed && this.output.length) { + const item = this.output[0]; + try { + await this.options.onMessage(item.message); + } catch (error) { + this.fail(asError(error)); + return; + } + if (this.closed) return; + this.output.shift(); + this.outputBytes -= item.bytes; + item.cancellation?.dispose(); + // This acknowledges physical delivery, not the opaque AHP request. + item.resolve(null); + } + } finally { + this.delivering = false; + } + } + + private fail(error: Error): void { + if (this.closed) return; + const cleanup = this.endpoint.request("closeConnection", { connectionId: this.id }); + this.finish(error); + // A failed transport cleanup is observable by an explicit close(), while + // onClose already reports the original delivery/admission failure. + this.closing = cleanup; + void cleanup.catch((cleanupError: unknown) => { + console.error("Failed to close native AHP connection", cleanupError); + }); + } + + close(): Promise { + assertNotCallback(); + if (this.closing) return this.closing; + if (this.closed) return Promise.resolve(); + this.closing = this.endpoint.request("closeConnection", { connectionId: this.id }); + this.finish(); + return this.closing; + } + + finish(error?: Error): void { + if (this.closed) return; + this.closed = true; + for (const item of this.input) item.reject(error ?? closedError()); + for (const item of this.output) { + item.cancellation?.dispose(); + item.reject(error ?? closedError()); + } + this.input = []; + this.output = []; + this.inputBytes = this.outputBytes = 0; + this.endpoint.connectionFinished(this.id); + try { + this.options.onClose?.(error); + } catch (callbackError) { + // onClose is a terminal notification, not a request; report a broken + // observer without interrupting other connections' shutdown. + console.error("AHP onClose callback failed", callbackError); + } + } +} diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 6e4b4fb5b4..48d4ec3682 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -93,6 +93,7 @@ import type { } from "./types.js"; import { defaultJoinSessionPermissionHandler } from "./types.js"; import type { FactoryHandle } from "./factory.js"; +import { AhpEndpointRegistry, type AhpEndpoint, type AhpEndpointOptions } from "./ahp.js"; /** * Minimum protocol version this SDK can communicate with. @@ -442,6 +443,7 @@ export class CopilotClient { private cliProcess: ChildProcess | null = null; private ffiHost: FfiRuntimeHost | null = null; private connection: MessageConnection | null = null; + private ahpEndpoints?: AhpEndpointRegistry; private messageWriter: TeardownResilientStreamMessageWriter | null = null; private connectionClosed: boolean = false; private socket: Socket | null = null; @@ -914,6 +916,23 @@ export class CopilotClient { } } + /** + * Registers a native AHP endpoint on the ordinary runtime connection. + * The application owns the physical transport; the SDK forwards opaque strings. + * Requires a matching runtime with ahp.* support. + */ + async createAhpEndpoint(options: AhpEndpointOptions = {}): Promise { + if (!this.connection) { + await this.start(); + } + if (!this.ahpEndpoints || this.ahpEndpoints.closed) { + // Connection setup registers generated handlers first. Override AHP + // handlers here so callback requests retain their CancellationToken. + this.ahpEndpoints = new AhpEndpointRegistry(this.connection!); + } + return this.ahpEndpoints.create(options); + } + /** * Starts the CLI server and establishes a connection. * @@ -1032,6 +1051,8 @@ export class CopilotClient { * ``` */ async stop(): Promise { + this.ahpEndpoints?.close(new Error("CopilotClient stopped")); + this.ahpEndpoints = undefined; const errors: Error[] = []; // Disconnect all active sessions with retry logic @@ -1264,6 +1285,8 @@ export class CopilotClient { * ``` */ async forceStop(): Promise { + this.ahpEndpoints?.close(new Error("CopilotClient stopped")); + this.ahpEndpoints = undefined; this.forceStopping = true; // Clear sessions immediately without trying to destroy them @@ -1645,6 +1668,7 @@ export class CopilotClient { ...(await getTraceContext(this.onGetTraceContext)), model: config.model, sessionId: localSessionId, + name: config.name, clientName: config.clientName, reasoningEffort: config.reasoningEffort, reasoningSummary: config.reasoningSummary, @@ -3013,6 +3037,11 @@ export class CopilotClient { return; } + // A reconnect replaces both generated handlers and the endpoint registry. + // The next createAhpEndpoint installs its manual handlers after this setup. + this.ahpEndpoints?.close(new Error("Runtime connection replaced")); + this.ahpEndpoints = undefined; + this.connection.onNotification("session.event", (notification: unknown) => { this.handleSessionEventNotification(notification); }); diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index b0bbbb56e4..b2bd98d46e 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -24622,6 +24622,207 @@ export interface SessionFsSqliteExistsRequest { sessionId: string; } +/** @experimental */ +/** @internal */ +export interface AhpConnectionClosedNotification { + endpointId: string; + connectionId: string; + error?: string; +} + +/** @experimental */ +/** @internal */ +export interface AhpConnectionRef { + endpointId: string; + connectionId: string; +} + +/** @experimental */ +/** @internal */ +export interface AhpCreateSessionRequest { + endpointId: string; + connectionId: string; + requestedSessionId: string; + workingDirectory?: string; + model?: string; + config?: JsonValue; +} +/** + * Executable endpoint handlers retained by the application, never serialized. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AhpEndpointCallbacks". + */ +/** @experimental */ +/** @internal */ +export interface AhpEndpointCallbacks { + createSession: boolean; + resumeSession: boolean; + listSessions: boolean; + sessionControl: boolean; +} + +/** @experimental */ +/** @internal */ +export interface AhpEndpointClosedNotification { + endpointId: string; + error?: string; +} + +/** @experimental */ +/** @internal */ +export interface AhpEndpointRef { + endpointId: string; +} + +/** @experimental */ +/** @internal */ +export interface AhpListSessionsResult { + sessionIds: string[]; +} + +/** @experimental */ +/** @internal */ +export interface AhpMessage { + endpointId: string; + connectionId: string; + /** + * One complete AHP JSON message, preserved without SDK-side decoding. + */ + message: string; +} + +/** @experimental */ +/** @internal */ +export interface AhpOpenConnectionResult { + connectionId: string; +} +/** + * Endpoint policy and the callbacks installed on its owning SDK connection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AhpRegisterEndpointRequest". + */ +/** @experimental */ +/** @internal */ +export interface AhpRegisterEndpointRequest { + callbacks: AhpEndpointCallbacks; + allowSessionCreation?: boolean; + capabilities?: JsonValue; +} + +/** @experimental */ +/** @internal */ +export interface AhpResumeSessionRequest { + endpointId: string; + connectionId: string; + sessionId: string; +} + +/** @experimental */ +/** @internal */ +export interface AhpSessionControlRequest { + endpointId: string; + sessionId: string; + kind: string; + payload: JsonValue; +} + +/** @experimental */ +/** @internal */ +export interface AhpSessionControlResult { + applied: boolean; + reason?: string; + result?: JsonValue; +} + +/** @experimental */ +/** @internal */ +export interface AhpSessionIdentity { + sessionId: string; +} + +/** @experimental */ +/** @internal */ +export interface AhpSetCapabilitiesRequest { + endpointId: string; + capabilities: JsonValue; +} + +/** @experimental */ +export interface AhpRegisterEndpointResult { + endpointId: string; +} + +/** @experimental */ +export interface AhpOpenConnectionRequest { + endpointId: string; +} + +/** @experimental */ +export interface AhpSendRequest { + endpointId: string; + connectionId: string; + /** + * One complete AHP JSON message, preserved without SDK-side decoding. + */ + message: string; +} + +/** @experimental */ +export interface AhpCloseConnectionRequest { + endpointId: string; + connectionId: string; +} + +/** @experimental */ +export interface AhpDisposeEndpointRequest { + endpointId: string; +} + +/** @experimental */ +export interface AhpRefreshExposureRequest { + endpointId: string; +} + +/** @experimental */ +export interface AhpCreateSessionResult { + sessionId: string; +} + +/** @experimental */ +export interface AhpResumeSessionResult { + sessionId: string; +} + +/** @experimental */ +export interface AhpListSessionsRequest { + endpointId: string; +} + +/** @experimental */ +export interface AhpMessageRequest { + endpointId: string; + connectionId: string; + /** + * One complete AHP JSON message, preserved without SDK-side decoding. + */ + message: string; +} + +/** @experimental */ +export interface AhpConnectionClosedRequest { + endpointId: string; + connectionId: string; + error?: string; +} + +/** @experimental */ +export interface AhpEndpointClosedRequest { + endpointId: string; + error?: string; +} + /** Create typed server-scoped RPC methods (no session required). */ export function createServerRpc(connection: MessageConnection) { return { @@ -25387,6 +25588,46 @@ export function createServerRpc(connection: MessageConnection) { */ export function createInternalServerRpc(connection: MessageConnection) { return { + /** @experimental */ + ahp: { + /** + * Registers an application-owned, transport-neutral AHP agent endpoint. Callback flags refer to executable handlers retained by the SDK client; no listener is opened by the runtime. + * + * @param params Endpoint policy and the callbacks installed on its owning SDK connection. + */ + registerEndpoint: async (params: AhpRegisterEndpointRequest): Promise => + connection.sendRequest("ahp.registerEndpoint", params), + /** + * Opens an independent logical AHP connection on an endpoint owned by this SDK connection. + */ + openConnection: async (params: AhpOpenConnectionRequest): Promise => + connection.sendRequest("ahp.openConnection", params), + /** + * Admits one complete opaque AHP message to a logical connection. Success acknowledges admission, not completion; AHP responses arrive through ahp.message. Message size and retained queue limits are enforced by the runtime. + */ + send: async (params: AhpSendRequest): Promise => + connection.sendRequest("ahp.send", params), + /** + * Idempotently closes one logical AHP connection and releases its participation without closing the original session owner. + */ + closeConnection: async (params: AhpCloseConnectionRequest): Promise => + connection.sendRequest("ahp.closeConnection", params), + /** + * Idempotently closes an application-owned AHP endpoint and all of its logical connections. + */ + disposeEndpoint: async (params: AhpDisposeEndpointRequest): Promise => + connection.sendRequest("ahp.disposeEndpoint", params), + /** + * Re-evaluates this endpoint's session exposure policy and removes access and subscriptions for sessions no longer exposed. + */ + refreshExposure: async (params: AhpRefreshExposureRequest): Promise => + connection.sendRequest("ahp.refreshExposure", params), + /** + * Updates the endpoint's application-supplied agent catalog and customizations without changing session configuration. + */ + setCapabilities: async (params: AhpSetCapabilitiesRequest): Promise => + connection.sendRequest("ahp.setCapabilities", params), + }, /** * Performs the SDK server connection handshake and validates the optional connection token. Marked internal because this is JSON-RPC transport plumbing invoked automatically by an SDK client's own `connect()` wrapper, not a user-facing method. Stays internal as long as the SDK client owns the handshake; would only become public if the SDK ever exposed the raw schema surface to consumers without a connection wrapper. * diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 8a5a730b5a..30ccf211d5 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -9,6 +9,18 @@ */ export { CopilotClient } from "./client.js"; +export type { + AhpEndpoint, + AhpEndpointOptions, + AhpConnection, + AhpConnectionOptions, + AhpCallbackContext, + AhpCreateSessionRequest, + AhpResumeSessionRequest, + AhpSessionControlRequest, + AhpSessionControlResult, + AhpSessionIdentity, +} from "./ahp.js"; export { DisableBypassPermissionsModes, RuntimeConnection } from "./types.js"; export { BuiltInTools, ToolSet } from "./toolSet.js"; export { CopilotSession, type AssistantMessageEvent } from "./session.js"; diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 9c4258d9c9..cf65f80a9b 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -2986,6 +2986,11 @@ export interface SessionConfig extends SessionConfigBase { */ sessionId?: string; + /** + * Optional friendly name. A named session is persisted even before its first turn. + */ + name?: string; + /** * Creates a remote session in the cloud instead of a local session. * The optional repository is associated with the cloud session. diff --git a/nodejs/test/ahp.test.ts b/nodejs/test/ahp.test.ts new file mode 100644 index 0000000000..14456d7c06 --- /dev/null +++ b/nodejs/test/ahp.test.ts @@ -0,0 +1,647 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { PassThrough } from "node:stream"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { + CancellationToken, + CancellationTokenSource, + createMessageConnection, + ResponseError, + StreamMessageReader, + StreamMessageWriter, +} from "vscode-jsonrpc/node.js"; +import { CopilotClient, RuntimeConnection, type AhpEndpointOptions } from "../src/index.js"; + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (error: Error) => void; + const promise = new Promise((yes, no) => { + resolve = yes; + reject = no; + }); + return { promise, resolve, reject }; +} + +function harness() { + const toServer = new PassThrough(); + const toClient = new PassThrough(); + const sdk = createMessageConnection( + new StreamMessageReader(toClient), + new StreamMessageWriter(toServer) + ); + const server = createMessageConnection( + new StreamMessageReader(toServer), + new StreamMessageWriter(toClient) + ); + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("localhost:1234"), + }); + (client as any).connection = sdk; + (client as any).attachConnectionHandlers(); + const register = vi.fn((_params: unknown) => ({ endpointId: "endpoint-1" })); + const close = vi.fn(() => null); + const dispose = vi.fn(() => null); + const send = vi.fn(() => null); + let nextConnection = 0; + server.onRequest("ahp.registerEndpoint", register); + server.onRequest("ahp.openConnection", () => ({ + connectionId: `connection-${++nextConnection}`, + })); + server.onRequest("ahp.closeConnection", close); + server.onRequest("ahp.disposeEndpoint", dispose); + server.onRequest("ahp.send", send); + server.onRequest("ahp.refreshExposure", () => null); + server.onRequest("ahp.setCapabilities", () => null); + server.onRequest("ping", () => ({ message: "pong" })); + sdk.listen(); + server.listen(); + onTestFinished(async () => { + await client.forceStop(); + server.dispose(); + toClient.destroy(); + toServer.destroy(); + }); + const message = (connectionId: string, text: string, token = CancellationToken.None) => + server.sendRequest( + "ahp.message", + { + endpointId: "endpoint-1", + connectionId, + message: text, + }, + token + ); + return { client, server, sdk, register, close, dispose, send, message, toClient }; +} + +describe("native AHP endpoint opaque bridge", () => { + it("reinstalls manual handlers after generated connection setup on reconnect", async () => { + const h = harness(); + const old = await h.client.createAhpEndpoint({ onListSessions: () => [] }); + const onClose = vi.fn(); + await old.openConnection({ onMessage: () => undefined, onClose }); + (h.client as any).attachConnectionHandlers(); + expect(onClose).toHaveBeenCalledWith( + expect.objectContaining({ message: "Runtime connection replaced" }) + ); + // Stand in for regenerated wrappers that do not forward CancellationToken. + h.sdk.onRequest("ahp.listSessions", () => { + throw new Error("No generated AHP callback handler"); + }); + const next = await h.client.createAhpEndpoint({ + onListSessions: ({ signal }) => { + expect(signal).toBeInstanceOf(AbortSignal); + return [{ sessionId: "after-reconnect" }]; + }, + }); + await expect( + h.server.sendRequest("ahp.listSessions", { endpointId: next.id }) + ).resolves.toEqual({ sessionIds: ["after-reconnect"] }); + await old.dispose(); + await next.dispose(); + expect(h.dispose).toHaveBeenCalledOnce(); + }); + + it("registers only flags and wire options; callbacks can call ordinary SDK without deadlock", async () => { + const h = harness(); + const callback = vi.fn(async () => { + expect(await h.client.ping()).toMatchObject({ message: "pong" }); + return { sessionId: "chosen" }; + }); + const endpoint = await h.client.createAhpEndpoint({ + onCreateSession: callback, + allowSessionCreation: false, + capabilities: { opaque: ["value"] }, + }); + await endpoint.openConnection({ onMessage: () => undefined }); + expect(h.register).toHaveBeenCalledWith( + { + callbacks: { + createSession: true, + resumeSession: false, + listSessions: false, + sessionControl: false, + }, + allowSessionCreation: false, + capabilities: { opaque: ["value"] }, + }, + expect.anything() + ); + await expect( + h.server.sendRequest("ahp.createSession", { + endpointId: "endpoint-1", + connectionId: "connection-1", + requestedSessionId: "requested", + config: { opaque: true }, + }) + ).resolves.toEqual({ sessionId: "chosen" }); + expect(callback).toHaveBeenCalledWith( + { + connectionId: "connection-1", + requestedSessionId: "requested", + config: { opaque: true }, + }, + { signal: expect.any(AbortSignal) } + ); + }); + + it("keeps local session configuration and tools on the ordinary SDK path", async () => { + const h = harness(); + const tool = vi.fn(() => "safe result"); + const permission = vi.fn(() => ({ kind: "denied-interactively-by-user" as const })); + const create = vi.fn((params) => ({ sessionId: params.sessionId })); + h.server.onRequest("session.create", create); + const config = { + sessionId: "local", + name: "Persisted application session", + systemMessage: { mode: "replace" as const, content: "Distinct application prompt" }, + tools: [{ name: "local_tool", description: "Safe", handler: tool }], + onPermissionRequest: permission, + }; + const endpoint = await h.client.createAhpEndpoint({ + onCreateSession: () => h.client.createSession(config), + }); + await endpoint.openConnection({ onMessage: () => undefined }); + await expect( + h.server.sendRequest("ahp.createSession", { + endpointId: "endpoint-1", + connectionId: "connection-1", + requestedSessionId: "requested", + }) + ).resolves.toEqual({ sessionId: "local" }); + expect(create).toHaveBeenCalledTimes(1); + const wire = create.mock.calls[0][0]; + expect(wire.name).toBe(config.name); + expect(wire.systemMessage).toEqual(config.systemMessage); + expect(wire.tools[0]).toMatchObject({ name: "local_tool" }); + expect(wire.tools[0].handler).toBeUndefined(); + expect(config.tools[0].handler).toBe(tool); + const session = (h.client as any).sessions.get("local"); + expect(session.toolHandlers.get("local_tool")).toBe(tool); + expect(config.onPermissionRequest).toBe(permission); + }); + + it("maps list, resume and control callbacks without creating a session again", async () => { + const h = harness(); + const create = vi.fn(); + h.server.onRequest("session.create", create); + const endpoint = await h.client.createAhpEndpoint({ + onListSessions: () => [{ sessionId: "visible" }], + onResumeSession: ({ sessionId }) => ({ sessionId }), + onSessionControl: ({ kind, payload }) => ({ applied: true, result: { kind, payload } }), + }); + await endpoint.openConnection({ onMessage: () => undefined }); + await expect( + h.server.sendRequest("ahp.listSessions", { endpointId: "endpoint-1" }) + ).resolves.toEqual({ sessionIds: ["visible"] }); + await expect( + h.server.sendRequest("ahp.resumeSession", { + endpointId: "endpoint-1", + connectionId: "connection-1", + sessionId: "visible", + }) + ).resolves.toEqual({ sessionId: "visible" }); + await expect( + h.server.sendRequest("ahp.sessionControl", { + endpointId: "endpoint-1", + sessionId: "visible", + kind: "custom", + payload: null, + }) + ).resolves.toEqual({ applied: true, result: { kind: "custom", payload: null } }); + expect(create).not.toHaveBeenCalled(); + }); + + it("propagates callback rejection with no fallback and prevents recursive AHP operations", async () => { + const h = harness(); + let recursive = false; + const endpoint = await h.client.createAhpEndpoint({ + onListSessions: async () => { + if (recursive) await endpoint.refreshExposure(); + throw new Error("Application policy denied"); + }, + }); + await expect( + h.server.sendRequest("ahp.listSessions", { endpointId: endpoint.id }) + ).rejects.toThrow("Application policy denied"); + recursive = true; + await expect( + h.server.sendRequest("ahp.listSessions", { endpointId: endpoint.id }) + ).rejects.toThrow("Recursive AHP"); + }); + + it.each(["createSession", "resumeSession"] as const)( + "rejects an in-transit %s callback when its connection closes before reader dispatch", + async (method) => { + const h = harness(); + const sessionRpc = vi.fn((params: { sessionId: string }) => ({ + sessionId: params.sessionId, + })); + h.server.onRequest( + method === "createSession" ? "session.create" : "session.resume", + sessionRpc + ); + const callback = vi.fn(() => + method === "createSession" + ? h.client.createSession({ sessionId: "ghost" }) + : h.client.resumeSession("ghost", {}) + ); + const endpoint = await h.client.createAhpEndpoint( + method === "createSession" + ? { onCreateSession: callback } + : { onResumeSession: callback } + ); + const connection = await endpoint.openConnection({ onMessage: () => undefined }); + const response = h.server + .sendRequest(`ahp.${method}`, { + endpointId: endpoint.id, + connectionId: connection.id, + ...(method === "createSession" + ? { requestedSessionId: "ghost" } + : { sessionId: "ghost" }), + }) + .catch((error: Error) => error); + // close() removes the registration synchronously, before the paired + // reader dispatches the already-sent callback request. + await connection.close(); + expect(await response).toMatchObject({ message: expect.stringContaining("closed") }); + expect(callback).not.toHaveBeenCalled(); + expect(sessionRpc).not.toHaveBeenCalled(); + } + ); + + it.each(["cancel", "dispose", "close", "stop", "eof"] as const)( + "aborts callbacks and rejects late completion on %s", + async (action) => { + const h = harness(); + const started = deferred(); + const blocked = deferred<{ sessionId: string }>(); + const endpoint = await h.client.createAhpEndpoint({ + onCreateSession: (_request, { signal }) => { + started.resolve(signal); + return blocked.promise; + }, + }); + const connection = await endpoint.openConnection({ onMessage: () => undefined }); + const source = new CancellationTokenSource(); + const request = h.server.sendRequest( + "ahp.createSession", + { + endpointId: endpoint.id, + connectionId: connection.id, + requestedSessionId: "requested", + }, + source.token + ); + // Observe rejection immediately, including EOF where the peer is disposed below. + const result = request.catch((error: Error) => error); + const signal = await started.promise; + if (action === "cancel") source.cancel(); + if (action === "dispose") await endpoint.dispose(); + if (action === "close") await connection.close(); + if (action === "stop") await h.client.stop(); + if (action === "eof") h.toClient.end(); + await vi.waitFor(() => expect(signal.aborted).toBe(true)); + if (action === "stop" || action === "eof") h.server.dispose(); + expect(await result).toBeInstanceOf(Error); + blocked.resolve({ sessionId: "too-late" }); + source.dispose(); + } + ); + + it("preserves opaque messages and string, null, and numeric AHP IDs verbatim", async () => { + const h = harness(); + const received: string[] = []; + const endpoint = await h.client.createAhpEndpoint(); + const connection = await endpoint.openConnection({ + onMessage: (text) => { + received.push(text); + }, + }); + const frames = [ + '{"jsonrpc":"2.0","id":"opaque:id","method":"x"}', + '{ "jsonrpc": "2.0", "id": null, "result": {} }', + '{"jsonrpc":"2.0","id":9007199254740993,"result":"unchanged"}', + "not parsed by the SDK", + ]; + for (const frame of frames) { + await connection.send(frame); + await expect(h.message(connection.id, frame)).resolves.toBeNull(); + } + await vi.waitFor(() => expect(received).toEqual(frames)); + expect(h.send.mock.calls.map((call: any) => call[0].message)).toEqual(frames); + }); + + it("serializes delivery independently without blocking the global reader", async () => { + const h = harness(); + const blocked = deferred(); + const delivered = deferred(); + const first = vi.fn(() => blocked.promise); + const endpoint = await h.client.createAhpEndpoint(); + const a = await endpoint.openConnection({ onMessage: first }); + const b = await endpoint.openConnection({ onMessage: () => delivered.resolve() }); + let firstConnectionAcknowledged = false; + const pending = Promise.all([h.message(a.id, "a1"), h.message(a.id, "a2")]).then(() => { + firstConnectionAcknowledged = true; + }); + await h.message(b.id, "b1"); + await delivered.promise; + expect(first).toHaveBeenCalledTimes(1); + expect(firstConnectionAcknowledged).toBe(false); + await expect(h.client.ping()).resolves.toMatchObject({ message: "pong" }); + blocked.resolve(); + await pending; + await vi.waitFor(() => expect(first).toHaveBeenCalledTimes(2)); + }); + + it.each([ + "cancel-active", + "cancel-queued", + "close", + "dispose", + "native-close", + "native-endpoint-close", + "stop", + "eof", + ] as const)("rejects active and queued output ACKs on %s", async (action) => { + const h = harness(); + const started = deferred(); + const blocked = deferred(); + const source = new CancellationTokenSource(); + const endpoint = await h.client.createAhpEndpoint({ onListSessions: () => [] }); + const onMessage = vi.fn(() => { + started.resolve(); + return blocked.promise; + }); + const onClose = vi.fn(); + const connection = await endpoint.openConnection({ onMessage, onClose }); + const active = h + .message( + connection.id, + "active", + action === "cancel-active" ? source.token : CancellationToken.None + ) + .catch((error: Error) => error); + const queued = h + .message( + connection.id, + "queued", + action === "cancel-queued" ? source.token : CancellationToken.None + ) + .catch((error: Error) => error); + await started.promise; + // A later request is a reader barrier: both messages are admitted, even + // though the active application callback and both ACKs are still pending. + await h.server.sendRequest("ahp.listSessions", { endpointId: endpoint.id }); + if (action.startsWith("cancel")) source.cancel(); + if (action === "close") await connection.close(); + if (action === "dispose") await endpoint.dispose(); + if (action === "native-close") { + await h.server.sendNotification("ahp.connectionClosed", { + endpointId: endpoint.id, + connectionId: connection.id, + }); + } + if (action === "native-endpoint-close") { + await h.server.sendNotification("ahp.endpointClosed", { endpointId: endpoint.id }); + } + if (action === "stop") await h.client.stop(); + if (action === "eof") h.toClient.end(); + await vi.waitFor(() => expect(onClose).toHaveBeenCalledOnce()); + if (action === "stop" || action === "eof") h.server.dispose(); + for (const result of await Promise.all([active, queued])) { + expect(result).toBeInstanceOf(Error); + if (action.startsWith("cancel")) expect(result).toMatchObject({ code: -32800 }); + } + expect(onMessage).toHaveBeenCalledOnce(); + blocked.resolve(); + await connection.close(); + if (action !== "stop" && action !== "eof") { + await expect(h.message(connection.id, "late")).rejects.toThrow("closed"); + await h.client.ping(); + } + source.dispose(); + }); + + it.each([ + { name: "message", limits: { maxMessageBytes: 3 }, frames: ["💡"] }, + { name: "count", limits: { maxQueuedMessages: 1 }, frames: ["a", "b"] }, + { name: "bytes", limits: { maxBufferedBytes: 3 }, frames: ["ab", "cd"] }, + ])("enforces $name bounds in both directions", async ({ limits, frames }) => { + const h = harness(); + const blocked = deferred(); + h.server.onRequest("ahp.send", () => blocked.promise); + const endpoint = await h.client.createAhpEndpoint({ limits }); + const inputClosed = vi.fn(); + const outputClosed = vi.fn(); + const a = await endpoint.openConnection({ + onMessage: () => undefined, + onClose: inputClosed, + }); + const b = await endpoint.openConnection({ + onMessage: () => blocked.promise.then(() => undefined), + onClose: outputClosed, + }); + const pending = frames.map((frame) => a.send(frame).catch((error: Error) => error)); + const outputPending = frames.map((frame) => + h.message(b.id, frame).catch((error: Error) => error) + ); + await vi.waitFor(() => { + expect(inputClosed).toHaveBeenCalledOnce(); + expect(outputClosed).toHaveBeenCalledOnce(); + expect(h.close).toHaveBeenCalledTimes(2); + }); + for (const result of await Promise.all(pending)) expect(result).toBeInstanceOf(Error); + for (const result of await Promise.all(outputPending)) expect(result).toBeInstanceOf(Error); + expect(inputClosed.mock.calls[0][0]).toBeInstanceOf(Error); + blocked.resolve(null); + }); + + it("closes on output callback failure, not the endpoint or an unrelated owner session", async () => { + const h = harness(); + const disconnect = vi.fn(); + h.server.onRequest("session.disconnect", disconnect); + h.server.onRequest("session.create", (params: { sessionId: string }) => ({ + sessionId: params.sessionId, + })); + const owner = await h.client.createSession({ + sessionId: "unrelated-owner", + onPermissionRequest: () => ({ kind: "no-result" }), + }); + const ownerDisconnect = vi.spyOn(owner, "disconnect"); + const endpoint = await h.client.createAhpEndpoint(); + const onClose = vi.fn(); + const connection = await endpoint.openConnection({ + onMessage: () => Promise.reject(new Error("socket write failed")), + onClose, + }); + await expect(h.message(connection.id, "opaque")).rejects.toThrow("socket write failed"); + await vi.waitFor(() => + expect(onClose).toHaveBeenCalledWith( + expect.objectContaining({ message: "socket write failed" }) + ) + ); + await connection.close(); + await endpoint.refreshExposure(); + await endpoint.dispose(); + expect(disconnect).not.toHaveBeenCalled(); + expect(ownerDisconnect).not.toHaveBeenCalled(); + }); + + it("close/dispose are idempotent and forward exposure/capabilities", async () => { + const h = harness(); + const refresh = vi.fn((_params: unknown) => null); + const capabilities = vi.fn((_params: unknown) => null); + h.server.onRequest("ahp.refreshExposure", refresh); + h.server.onRequest("ahp.setCapabilities", capabilities); + const endpoint = await h.client.createAhpEndpoint(); + expect(h.register.mock.calls[0][0]).toEqual({ + callbacks: { + createSession: false, + resumeSession: false, + listSessions: false, + sessionControl: false, + }, + }); + const onClose = vi.fn(); + const connection = await endpoint.openConnection({ onMessage: () => undefined, onClose }); + await endpoint.refreshExposure(); + await endpoint.setCapabilities({ custom: true }); + expect(refresh.mock.calls[0][0]).toEqual({ endpointId: endpoint.id }); + expect(capabilities.mock.calls[0][0]).toEqual({ + endpointId: endpoint.id, + capabilities: { custom: true }, + }); + await Promise.all([connection.close(), connection.close()]); + await Promise.all([endpoint.dispose(), endpoint.dispose()]); + expect(h.close).toHaveBeenCalledOnce(); + expect(h.dispose).toHaveBeenCalledOnce(); + expect(onClose).toHaveBeenCalledOnce(); + await expect(connection.send("late")).rejects.toThrow("closed"); + await expect(endpoint.openConnection({ onMessage: () => undefined })).rejects.toThrow( + "closed" + ); + }); + + it("locally tears down even when remote disposal fails", async () => { + const h = harness(); + h.server.onRequest("ahp.disposeEndpoint", () => { + throw new Error("runtime gone"); + }); + const endpoint = await h.client.createAhpEndpoint(); + const onClose = vi.fn(); + await endpoint.openConnection({ onMessage: () => undefined, onClose }); + await expect(endpoint.dispose()).rejects.toThrow("runtime gone"); + await expect(endpoint.dispose()).rejects.toThrow("runtime gone"); + expect(onClose).toHaveBeenCalledOnce(); + }); + + it("handles native close notifications without echoing cleanup or disconnecting sessions", async () => { + const h = harness(); + const endpoint = await h.client.createAhpEndpoint(); + const onClose = vi.fn(); + const a = await endpoint.openConnection({ onMessage: () => undefined, onClose }); + const b = await endpoint.openConnection({ onMessage: () => undefined, onClose }); + await h.server.sendNotification("ahp.connectionClosed", { + endpointId: endpoint.id, + connectionId: a.id, + error: "native overflow", + }); + await vi.waitFor(() => + expect(onClose).toHaveBeenCalledWith( + expect.objectContaining({ message: "native overflow" }) + ) + ); + await h.server.sendNotification("ahp.endpointClosed", { endpointId: endpoint.id }); + await vi.waitFor(() => expect(onClose).toHaveBeenCalledTimes(2)); + await Promise.all([a.close(), b.close(), endpoint.dispose()]); + expect(h.close).not.toHaveBeenCalled(); + expect(h.dispose).not.toHaveBeenCalled(); + }); + + it("rejects all queued sends when runtime admission fails", async () => { + const h = harness(); + const blocked = deferred(); + const started = deferred(); + h.server.onRequest("ahp.send", () => { + started.resolve(); + return blocked.promise; + }); + const endpoint = await h.client.createAhpEndpoint(); + const onClose = vi.fn(); + const connection = await endpoint.openConnection({ onMessage: () => undefined, onClose }); + const first = connection.send("first").catch((error: Error) => error); + const second = connection.send("second").catch((error: Error) => error); + await started.promise; + blocked.reject(new Error("native admission denied")); + expect(await first).toMatchObject({ + message: expect.stringContaining("native admission denied"), + }); + expect(await second).toMatchObject({ + message: expect.stringContaining("native admission denied"), + }); + await connection.close(); + expect(onClose).toHaveBeenCalledOnce(); + }); + + it.each([ + undefined, + { maxMessageBytes: 1024 * 1024, maxQueuedMessages: 128, maxBufferedBytes: 8 * 1024 * 1024 }, + ])("enforces hard limits with default or explicit ceilings: %j", async (limits) => { + const h = harness(); + const blocked = deferred(); + h.server.onRequest("ahp.send", () => blocked.promise); + const endpoint = await h.client.createAhpEndpoint({ limits }); + const large = await endpoint.openConnection({ onMessage: () => undefined }); + await expect(large.send("a".repeat(1024 * 1024 + 1))).rejects.toThrow("message size limit"); + const queued = await endpoint.openConnection({ onMessage: () => undefined }); + const pending = Array.from({ length: 128 }, () => + queued.send("a".repeat(64 * 1024)).catch((error: Error) => error) + ); + await expect(queued.send("a".repeat(64 * 1024))).rejects.toThrow("buffer limit"); + expect((await Promise.all(pending)).every((result) => result instanceof Error)).toBe(true); + const buffered = await endpoint.openConnection({ onMessage: () => undefined }); + const bytes = Array.from({ length: 8 }, () => + buffered.send("a".repeat(1024 * 1024)).catch((error: Error) => error) + ); + await expect(buffered.send("overflow")).rejects.toThrow("buffer limit"); + expect((await Promise.all(bytes)).every((result) => result instanceof Error)).toBe(true); + blocked.resolve(null); + await Promise.all([large.close(), queued.close(), buffered.close()]); + await h.client.ping(); + }); + + it("reports missing runtime methods clearly, preserving other errors", async () => { + const h = harness(); + h.server.onRequest("ahp.registerEndpoint", () => { + throw new ResponseError(-32601, "missing"); + }); + await expect(h.client.createAhpEndpoint()).rejects.toThrow("does not support native AHP"); + h.server.onRequest("ahp.registerEndpoint", () => { + throw new ResponseError(-32000, "policy denied"); + }); + await expect(h.client.createAhpEndpoint()).rejects.toThrow("policy denied"); + }); + + it("validates limits before registering", async () => { + const h = harness(); + await expect( + h.client.createAhpEndpoint({ limits: { maxMessageBytes: 0 } } as AhpEndpointOptions) + ).rejects.toThrow("positive safe integers"); + expect(h.register).not.toHaveBeenCalled(); + }); + + it.each([ + { maxMessageBytes: 1024 * 1024 + 1 }, + { maxQueuedMessages: 129 }, + { maxBufferedBytes: 8 * 1024 * 1024 + 1 }, + { maxQueuedMessages: 256, maxBufferedBytes: 16 * 1024 * 1024 }, + ])("rejects configuration above hard ceilings before registration: %j", async (limits) => { + const h = harness(); + await expect(h.client.createAhpEndpoint({ limits })).rejects.toThrow("cannot exceed"); + expect(h.register).not.toHaveBeenCalled(); + }); +}); diff --git a/nodejs/test/fixtures/ahp-drive-agent.mjs b/nodejs/test/fixtures/ahp-drive-agent.mjs new file mode 100644 index 0000000000..1de5997a08 --- /dev/null +++ b/nodejs/test/fixtures/ahp-drive-agent.mjs @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// Cross-language E2E peer: use the standard AHP client, not the SDK session API. +import assert from "node:assert/strict"; +import { randomUUID } from "node:crypto"; +import { ActionType, MessageKind, ResponsePartKind } from "@microsoft/agent-host-protocol"; +import { AhpClient, AhpStateMirror, RpcError } from "@microsoft/agent-host-protocol/client"; +import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws"; + +const [url, existingId, excludedId] = process.argv.slice(2); +const channel = `ahp-session:/${existingId || randomUUID()}`; +const deadline = setTimeout(() => { + console.error("AHP agent turn timed out"); + process.exit(1); +}, 60_000); + +async function connect() { + const client = new AhpClient(await WebSocketTransport.connect(url)); + client.connect(); + await client.initialize({ clientId: randomUUID(), protocolVersions: ["0.9.0"] }); + return client; +} + +try { + const client = await connect(); + try { + const listed = await client.request("listSessions", { channel: "ahp-root://" }); + assert(!listed.items.some((item) => item.resource === `ahp-session:/${excludedId}`)); + await assert.rejects(client.subscribe(`ahp-session:/${excludedId}`), RpcError); + if (!existingId) { + await client.request("createSession", { channel, provider: "copilot" }); + } + const { result } = await client.subscribe(channel); + const mirror = new AhpStateMirror(); + mirror.applySnapshot(result.snapshot); + const chat = mirror.getSession(channel).defaultChat; + assert(chat); + const { subscription } = await client.subscribe(chat); + const turnId = randomUUID(); + const parts = new Map(); + let completedTool = false; + const tools = new Set(); + client.dispatch(chat, { + type: ActionType.ChatTurnStarted, + turnId, + startedAt: new Date().toISOString(), + message: { + text: "Use encrypt_string to encrypt this string: Hello", + origin: { kind: MessageKind.User }, + }, + }); + let completed = false; + for await (const event of subscription) { + if (event.type !== "action") continue; + const action = event.params.action; + if (action.turnId !== turnId) continue; + if ( + action.type === ActionType.ChatResponsePart && + action.part.kind === ResponsePartKind.Markdown + ) { + parts.set(action.part.id, action.part.content); + } else if (action.type === ActionType.ChatDelta) { + parts.set(action.partId, (parts.get(action.partId) ?? "") + action.content); + } else if ( + action.type === ActionType.ChatToolCallStart && + action.toolName === "encrypt_string" + ) { + tools.add(action.toolCallId); + } else if ( + action.type === ActionType.ChatToolCallComplete && + tools.has(action.toolCallId) + ) { + assert(action.result.success, JSON.stringify(action.result)); + completedTool = true; + } else if (action.type === ActionType.ChatError) { + assert.fail(JSON.stringify(action.part)); + } else if (action.type === ActionType.ChatTurnComplete) { + completed = true; + break; + } + } + assert(completed, "AHP stream closed before turn completion"); + assert(completedTool, "Rust application tool did not complete over AHP"); + assert.match([...parts.values()].join(""), /HELLO/); + } finally { + await client.shutdown(); + } + + const reconnected = await connect(); + try { + const { result } = await reconnected.subscribe(channel); + const mirror = new AhpStateMirror(); + mirror.applySnapshot(result.snapshot); + const chat = await reconnected.subscribe(mirror.getSession(channel).defaultChat); + assert.match(JSON.stringify(chat.result.snapshot), /HELLO/); + } finally { + await reconnected.shutdown(); + } + console.log("AHP_TOOL_TURN_AND_RECONNECT_OK"); +} finally { + clearTimeout(deadline); +} diff --git a/nodejs/tsconfig.test.json b/nodejs/tsconfig.test.json index 8d24d6dfd8..a99388ac4f 100644 --- a/nodejs/tsconfig.test.json +++ b/nodejs/tsconfig.test.json @@ -7,6 +7,7 @@ }, "include": [ "src/**/*", + "test/ahp.test.ts", "test/dependency-policy.test.ts", "test/ffiRuntimeHost.test.ts", "test/session-event-types.test.ts", diff --git a/rust/README.md b/rust/README.md index 11d9637b22..76a639af32 100644 --- a/rust/README.md +++ b/rust/README.md @@ -53,6 +53,111 @@ The SDK manages the CLI process lifecycle: spawning, health-checking, and gracef ## API Reference +### Native AHP endpoints (experimental) + +`Client::create_ahp_endpoint()` exposes the existing runtime through an +application-owned transport. The Rust SDK forwards opaque AHP 0.9 messages; the +runtime implements the agent protocol. It does not start another runtime or a +public listener. This requires the matching runtime from +[copilot-agent-runtime#21650](https://github.com/github/copilot-agent-runtime/pull/21650), +not an older published CLI without `ahp.*` support. + +```rust,no_run +use std::sync::Arc; +use github_copilot_sdk::{Client, Error}; +use github_copilot_sdk::ahp::{ + AhpConnectionOptions, AhpEndpointOptions, AhpSessionIdentity, +}; + +# async fn example(client: &Client, authorized_session_id: String) -> Result<(), Error> { +let endpoint = client.create_ahp_endpoint(AhpEndpointOptions { + allow_session_creation: Some(false), + on_list_sessions: Some(Arc::new(move |(), _context| { + let session_id = authorized_session_id.clone(); + Box::pin(async move { Ok(vec![AhpSessionIdentity { session_id }]) }) + })), + ..Default::default() +}).await?; + +// For each authenticated physical connection: +let connection = endpoint.open_connection(AhpConnectionOptions { + on_message: Arc::new(|text| Box::pin(async move { + // Await the application's socket write/flush here, not an unbounded enqueue. + # let _ = text; + Ok(()) + })), + on_close: Some(Arc::new(|_error| { + // Close the application's physical connection. + })), +}).await?; +// Forward each incoming text message with connection.send(text).await?. +connection.close().await?; +endpoint.dispose().await?; +# Ok(()) +# } +``` + +Create/resume callbacks return `AhpSessionIdentity` for ordinary SDK sessions. +They can call `client.create_session()` or `client.resume_session()` reentrantly; +recursive endpoint operations are rejected. Keep those session handles alive as +usual. Policy futures are cancelled when their request or owning connection, +endpoint, or client closes; `AhpCallbackContext::cancellation` also lets application +work observe that lifetime. Cancellation does not undo an already-created session. +`on_session_control` returns an explicit applied/refused outcome, including an +optional result for the requesting client. + +**`on_list_sessions` is endpoint-wide authorization**, enforced by the runtime +for subscriptions, history, actions, and disposal, including direct session URIs. +Use `refresh_exposure()` after policy changes and `set_capabilities()` to replace +the capability catalogue. Omitted callbacks preserve native runtime defaults. +The application must authenticate and secure its physical listener. + +`send()` acknowledges runtime admission, not the eventual AHP response. +`on_message` is serialized in wire order per logical connection and acknowledges +physical delivery. It must complete after the write finishes. Other connections +and ordinary SDK operations remain independent. Default bounds in each direction +are 1 MiB per UTF-8 message, 128 outstanding messages, and 8 MiB buffered bytes, +including in-flight deliveries. `AhpLimits` can tighten these bounds. Overflow or +delivery failure closes only the affected logical connection; `on_close` fires +once and should return promptly. + +Explicitly close connections and dispose the endpoint when the listener shuts +down. Dropping a cloned handle alone does not dispose an endpoint. Callbacks can +retain `Client` clones, so use `dispose()`/`stop()` rather than relying on reference +counting for cleanup. Disposal leaves application-owned sessions alive; stopping +the client also disconnects its session handles. + +#### Recorded AHP end-to-end coverage + +`tests/e2e/ahp.rs` hosts an application-owned Rust WebSocket listener and launches +the official `@microsoft/agent-host-protocol` client. It covers both an existing +session and a callback-created session: a real agent turn invokes a Rust tool, +excluded-session access is rejected, and reconnect restores the response. +Model exchanges use the ordinary CapiProxy record-replay harness and the existing +`test/snapshots/tools/invokes_custom_tool.yaml` recording, not fabricated AHP +responses. + +From the repository root, after building the matching runtime: + +```sh +npm --prefix nodejs ci +npm --prefix test/harness ci +cd rust +COPILOT_CLI_PATH=/path/to/copilot-agent-runtime/dist-cli/index.js \ + cargo test --no-default-features --features test-support --test e2e ahp:: +``` + +The Rust wire types and RPC methods are generated from that same runtime's +schemas. To regenerate from a local checkout (paths relative to the SDK root): + +```sh +scripts/codegen/node_modules/.bin/tsx scripts/codegen/rust.ts \ + ../copilot-agent-runtime/generated/session-events.schema.json \ + ../copilot-agent-runtime/generated/api.schema.json +cd rust +cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml +``` + ### Client ```rust,ignore diff --git a/rust/src/ahp.rs b/rust/src/ahp.rs new file mode 100644 index 0000000000..f82c6f45f0 --- /dev/null +++ b/rust/src/ahp.rs @@ -0,0 +1,935 @@ +//! Transport-neutral native Agent Host Protocol endpoints. +//! +//! Applications own the public listener and forward opaque AHP 0.9 messages. +//! The runtime owns the agent mapping. An output callback must finish only after +//! transport delivery: its return value is the runtime's delivery acknowledgement. +//! This experimental API requires a runtime implementing the native `ahp.*` RPCs. + +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Weak}; + +use futures_util::FutureExt; +use futures_util::future::{BoxFuture, Shared}; +use parking_lot::Mutex; +use serde_json::Value; +use tokio::sync::{Mutex as AsyncMutex, OwnedSemaphorePermit, Semaphore, oneshot}; +use tokio_util::sync::CancellationToken; +use wire::rpc_methods; + +use crate::generated::api_types as wire; +use crate::jsonrpc::JsonRpcMessage; +use crate::{ + Client, ClientInner, Error, ErrorKind, JsonRpcError, JsonRpcRequest, JsonRpcResponse, Result, +}; + +tokio::task_local! { + static IN_CALLBACK: (); +} + +/// Cancellation of an application policy callback. +#[derive(Clone)] +pub struct AhpCallbackContext { + /// Cancelled when the request, logical connection, endpoint, or client closes. + pub cancellation: CancellationToken, +} + +/// Identity returned by application session policy. +#[derive(Clone, Debug)] +pub struct AhpSessionIdentity { + /// ID of the ordinary SDK session to expose. + pub session_id: String, +} + +/// Native request to create an application-owned session. +#[derive(Clone, Debug)] +pub struct AhpCreateSessionRequest { + /// Logical AHP connection requesting creation. + pub connection_id: String, + /// Runtime-validated ID to use when creating the SDK session. + pub requested_session_id: String, + /// Requested workspace, subject to application policy. + pub working_directory: Option, + /// Requested model, subject to application policy. + pub model: Option, + /// Opaque AHP configuration. + pub config: Option, +} + +/// Native request to resume an application-owned session. +#[derive(Clone, Debug)] +pub struct AhpResumeSessionRequest { + /// Logical AHP connection requesting resume. + pub connection_id: String, + /// ID of the session to resume. + pub session_id: String, +} + +/// Application-owned session control request. +#[derive(Clone, Debug)] +pub struct AhpSessionControlRequest { + /// Target SDK session. + pub session_id: String, + /// Native control operation. + pub kind: String, + /// Opaque operation arguments. + pub payload: Value, +} + +/// Outcome of an application-owned control operation. +#[derive(Clone, Debug, Default)] +pub struct AhpSessionControlResult { + /// Whether the requested change was applied. + pub applied: bool, + /// Explanation of a refusal. + pub reason: Option, + /// Explicit operation result, including outcomes that refuse a change. + pub result: Option, +} + +/// Asynchronous application policy callback. Ordinary SDK calls are reentrant; +/// recursive AHP endpoint operations from a policy callback are rejected. +pub type AhpCallback = + Arc BoxFuture<'static, Result> + Send + Sync>; + +/// Per-direction bounds, including messages waiting for and undergoing delivery. +#[derive(Clone, Copy, Debug)] +pub struct AhpLimits { + /// Maximum UTF-8 message size; at most 1 MiB. + pub max_message_bytes: usize, + /// Maximum queued and in-flight messages; at most 128. + pub max_queued_messages: usize, + /// Maximum queued and in-flight UTF-8 bytes; at most 8 MiB. + pub max_buffered_bytes: usize, +} + +impl Default for AhpLimits { + fn default() -> Self { + Self { + max_message_bytes: 1024 * 1024, + max_queued_messages: 128, + max_buffered_bytes: 8 * 1024 * 1024, + } + } +} + +impl AhpLimits { + fn validate(self) -> Result<()> { + let max = Self::default(); + if self.max_message_bytes == 0 + || self.max_message_bytes > max.max_message_bytes + || self.max_queued_messages == 0 + || self.max_queued_messages > max.max_queued_messages + || self.max_buffered_bytes == 0 + || self.max_buffered_bytes > max.max_buffered_bytes + { + return Err(invalid( + "AHP limits must be positive and cannot exceed native limits", + )); + } + Ok(()) + } +} + +/// Endpoint-local session policy. Omitted callbacks use runtime defaults. +#[derive(Default)] +pub struct AhpEndpointOptions { + /// Whether clients may create sessions. + pub allow_session_creation: Option, + /// Opaque root capabilities. + pub capabilities: Option, + /// Create an ordinary SDK session and return its identity. + pub on_create_session: Option>, + /// Resume an ordinary SDK session and return its identity. + pub on_resume_session: Option>, + /// Authorize the endpoint's session set for all AHP operations. + pub on_list_sessions: Option>>, + /// Handle application-owned controls such as disposal. + pub on_session_control: Option>, + /// Optional tighter bounds than the runtime defaults. + pub limits: AhpLimits, +} + +/// Application transport callbacks for one independent logical connection. +pub struct AhpConnectionOptions { + /// Deliver one opaque message; resolve after physical delivery, not enqueueing. + pub on_message: Arc BoxFuture<'static, Result<()>> + Send + Sync>, + /// Terminal notification, with an error message on abnormal closure. + pub on_close: Option) + Send + Sync>>, +} + +/// Cloneable native endpoint handle. Explicitly dispose it when its listener closes. +#[derive(Clone)] +pub struct AhpEndpoint(Arc); + +/// Cloneable logical connection. Closing it does not close its SDK session. +#[derive(Clone)] +pub struct AhpConnection(Arc); + +type Cleanup = Shared>>; + +struct Endpoint { + id: String, + client: Weak, + options: AhpEndpointOptions, + closed: CancellationToken, + finished: AtomicBool, + connections: Mutex>>, + cleanup: Mutex>, +} + +struct Connection { + id: String, + endpoint: Weak, + options: AhpConnectionOptions, + closed: CancellationToken, + finished: AtomicBool, + input: Budget, + output: Budget, + sending: AsyncMutex<()>, + delivery_tail: Mutex>>, + cleanup: Mutex>, +} + +struct Budget { + messages: Arc, + bytes: Arc, + max_message_bytes: usize, +} + +impl Budget { + fn new(limits: AhpLimits) -> Self { + Self { + messages: Arc::new(Semaphore::new(limits.max_queued_messages)), + bytes: Arc::new(Semaphore::new(limits.max_buffered_bytes)), + max_message_bytes: limits.max_message_bytes, + } + } + + fn reserve(&self, message: &str) -> Result<(OwnedSemaphorePermit, OwnedSemaphorePermit)> { + if message.len() > self.max_message_bytes { + return Err(invalid("AHP message size limit exceeded")); + } + let count = self + .messages + .clone() + .try_acquire_owned() + .map_err(|_| invalid("AHP connection buffer limit exceeded"))?; + let bytes = self + .bytes + .clone() + .try_acquire_many_owned(message.len() as u32) + .map_err(|_| invalid("AHP connection buffer limit exceeded"))?; + Ok((count, bytes)) + } +} + +fn invalid(message: impl Into) -> Error { + Error::with_message(ErrorKind::InvalidConfig, message.into()) +} + +fn closed() -> Error { + invalid("AHP endpoint or connection is closed") +} + +fn assert_not_callback() -> Result<()> { + if IN_CALLBACK.try_with(|_| ()).is_ok() { + return Err(invalid( + "Recursive AHP endpoint operations from an AHP callback are not supported", + )); + } + Ok(()) +} + +fn client(inner: &Weak) -> Result { + inner.upgrade().map(Client::from_inner).ok_or_else(closed) +} + +fn check_response(response: JsonRpcResponse) -> Result { + if let Some(error) = response.error { + return Err(Error::with_message( + ErrorKind::Rpc { code: error.code }, + if error.code == -32601 { + "This runtime does not support native AHP endpoints. Use a matching runtime with ahp.* support.".into() + } else { + error.message + }, + )); + } + Ok(response.result.unwrap_or(Value::Null)) +} + +impl Client { + /// Register a transport-neutral AHP endpoint on this client's runtime. + /// + /// This does not start a listener or another runtime. The application owns + /// transport authentication and endpoint-local session policy. + pub async fn create_ahp_endpoint(&self, options: AhpEndpointOptions) -> Result { + assert_not_callback()?; + options.limits.validate()?; + self.inner.router.ensure_started( + &self.inner.notification_tx, + &self.inner.request_rx, + self.inner.extension_launch_provider.clone(), + self.inner.llm_inference.get().cloned(), + self.inner.on_github_telemetry.clone(), + self.inner.github_token_registry.clone(), + ); + let registry = self.inner.router.ahp.clone(); + *registry.client.lock() = Arc::downgrade(&self.inner); + let weak_registry = Arc::downgrade(®istry); + self.inner + .rpc + .set_server_message_handler(Box::new(move |message| { + let Some(registry) = weak_registry.upgrade() else { + return false; + }; + match message { + JsonRpcMessage::Request(request) if request.method.starts_with("ahp.") => { + registry.dispatch(request.clone()); + true + } + JsonRpcMessage::Notification(notification) + if notification.method.starts_with("ahp.") + || notification.method == "$/cancelRequest" => + { + registry.notification( + ¬ification.method, + notification.params.as_ref().unwrap_or(&Value::Null), + ); + false + } + _ => false, + } + })); + let params = serde_json::to_value(wire::AhpRegisterEndpointRequest { + allow_session_creation: options.allow_session_creation, + capabilities: options.capabilities.clone(), + callbacks: wire::AhpEndpointCallbacks { + create_session: options.on_create_session.is_some(), + resume_session: options.on_resume_session.is_some(), + list_sessions: options.on_list_sessions.is_some(), + session_control: options.on_session_control.is_some(), + }, + ..Default::default() + })?; + let registered = Arc::new(Mutex::new(None)); + let registered_inline = registered.clone(); + let weak_client = Arc::downgrade(&self.inner); + let closed_token = self.inner.rpc.connection_closed_token(); + let client = self.clone(); + let (result_tx, result_rx) = oneshot::channel(); + tokio::spawn(async move { + let result = async { + let response = client + .inner + .rpc + .send_request_with_inline_callback( + rpc_methods::AHP_REGISTERENDPOINT, + Some(params), + Some(Box::new(move |response| { + if response.error.is_some() { + return Ok(()); + } + let response: wire::AhpEndpointRef = serde_json::from_value( + response.result.clone().unwrap_or(Value::Null), + )?; + let id = response.endpoint_id; + let endpoint = Arc::new(Endpoint { + id: id.clone(), + client: weak_client, + options, + closed: closed_token, + finished: AtomicBool::new(false), + connections: Mutex::new(HashMap::new()), + cleanup: Mutex::new(None), + }); + registry.endpoints.lock().insert(id, endpoint.clone()); + let weak = Arc::downgrade(&endpoint); + let token = endpoint.closed.clone(); + tokio::spawn(async move { + token.cancelled().await; + if let Some(endpoint) = weak.upgrade() { + endpoint.finish(Some("AHP endpoint closed".into())); + } + }); + *registered_inline.lock() = Some(AhpEndpoint(endpoint)); + Ok(()) + })), + ) + .await?; + check_response(response)?; + registered.lock().take().ok_or_else(closed) + } + .await; + if let Err(Ok(endpoint)) = result_tx.send(result) + && let Err(error) = endpoint.dispose().await + { + tracing::warn!(%error, "Failed to dispose abandoned AHP endpoint"); + } + }); + result_rx.await.map_err(|_| closed())? + } +} + +impl AhpEndpoint { + /// Runtime-assigned endpoint ID. + pub fn id(&self) -> &str { + &self.0.id + } + + /// Open a logical AHP connection with independent ordering and backpressure. + pub async fn open_connection(&self, options: AhpConnectionOptions) -> Result { + assert_not_callback()?; + let endpoint = &self.0; + if endpoint.closed.is_cancelled() { + return Err(closed()); + } + let registered = Arc::new(Mutex::new(None)); + let registered_inline = registered.clone(); + let endpoint = endpoint.clone(); + let (result_tx, result_rx) = oneshot::channel(); + tokio::spawn(async move { + let result = async { + let response = client(&endpoint.client)? + .inner + .rpc + .send_request_with_inline_callback( + rpc_methods::AHP_OPENCONNECTION, + Some(serde_json::to_value(wire::AhpEndpointRef { + endpoint_id: endpoint.id.clone(), + })?), + Some(Box::new(move |response| { + if response.error.is_some() { + return Ok(()); + } + let response: wire::AhpOpenConnectionResult = serde_json::from_value( + response.result.clone().unwrap_or(Value::Null), + )?; + let id = response.connection_id; + let connection = Arc::new(Connection { + id: id.clone(), + endpoint: Arc::downgrade(&endpoint), + closed: endpoint.closed.child_token(), + finished: AtomicBool::new(false), + options, + input: Budget::new(endpoint.options.limits), + output: Budget::new(endpoint.options.limits), + sending: AsyncMutex::new(()), + delivery_tail: Mutex::new(None), + cleanup: Mutex::new(None), + }); + endpoint.connections.lock().insert(id, connection.clone()); + if endpoint.closed.is_cancelled() { + connection.finish(Some("AHP endpoint closed".into())); + return Err(closed()); + } + *registered_inline.lock() = Some(AhpConnection(connection)); + Ok(()) + })), + ) + .await?; + check_response(response)?; + registered.lock().take().ok_or_else(closed) + } + .await; + if let Err(Ok(connection)) = result_tx.send(result) + && let Err(error) = connection.close().await + { + tracing::warn!(%error, "Failed to close abandoned AHP connection"); + } + }); + result_rx.await.map_err(|_| closed())? + } + + /// Recompute and enforce the application-authorized session set. + pub async fn refresh_exposure(&self) -> Result<()> { + assert_not_callback()?; + self.0 + .run(async { + client(&self.0.client)? + .rpc() + .ahp() + .refresh_exposure(wire::AhpEndpointRef { + endpoint_id: self.0.id.clone(), + }) + .await + }) + .await?; + Ok(()) + } + + /// Replace the opaque root capability catalogue. + pub async fn set_capabilities(&self, capabilities: Value) -> Result<()> { + assert_not_callback()?; + self.0 + .run(async { + client(&self.0.client)? + .rpc() + .ahp() + .set_capabilities(wire::AhpSetCapabilitiesRequest { + endpoint_id: self.0.id.clone(), + capabilities, + }) + .await + }) + .await?; + Ok(()) + } + + /// Idempotently dispose the endpoint and cancel callbacks and deliveries. + /// Application-owned SDK sessions remain alive. + pub async fn dispose(&self) -> Result<()> { + assert_not_callback()?; + let cleanup = { + let mut cleanup = self.0.cleanup.lock(); + if let Some(future) = &*cleanup { + future.clone() + } else { + if self.0.closed.is_cancelled() { + return Ok(()); + } + let client = client(&self.0.client)?; + let id = self.0.id.clone(); + let future = async move { + client + .rpc() + .ahp() + .dispose_endpoint(wire::AhpEndpointRef { endpoint_id: id }) + .await + .map(|_| ()) + .map_err(|e| e.to_string()) + } + .boxed() + .shared(); + *cleanup = Some(future.clone()); + future + } + }; + self.0.finish(None); + cleanup.await.map_err(invalid) + } +} + +impl Endpoint { + async fn run(&self, work: impl std::future::Future>) -> Result { + if self.closed.is_cancelled() { + return Err(closed()); + } + tokio::select! { + biased; + _ = self.closed.cancelled() => Err(closed()), + result = work => result, + } + } + + fn finish(&self, error: Option) { + if self.finished.swap(true, Ordering::SeqCst) { + return; + } + self.closed.cancel(); + let connections = std::mem::take(&mut *self.connections.lock()); + for connection in connections.into_values() { + connection.finish(error.clone()); + } + if let Ok(client) = client(&self.client) { + client.inner.router.ahp.endpoints.lock().remove(&self.id); + } + } +} + +impl AhpConnection { + /// Runtime-assigned logical connection ID. + pub fn id(&self) -> &str { + &self.0.id + } + + /// Send an opaque UTF-8 message. Resolves on runtime admission, not an AHP response. + pub async fn send(&self, message: impl Into) -> Result<()> { + assert_not_callback()?; + let message = message.into(); + let connection = &self.0; + if connection.closed.is_cancelled() { + return Err(closed()); + } + let permits = match connection.input.reserve(&message) { + Ok(permits) => permits, + Err(error) => { + connection.fail(error.to_string()); + return Err(error); + } + }; + let result = async { + let _permits = permits; + let _guard = connection.sending.lock().await; + let endpoint = connection.endpoint.upgrade().ok_or_else(closed)?; + endpoint + .run(async { + client(&endpoint.client)? + .rpc() + .ahp() + .send(wire::AhpMessage { + endpoint_id: endpoint.id.clone(), + connection_id: connection.id.clone(), + message, + }) + .await + }) + .await?; + Ok(()) + }; + let result: Result<()> = tokio::select! { + biased; + _ = connection.closed.cancelled() => Err(closed()), + result = result => result, + }; + if let Err(error) = &result { + connection.fail(error.to_string()); + } + result + } + + /// Idempotently close this connection without closing its SDK sessions. + pub async fn close(&self) -> Result<()> { + assert_not_callback()?; + let cleanup = self.0.begin_close(None); + match cleanup { + Some(cleanup) => cleanup.await.map_err(invalid), + None => Ok(()), + } + } +} + +impl Connection { + fn finish(&self, error: Option) { + if self.finished.swap(true, Ordering::SeqCst) { + return; + } + self.endpoint + .upgrade() + .and_then(|endpoint| endpoint.connections.lock().remove(&self.id)); + self.closed.cancel(); + if let Some(callback) = &self.options.on_close + && std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(error))).is_err() + { + tracing::warn!("AHP on_close callback panicked"); + } + } + + fn begin_close(&self, error: Option) -> Option { + let mut cleanup = self.cleanup.lock(); + if let Some(future) = &*cleanup { + return Some(future.clone()); + } + if self.closed.is_cancelled() { + return None; + } + let endpoint = self.endpoint.upgrade()?; + let client = match client(&endpoint.client) { + Ok(client) => client, + Err(_) => { + self.finish(error); + return None; + } + }; + let params = wire::AhpConnectionRef { + endpoint_id: endpoint.id.clone(), + connection_id: self.id.clone(), + }; + let future = async move { + client + .rpc() + .ahp() + .close_connection(params) + .await + .map(|_| ()) + .map_err(|e| e.to_string()) + } + .boxed() + .shared(); + *cleanup = Some(future.clone()); + drop(cleanup); + self.finish(error); + Some(future) + } + + fn fail(&self, error: String) { + if let Some(cleanup) = self.begin_close(Some(error)) { + tokio::spawn(async move { + if let Err(error) = cleanup.await { + tracing::warn!(%error, "Failed to close native AHP connection"); + } + }); + } + } +} + +#[derive(Default)] +pub(crate) struct Registry { + client: Mutex>, + endpoints: Mutex>>, + requests: Mutex>, +} + +impl Registry { + pub(crate) fn clear(&self) { + let endpoints = std::mem::take(&mut *self.endpoints.lock()); + for endpoint in endpoints.into_values() { + endpoint.finish(None); + } + } + + pub(crate) fn notification(&self, method: &str, params: &Value) { + if method == "$/cancelRequest" { + if let Some(id) = params["id"].as_u64() + && let Some(token) = self.requests.lock().get(&id) + { + token.cancel(); + } + return; + } + let result = match method { + "ahp.endpointClosed" => serde_json::from_value::( + params.clone(), + ) + .map(|notification| { + let endpoint = self + .endpoints + .lock() + .get(¬ification.endpoint_id) + .cloned(); + if let Some(endpoint) = endpoint { + endpoint.finish(notification.error); + } + }), + "ahp.connectionClosed" => { + serde_json::from_value::(params.clone()).map( + |notification| { + let endpoint = self + .endpoints + .lock() + .get(¬ification.endpoint_id) + .cloned(); + if let Some(endpoint) = endpoint { + let connection = endpoint + .connections + .lock() + .get(¬ification.connection_id) + .cloned(); + if let Some(connection) = connection { + connection.finish(notification.error); + } + } + }, + ) + } + _ => return, + }; + if let Err(error) = result { + tracing::warn!(%error, %method, "Invalid AHP lifecycle notification"); + } + } + + pub(crate) fn dispatch(self: &Arc, request: JsonRpcRequest) { + let params = request.params.clone().unwrap_or(Value::Null); + let endpoint = params["endpointId"] + .as_str() + .and_then(|id| self.endpoints.lock().get(id).cloned()); + let Some(endpoint) = endpoint else { + let client = self.client.lock().upgrade().map(Client::from_inner); + if let Some(client) = client { + tokio::spawn(async move { + if let Err(error) = client + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".into(), + id: request.id, + result: None, + error: Some(JsonRpcError { + code: -32603, + message: closed().to_string(), + data: None, + }), + }) + .await + { + tracing::warn!(%error, "Failed to reject unknown AHP endpoint"); + } + }); + } + return; + }; + let connection = params["connectionId"] + .as_str() + .and_then(|id| endpoint.connections.lock().get(id).cloned()); + let token = connection + .as_ref() + .map_or_else(|| endpoint.closed.child_token(), |c| c.closed.child_token()); + self.requests.lock().insert(request.id, token.clone()); + let registry = self.clone(); + // Reserve before spawning: queued delivery tasks also count against the bound. + let delivery = if request.method == "ahp.message" { + Some(connection.as_ref().ok_or_else(closed).and_then(|c| { + let message: wire::AhpMessage = serde_json::from_value(params.clone())?; + let permits = c.output.reserve(&message.message)?; + let (done, next) = oneshot::channel(); + let previous = c.delivery_tail.lock().replace(next); + Ok((permits, previous, done, message.message)) + })) + } else { + None + }; + tokio::spawn(async move { + let mut delivery_guard = None; + let work = async { + if let Some(delivery) = delivery { + let (permits, previous, done, message) = delivery?; + delivery_guard = Some((permits, done)); + if let Some(previous) = previous { + let _ = previous.await; + } + let connection = connection.as_ref().ok_or_else(closed)?; + (connection.options.on_message)(message).await?; + return Ok(Value::Null); + } + if matches!( + request.method.as_str(), + "ahp.createSession" | "ahp.resumeSession" + ) && connection.is_none() + { + return Err(closed()); + } + let context = AhpCallbackContext { + cancellation: token.clone(), + }; + IN_CALLBACK + .scope((), async { + match request.method.as_str() { + "ahp.createSession" => { + let handler = endpoint + .options + .on_create_session + .as_ref() + .ok_or_else(|| invalid("No AHP create callback"))?; + let request: wire::AhpCreateSessionRequest = + serde_json::from_value(params)?; + let result = handler( + AhpCreateSessionRequest { + connection_id: request.connection_id, + requested_session_id: request.requested_session_id, + working_directory: request.working_directory, + model: request.model, + config: request.config, + }, + context, + ) + .await?; + Ok(serde_json::to_value(wire::AhpSessionIdentity { + session_id: result.session_id.into(), + })?) + } + "ahp.resumeSession" => { + let handler = endpoint + .options + .on_resume_session + .as_ref() + .ok_or_else(|| invalid("No AHP resume callback"))?; + let request: wire::AhpResumeSessionRequest = + serde_json::from_value(params)?; + let result = handler( + AhpResumeSessionRequest { + connection_id: request.connection_id, + session_id: request.session_id.to_string(), + }, + context, + ) + .await?; + Ok(serde_json::to_value(wire::AhpSessionIdentity { + session_id: result.session_id.into(), + })?) + } + "ahp.listSessions" => { + let handler = endpoint + .options + .on_list_sessions + .as_ref() + .ok_or_else(|| invalid("No AHP list callback"))?; + let sessions = handler((), context).await?; + Ok(serde_json::to_value(wire::AhpListSessionsResult { + session_ids: sessions + .into_iter() + .map(|s| s.session_id) + .collect(), + })?) + } + "ahp.sessionControl" => { + let handler = endpoint + .options + .on_session_control + .as_ref() + .ok_or_else(|| invalid("No AHP control callback"))?; + let request: wire::AhpSessionControlRequest = + serde_json::from_value(params)?; + let result = handler( + AhpSessionControlRequest { + session_id: request.session_id.to_string(), + kind: request.kind, + payload: request.payload, + }, + context, + ) + .await?; + Ok(serde_json::to_value(wire::AhpSessionControlResult { + applied: result.applied, + reason: result.reason, + result: result.result, + })?) + } + _ => Err(invalid("Unknown AHP callback")), + } + }) + .await + }; + let result: Result = tokio::select! { + biased; + _ = token.cancelled() => Err(Error::with_message(ErrorKind::Rpc { code: -32800 }, "AHP callback cancelled")), + result = std::panic::AssertUnwindSafe(work).catch_unwind() => + result.unwrap_or_else(|_| Err(invalid("AHP callback panicked"))), + }; + registry.requests.lock().remove(&request.id); + if request.method == "ahp.message" + && let Err(error) = &result + && let Some(connection) = connection + { + connection.fail(error.to_string()); + } + if let Ok(client) = client(&endpoint.client) { + let (result, error) = match result { + Ok(result) => (Some(result), None), + Err(error) => ( + None, + Some(JsonRpcError { + code: match error.kind() { + ErrorKind::Rpc { code } => *code, + _ => -32603, + }, + message: error.to_string(), + data: None, + }), + ), + }; + if let Err(error) = client + .send_response(&JsonRpcResponse { + jsonrpc: "2.0".into(), + id: request.id, + result, + error, + }) + .await + { + tracing::warn!(%error, "Failed to send AHP callback response"); + } + } + drop(delivery_guard); + }); + } +} diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 3e8b45a72b..6cb211d297 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -10,12 +10,12 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; pub use super::session_events::{ - AbortReason, AgentModelPolicy, AutoTier, ContextTier, McpOauthHttpResponse, - McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, - ModelChangeSource, OmittedBinaryOmittedReason, PermissionDecisionSource, PermissionMode, - PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, - SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, - UserToolSessionApproval, Verbosity, + AbortReason, AgentModelPolicy, AutoTier, ContextTier, ManagedSettingsResolvedSource, + McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, + McpServerStatus, ModelChangeSource, OmittedBinaryOmittedReason, PermissionDecisionSource, + PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, + SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskBlocker, + TaskCompletionOutcome, UserToolSessionApproval, Verbosity, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -25,6 +25,20 @@ pub mod rpc_methods { pub const PING: &str = "ping"; /// `connect` pub const CONNECT: &str = "connect"; + /// `ahp.registerEndpoint` + pub const AHP_REGISTERENDPOINT: &str = "ahp.registerEndpoint"; + /// `ahp.openConnection` + pub const AHP_OPENCONNECTION: &str = "ahp.openConnection"; + /// `ahp.send` + pub const AHP_SEND: &str = "ahp.send"; + /// `ahp.closeConnection` + pub const AHP_CLOSECONNECTION: &str = "ahp.closeConnection"; + /// `ahp.disposeEndpoint` + pub const AHP_DISPOSEENDPOINT: &str = "ahp.disposeEndpoint"; + /// `ahp.refreshExposure` + pub const AHP_REFRESHEXPOSURE: &str = "ahp.refreshExposure"; + /// `ahp.setCapabilities` + pub const AHP_SETCAPABILITIES: &str = "ahp.setCapabilities"; /// `hooks.discover` pub const HOOKS_DISCOVER: &str = "hooks.discover"; /// `models.list` @@ -73,6 +87,8 @@ pub mod rpc_methods { pub const REGISTEREXTENSIONLAUNCHPROVIDER: &str = "registerExtensionLaunchProvider"; /// `catalog.search` pub const CATALOG_SEARCH: &str = "catalog.search"; + /// `catalog.select` + pub const CATALOG_SELECT: &str = "catalog.select"; /// `plugins.list` pub const PLUGINS_LIST: &str = "plugins.list"; /// `plugins.install` @@ -290,6 +306,36 @@ pub mod rpc_methods { pub const SESSION_FACTORY_JOURNAL_GET: &str = "session.factory.journal.get"; /// `session.factory.journal.put` pub const SESSION_FACTORY_JOURNAL_PUT: &str = "session.factory.journal.put"; + /// `session.workflow.run` + pub const SESSION_WORKFLOW_RUN: &str = "session.workflow.run"; + /// `session.workflow.resume` + pub const SESSION_WORKFLOW_RESUME: &str = "session.workflow.resume"; + /// `session.workflow.runFromTool` + pub const SESSION_WORKFLOW_RUNFROMTOOL: &str = "session.workflow.runFromTool"; + /// `session.workflow.resumeFromTool` + pub const SESSION_WORKFLOW_RESUMEFROMTOOL: &str = "session.workflow.resumeFromTool"; + /// `session.workflow.getRun` + pub const SESSION_WORKFLOW_GETRUN: &str = "session.workflow.getRun"; + /// `session.workflow.listRuns` + pub const SESSION_WORKFLOW_LISTRUNS: &str = "session.workflow.listRuns"; + /// `session.workflow.getRunDetail` + pub const SESSION_WORKFLOW_GETRUNDETAIL: &str = "session.workflow.getRunDetail"; + /// `session.workflow.getRunProgress` + pub const SESSION_WORKFLOW_GETRUNPROGRESS: &str = "session.workflow.getRunProgress"; + /// `session.workflow.cancel` + pub const SESSION_WORKFLOW_CANCEL: &str = "session.workflow.cancel"; + /// `session.workflow.pause` + pub const SESSION_WORKFLOW_PAUSE: &str = "session.workflow.pause"; + /// `session.workflow.pauseAtCheckpoint` + pub const SESSION_WORKFLOW_PAUSEATCHECKPOINT: &str = "session.workflow.pauseAtCheckpoint"; + /// `session.workflow.log` + pub const SESSION_WORKFLOW_LOG: &str = "session.workflow.log"; + /// `session.workflow.agent` + pub const SESSION_WORKFLOW_AGENT: &str = "session.workflow.agent"; + /// `session.workflow.journal.get` + pub const SESSION_WORKFLOW_JOURNAL_GET: &str = "session.workflow.journal.get"; + /// `session.workflow.journal.put` + pub const SESSION_WORKFLOW_JOURNAL_PUT: &str = "session.workflow.journal.put"; /// `session.model.getCurrent` pub const SESSION_MODEL_GETCURRENT: &str = "session.model.getCurrent"; /// `session.model.switchTo` @@ -500,8 +546,48 @@ pub mod rpc_methods { pub const SESSION_MCP_RESOURCES_LIST: &str = "session.mcp.resources.list"; /// `session.mcp.resources.listTemplates` pub const SESSION_MCP_RESOURCES_LISTTEMPLATES: &str = "session.mcp.resources.listTemplates"; + /// `session.connectors.getCapabilities` + pub const SESSION_CONNECTORS_GETCAPABILITIES: &str = "session.connectors.getCapabilities"; + /// `session.connectors.getStatus` + pub const SESSION_CONNECTORS_GETSTATUS: &str = "session.connectors.getStatus"; + /// `session.connectors.list` + pub const SESSION_CONNECTORS_LIST: &str = "session.connectors.list"; + /// `session.connectors.refresh` + pub const SESSION_CONNECTORS_REFRESH: &str = "session.connectors.refresh"; + /// `session.connectors.connect` + pub const SESSION_CONNECTORS_CONNECT: &str = "session.connectors.connect"; + /// `session.connectors.reconnect` + pub const SESSION_CONNECTORS_RECONNECT: &str = "session.connectors.reconnect"; + /// `session.connectors.continueConnection` + pub const SESSION_CONNECTORS_CONTINUECONNECTION: &str = "session.connectors.continueConnection"; + /// `session.connectors.disconnect` + pub const SESSION_CONNECTORS_DISCONNECT: &str = "session.connectors.disconnect"; + /// `session.connectors.reconcile` + pub const SESSION_CONNECTORS_RECONCILE: &str = "session.connectors.reconcile"; + /// `session.managedSettings.get` + pub const SESSION_MANAGEDSETTINGS_GET: &str = "session.managedSettings.get"; /// `session.plugins.list` pub const SESSION_PLUGINS_LIST: &str = "session.plugins.list"; + /// `session.plugins.install` + pub const SESSION_PLUGINS_INSTALL: &str = "session.plugins.install"; + /// `session.plugins.uninstall` + pub const SESSION_PLUGINS_UNINSTALL: &str = "session.plugins.uninstall"; + /// `session.plugins.update` + pub const SESSION_PLUGINS_UPDATE: &str = "session.plugins.update"; + /// `session.plugins.enable` + pub const SESSION_PLUGINS_ENABLE: &str = "session.plugins.enable"; + /// `session.plugins.disable` + pub const SESSION_PLUGINS_DISABLE: &str = "session.plugins.disable"; + /// `session.plugins.marketplaces.list` + pub const SESSION_PLUGINS_MARKETPLACES_LIST: &str = "session.plugins.marketplaces.list"; + /// `session.plugins.marketplaces.add` + pub const SESSION_PLUGINS_MARKETPLACES_ADD: &str = "session.plugins.marketplaces.add"; + /// `session.plugins.marketplaces.remove` + pub const SESSION_PLUGINS_MARKETPLACES_REMOVE: &str = "session.plugins.marketplaces.remove"; + /// `session.plugins.marketplaces.browse` + pub const SESSION_PLUGINS_MARKETPLACES_BROWSE: &str = "session.plugins.marketplaces.browse"; + /// `session.plugins.marketplaces.refresh` + pub const SESSION_PLUGINS_MARKETPLACES_REFRESH: &str = "session.plugins.marketplaces.refresh"; /// `session.plugins.reload` pub const SESSION_PLUGINS_RELOAD: &str = "session.plugins.reload"; /// `session.provider.getEndpoint` @@ -706,6 +792,10 @@ pub mod rpc_methods { pub const SESSION_QUEUE_REMOVEAT: &str = "session.queue.removeAt"; /// `session.queue.updateText` pub const SESSION_QUEUE_UPDATETEXT: &str = "session.queue.updateText"; + /// `session.queue.withdrawMessage` + pub const SESSION_QUEUE_WITHDRAWMESSAGE: &str = "session.queue.withdrawMessage"; + /// `session.queue.appendSteering` + pub const SESSION_QUEUE_APPENDSTEERING: &str = "session.queue.appendSteering"; /// `session.queue.duplicateAt` pub const SESSION_QUEUE_DUPLICATEAT: &str = "session.queue.duplicateAt"; /// `session.queue.setDrainPaused` @@ -781,6 +871,10 @@ pub mod rpc_methods { pub const FACTORY_EXECUTE: &str = "factory.execute"; /// `factory.abort` pub const FACTORY_ABORT: &str = "factory.abort"; + /// `workflow.execute` + pub const WORKFLOW_EXECUTE: &str = "workflow.execute"; + /// `workflow.abort` + pub const WORKFLOW_ABORT: &str = "workflow.abort"; /// `tasks.cancel` pub const TASKS_CANCEL: &str = "tasks.cancel"; /// `sessionFs.readFile` @@ -879,6 +973,13 @@ pub struct CopilotUserResponseEndpoints { pub telemetry: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CopilotUserResponseEnterpriseListItem { + /// Numeric database ID of the enterprise. + pub id: i64, +} + /// Chat quota snapshot from the raw Copilot user-response passthrough, with entitlement, overage, remaining quota, reset, and billing fields. /// ///
@@ -1125,6 +1226,9 @@ pub struct CopilotUserResponse { /// Endpoint URLs from the raw Copilot `/copilot_internal/v2/token` user-response passthrough. #[serde(skip_serializing_if = "Option::is_none")] pub endpoints: Option, + /// Enterprises that provide the user's Copilot license, each with a stable numeric ID. + #[serde(rename = "enterprise_list", skip_serializing_if = "Option::is_none")] + pub enterprise_list: Option>, /// Whether MCP (Model Context Protocol) support is enabled for the user. #[serde(rename = "is_mcp_enabled", skip_serializing_if = "Option::is_none")] pub is_mcp_enabled: Option, @@ -1993,6 +2097,185 @@ pub struct AgentsGetDiscoveryPathsRequest { pub project_paths: Option>, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AhpConnectionClosedNotification { + pub connection_id: String, + pub endpoint_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// +///
+/// +/// **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 AhpConnectionRef { + pub connection_id: String, + pub endpoint_id: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AhpCreateSessionRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub config: Option, + pub connection_id: String, + pub endpoint_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + pub requested_session_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub working_directory: Option, +} + +/// Executable endpoint handlers retained by the application, never serialized. +/// +///
+/// +/// **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 AhpEndpointCallbacks { + pub create_session: bool, + pub list_sessions: bool, + pub resume_session: bool, + pub session_control: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AhpEndpointClosedNotification { + pub endpoint_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// +///
+/// +/// **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 AhpEndpointRef { + pub endpoint_id: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AhpListSessionsResult { + pub session_ids: Vec, +} + +/// +///
+/// +/// **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 AhpMessage { + pub connection_id: String, + pub endpoint_id: String, + /// One complete AHP JSON message, preserved without SDK-side decoding. + pub message: String, +} + +/// +///
+/// +/// **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 AhpOpenConnectionResult { + pub connection_id: String, +} + +/// Endpoint policy and the callbacks installed on its owning SDK connection. +/// +///
+/// +/// **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 AhpRegisterEndpointRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_session_creation: Option, + #[doc(hidden)] + pub(crate) callbacks: AhpEndpointCallbacks, + #[serde(skip_serializing_if = "Option::is_none")] + pub capabilities: Option, + /// Trusted application workspace for all new sessions; ignores client directory hints. + #[serde(skip_serializing_if = "Option::is_none")] + pub fixed_working_directory: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AhpResumeSessionRequest { + pub connection_id: String, + pub endpoint_id: String, + pub session_id: SessionId, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AhpSessionControlRequest { + pub endpoint_id: String, + pub kind: String, + pub payload: serde_json::Value, + pub session_id: SessionId, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AhpSessionControlResult { + pub applied: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AhpSessionIdentity { + pub session_id: SessionId, +} + +/// +///
+/// +/// **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 AhpSetCapabilitiesRequest { + pub capabilities: serde_json::Value, + pub endpoint_id: String, +} + /// Blob attachment with inline base64-encoded data /// ///
@@ -3163,6 +3446,81 @@ pub struct CardDigest { pub value: String, } +/// Where and when an Agent Plugin catalog reference was observed. Discovery provenance deliberately carries no descriptor URL, raw data, candidate handle, or content digest. +/// +///
+/// +/// **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 CatalogAgentPluginCandidateProvenance { + /// Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. + pub authority: String, + /// Canonical Agent Plugin media type. + pub media_type: CatalogAgentPluginMediaType, + /// ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. + pub observed_at: String, +} + +/// Syntactically validated GitHub repository provenance declared by catalog metadata. This is a source claim rather than proof that the descriptor URL resolves to the repository. +/// +///
+/// +/// **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 CatalogPluginRepositorySource { + /// Case-preserving safe relative POSIX path derived from metadata.repoPath. + pub path: String, + /// Canonical lowercase owner/repository name derived from metadata.sourceSet. + pub repository: String, +} + +/// An inert Agent Plugin catalog result. Its canonical catalog identity, declared version, repository source claim, and explicit compatibility tags are safe to correlate, while its descriptor, URL, raw data, and installed-plugin state remain runtime-private. This contract-only variant does not mint or expose a candidate handle. +/// +///
+/// +/// **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 CatalogAgentPluginCandidate { + /// Explicit validated compatibility tags, in canonical order. An empty list means the source declared no recognised compatibility; clients must not infer compatibility from other fields. `canvas-only` requires both `canvas` and `github-copilot`. + pub compatibility_tags: Vec, + /// Description taken verbatim from the card. Inert untrusted text. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Display name taken verbatim from the card. Inert untrusted text. + pub display_name: String, + /// Validated, normalised catalogue resource URN. This identity comes only from the catalog identifier and is never inferred from display text or installed-plugin state. + pub identity: String, + /// Discriminator: this candidate describes an Agent Plugin. + pub kind: CatalogAgentPluginCandidateKind, + /// Canonical Agent Plugin media type. + pub media_type: CatalogAgentPluginMediaType, + /// Where the Agent Plugin catalog reference was observed, without its descriptor, URL, raw data, or content digest. + pub provenance: CatalogAgentPluginCandidateProvenance, + /// Publisher taken verbatim from the card. Inert untrusted text. + #[serde(skip_serializing_if = "Option::is_none")] + pub publisher: Option, + /// Bounded repository provenance declared through the catalog's sourceSet and repoPath metadata. It contains no descriptor URL. + pub source: CatalogPluginRepositorySource, + /// Versioned trust metadata observed from the catalog authority. Optional for protocol-3 compatibility and emitted only when the caller also requires the trust-snapshot capability. + #[serde(skip_serializing_if = "Option::is_none")] + pub trust: Option, + /// Optional version declared by the catalog source. Omitted rather than guessed when the source supplies no version. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + /// Where and when an AI skill catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. /// ///
@@ -3177,7 +3535,7 @@ pub struct CatalogAiSkillCandidateProvenance { /// Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. pub authority: String, /// Media type advertised for the referenced AI skill card - pub media_type: CatalogAiSkillCandidateProvenanceMediaType, + pub media_type: CatalogAiSkillMediaType, /// ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. pub observed_at: String, } @@ -3235,11 +3593,11 @@ pub struct CatalogAiSkillCandidate { /// ISO 8601 timestamp after which the handle is stale and will be rejected. pub handle_expires_at: String, /// AI skills are discovery-only and cannot be installed through this surface - pub installability: CatalogAiSkillCandidateInstallability, + pub installability: CatalogAiSkillInstallability, /// Discriminator: this candidate describes an AI skill pub kind: CatalogAiSkillCandidateKind, /// Media type of the underlying AI skill card - pub media_type: CatalogAiSkillCandidateMediaType, + pub media_type: CatalogAiSkillMediaType, /// Where the catalog reference was observed, without the card itself or any content digest. pub provenance: CatalogAiSkillCandidateProvenance, /// Publisher taken verbatim from the card. Inert untrusted text. @@ -3385,7 +3743,7 @@ pub struct CatalogHandleRejectedError { pub reason: CatalogHandleRejectionReason, } -/// The request was rejected before any work was done, because a bounded field fell outside its permitted range or a required field was unusable. +/// The request was rejected because a bounded field fell outside its permitted range or a required field was unusable. Pagination may also be rejected by the authority after a continuation request; repeat the search without page. /// ///
/// @@ -3437,7 +3795,7 @@ pub struct CatalogMalformedCardError { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CatalogNegotiatedContract { - /// Wire features the runtime understood for this operation. Always a superset of the caller's required features, because any shortfall is a refusal instead. Operation availability remains a separate typed result. + /// Wire features the runtime understood for this operation. Includes the five original catalog capabilities and only explicitly requested supported additions, in supported order without duplicates. Capabilities that introduce new success-union variants or operations are therefore included only when explicitly required, preserving older protocol-v3 clients. Always a superset of the caller's required features, because any shortfall is a refusal instead. Operation availability remains a separate typed result. pub granted_capabilities: Vec, /// Protocol version of the runtime that served the request. pub runtime_protocol_version: i64, @@ -3533,6 +3891,52 @@ pub struct CatalogPolicyRejectedError { pub source: McpPlanPolicySource, } +/// An explicit numbered-page request. The SDK treats the token as opaque; only the runtime decodes it and changes its targetPage. Authority validation binds navigation to the original search. No snapshot stability or token TTL is promised. +/// +///
+/// +/// **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 CatalogSearchPage { + /// Requested one-based page. Must not exceed either the token's signed pageCount or the navigation window ceil(1000 / pageSize). Repeat the search without page to discover newly available pages beyond that signed pageCount. + pub number: i32, + /// Opaque authority-issued pagination token from an earlier response. Never decode, modify or log it in an SDK consumer. + pub token: String, +} + +/// Authority-reported navigation metadata, returned only to callers requiring catalog-search-pagination and only when a supported token is present. Tokenless first-page and continuation responses omit this object; no counts are inferred from candidates. The opaque token may be retained for previous or numbered navigation even when hasNextPage is false. +/// +///
+/// +/// **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 CatalogSearchPagination { + /// One-based page returned by the authority. + pub current_page: i32, + /// Whether the authority token advertises a valid next target within the navigation window. Not inferred from token presence, truncated, or currentPage being less than pageCount. + pub has_next_page: bool, + /// Navigation window ceiling ceil(1000 / pageSize), not the number of existing pages. Legal targets must not exceed this ceiling or the token's signed pageCount. + pub max_page: i32, + /// Backend-reported page count, which may exceed maxPage. Present pagination metadata always describes a multi-page result; zero- and single-page responses omit pagination. Navigation targets must also be within the signed pageCount carried by the supplied token. + pub page_count: i64, + /// Page size bound to the search, equal to the effective request limit. + pub page_size: i32, + /// Opaque authority-issued pagination token. Only the runtime decodes it or changes targetPage; SDK consumers must not decode, modify or log it. It has no runtime-created expiry or cache. + pub token: String, + /// Backend-reported count for this response, not the number of returned candidates. Its relationship to the full query result set is unknown. + pub total_count: i64, + /// The relationship of totalCount to the complete query result set is unknown; neither exactness nor a lower-bound guarantee is implied. + pub total_count_relation: CatalogSearchTotalCountRelation, +} + /// A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. /// ///
@@ -3546,17 +3950,20 @@ pub struct CatalogPolicyRejectedError { pub struct CatalogSearchRequest { /// Protocol version and capabilities the caller requires. pub contract: CatalogClientContract, - /// Restrict results to these candidate kinds. When omitted, every kind the runtime supports is searched. + /// Restrict results to these candidate kinds. Agent Plugins are opt-in and require the `agent-plugin-discovery` capability so protocol-v3 clients generated before that variant cannot receive an unknown result; when omitted, the backwards-compatible MCP server and AI skill kinds are searched. #[serde(skip_serializing_if = "Option::is_none")] pub kinds: Option>, /// Maximum number of candidates to return. Defaults to 10 when omitted. #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, + /// Numbered navigation using metadata from an earlier response. Requires catalog-search-pagination and the same query, kinds and effective limit. Omit for a fresh first-page search. + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. pub query: String, } -/// A completed catalog search: inert candidate summaries, each carrying a single-use handle. +/// A completed catalog search containing inert candidate summaries. MCP server and AI skill variants carry a single-use handle; the Agent Plugin variant is handleless. /// ///
/// @@ -3573,9 +3980,12 @@ pub struct CatalogSearchSucceeded { pub kind: CatalogSearchSucceededKind, /// Protocol version and capabilities the runtime honoured. pub negotiated: CatalogNegotiatedContract, + /// Navigation metadata for callers explicitly requiring catalog-search-pagination. Omitted when the authority returns no token, including tokenless first-page and continuation responses. Counts are never substituted from candidates.length. + #[serde(skip_serializing_if = "Option::is_none")] + pub pagination: Option, /// Pseudonymous identifier for this search, issued by the runtime or by the catalog authority it queried and never by the caller, so it cannot be forged or replayed to attribute an install to a search that never happened. Always present on a success, so a result set can be tied to the installs it leads to. It identifies a search rather than a person: it is derived from no user, account, device, or query data, and must never be joined with user identity to re-identify anyone. pub search_id: String, - /// Whether further matches existed beyond the requested limit. + /// Legacy indication that the authority returned a page token. Preserved for compatibility; this is not a has-next-page indicator. Use pagination.hasNextPage when pagination metadata is present. pub truncated: bool, } @@ -3638,6 +4048,182 @@ pub struct CatalogUnavailableError { pub reason: CatalogUnavailableReason, } +/// The caller cancelled the selection interaction and the retained search state was released. +/// +///
+/// +/// **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 CatalogSelectionCancelled { + /// Discriminator: selection was cancelled + pub kind: CatalogSelectionCancelledKind, + /// The search identifier privately bound to the released selection group. + pub search_id: String, +} + +/// The caller explicitly declined every candidate and the retained search state was released. +/// +///
+/// +/// **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 CatalogSelectionDeclined { + /// Discriminator: the candidates were declined + pub kind: CatalogSelectionDeclinedKind, + /// The search identifier privately bound to the released selection group. + pub search_id: String, +} + +/// The selection reference belongs to another runtime instance or session. +/// +///
+/// +/// **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 CatalogSelectionForeign { + /// Discriminator for this typed selection rejection + pub kind: CatalogSelectionForeignKind, + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + pub message: String, +} + +/// The selection reference was malformed or unknown. +/// +///
+/// +/// **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 CatalogSelectionInvalid { + /// Discriminator for this typed selection rejection + pub kind: CatalogSelectionInvalidKind, + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + pub message: String, +} + +/// The selection group was already terminated or its pending host hand-off was already claimed. +/// +///
+/// +/// **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 CatalogSelectionReplayed { + /// Discriminator for this typed selection rejection + pub kind: CatalogSelectionReplayedKind, + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + pub message: String, +} + +/// Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. +/// +///
+/// +/// **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 CatalogSelectionRequest { + /// Protocol version and capabilities the caller requires. + pub contract: CatalogClientContract, + /// The terminal outcome declared by the caller. Timed-out means the host's live interaction deadline elapsed; a reference whose runtime TTL elapsed is rejected separately as stale. + pub outcome: CatalogSelectionDecision, + /// Opaque runtime-instance scoped reference to one visible candidate. For a non-selected outcome, any candidate reference from the same search closes that search's retained group. + pub selection_ref: String, + /// Locally owned root session whose retained search state is being resolved. + pub session_id: SessionId, +} + +/// The chosen candidate was transferred into a fresh bounded single-use handle for a later explicit planning 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, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CatalogSelectionSelected { + /// Fresh single-use candidate handle accepted by mcp.planInstall. Returned only to the native host and never included in model-tool output. + pub candidate_handle: String, + /// Discriminator: one candidate was selected + pub kind: CatalogSelectionSelectedKind, + /// The exact search identifier privately bound to the selected candidate. + pub search_id: String, +} + +/// The host declared that its live selection interaction timed out, and the retained search state was released. +/// +///
+/// +/// **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 CatalogSelectionTimedOut { + /// Discriminator: the host's live interaction timed out + pub kind: CatalogSelectionTimedOutKind, + /// The search identifier privately bound to the released selection group. + pub search_id: String, +} + +/// The runtime-enforced selection reference lifetime elapsed before the request arrived. +/// +///
+/// +/// **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 CatalogSelectionStale { + /// Discriminator for this typed selection rejection + pub kind: CatalogSelectionStaleKind, + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + pub message: String, +} + +/// The presented opaque handle was issued for another catalog operation. +/// +///
+/// +/// **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 CatalogSelectionWrongKind { + /// Discriminator for this typed selection rejection + pub kind: CatalogSelectionWrongKindKind, + /// Human-readable explanation safe to surface. Never contains the presented reference or private candidate state. + pub message: String, +} + /// Where and when the runtime observed the trust metadata. Observation time is not the authority's evaluation time and must not be used to infer staleness. /// ///
@@ -4274,6 +4860,263 @@ pub struct ConnectedRemoteSessionMetadata { pub summary: Option, } +/// Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. +/// +///
+/// +/// **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 ConnectorAccountRequest { + /// Opaque account selection ID previously returned by an account discovery API. + pub account_id: String, +} + +/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +/// +///
+/// +/// **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 ConnectorCapabilities { + /// Connector API contract version. + pub api_version: i64, + /// Current session availability. Disabled availability is reported without making a Connector request. + pub availability: ConnectorAvailability, + /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. + pub consent_continuation: bool, + /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. + pub max_deadline_ms: i64, + /// Maximum accepted polling attempts for one continuation call. + pub max_poll_attempts: i64, + /// Maximum accepted delay in milliseconds between polling attempts. + pub max_poll_interval_ms: i64, + /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. + pub opaque_account_selection: bool, +} + +/// Credential-free Connector catalog entry. +/// +///
+/// +/// **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 ConnectorCatalogEntry { + /// Untrusted service description, when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Untrusted display label from the service. + pub display_name: String, + /// Canonical Connector name used by lifecycle methods. + pub name: String, + /// Opaque stable runtime IDs currently projected into the session for this Connector. + pub runtime_server_ids: Vec, + /// Current authoritative service connection state. + pub status: ConnectorCatalogStatus, +} + +/// Validated Connector catalog snapshot cached by the session. +/// +///
+/// +/// **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 ConnectorCatalogResult { + /// Validated catalog entries in service order. + pub connectors: Vec, + /// Unix epoch milliseconds when this snapshot was accepted. + pub refreshed_at_ms: i64, + /// Monotonically increasing session-local catalog revision. + pub revision: i64, +} + +/// Selects one Connector and the pinned host-owned account used for its service and MCP authorization. +/// +///
+/// +/// **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 ConnectorConnectRequest { + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + pub account_id: String, + /// Canonical Connector name from the current catalog. + pub connector_name: String, +} + +/// Live status of one session-owned MCP projection. +/// +///
+/// +/// **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 ConnectorRuntimeStatus { + /// Canonical Connector name that owns this server. + pub connector_name: String, + /// Opaque runtime server ID. + pub runtime_server_id: String, + /// Current live MCP host status. + pub status: ConnectorMcpStatus, +} + +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +/// +///
+/// +/// **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 ConnectorStatus { + /// Opaque account selection pinned to this session, when one has been selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + /// Connector API contract version. + pub api_version: i64, + /// Current feature and session availability. + pub availability: ConnectorAvailability, + /// Latest validated catalog snapshot, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub catalog: Option, + /// Number of active opaque connection continuations. + pub pending_connections: i64, + /// Live MCP status for every Connector-owned runtime server. + pub runtime_servers: Vec, +} + +/// The service is connected and the session MCP graph was reconciled. +/// +///
+/// +/// **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 ConnectorConnectResultConnected { + /// The service is connected and the session MCP graph was reconciled. + pub kind: ConnectorConnectResultConnectedKind, + /// Fresh authoritative Connector state after MCP reconciliation. + pub status: ConnectorStatus, +} + +/// Host-owned consent is required before bounded continuation can complete. +/// +///
+/// +/// **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 ConnectorConnectResultConsentRequired { + /// Validated HTTPS consent URL. The runtime does not open it. + pub consent_url: String, + /// Opaque ID accepted by continueConnection. + pub continuation_id: String, + /// Host-owned consent is required before bounded continuation can complete. + pub kind: ConnectorConnectResultConsentRequiredKind, +} + +/// The service is still completing the connection without a consent URL. +/// +///
+/// +/// **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 ConnectorConnectResultPending { + /// Opaque ID accepted by continueConnection. + pub continuation_id: String, + /// The service is still completing the connection without a consent URL. + pub kind: ConnectorConnectResultPendingKind, +} + +/// Explicitly bounded continuation of a pending Connector connection. +/// +///
+/// +/// **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 ConnectorContinueRequest { + /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. + pub continuation_id: String, + /// Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. + pub deadline_ms: i32, + /// Maximum catalog requests made by this call. Must be between one and the capability limit. + pub max_attempts: i32, + /// Delay in milliseconds between attempts. Must not exceed the capability limit. + pub poll_interval_ms: i32, +} + +/// Authoritative result after disconnect and MCP reconciliation. +/// +///
+/// +/// **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 ConnectorDisconnectResult { + /// Whether the service accepted the idempotent disconnect. + pub disconnected: bool, + /// Fresh authoritative session state after removing Connector-owned MCP servers. + pub status: ConnectorStatus, +} + +/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. +/// +///
+/// +/// **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 ConnectorReconcileRequest { + /// Opaque account selection ID. It must match the account already pinned to the session, if any. + pub account_id: String, + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_catalog: Option, +} + /// Remote session connection parameters. /// ///
@@ -7070,9 +7913,18 @@ pub struct InstalledPluginInfo { pub direct_source_id: Option, /// Whether the plugin is currently enabled for new sessions pub enabled: bool, + /// Whether the managed desired plugin currently has an installed or live record. Set to false for a managed desired entry retained in the listing after installation or reconciliation failed. + #[serde(skip_serializing_if = "Option::is_none")] + pub installed: Option, /// Absolute path of the marketplace directory a live plugin was resolved from. Present only on live, never-persisted records — a plugin belonging to a directory/local marketplace, which is loaded from its real directory on every pass instead of a copy under the installed-plugins cache. Its presence is what marks a listed plugin as live: such a plugin is always present on disk, so `enabled` is its only meaningful state and it is never "not installed". #[serde(skip_serializing_if = "Option::is_none")] pub installed_from: Option, + /// Whether enterprise managed settings control this plugin's enabled state. + #[serde(skip_serializing_if = "Option::is_none")] + pub managed: Option, + /// The enabled state required by enterprise managed settings, when this plugin spec is managed. + #[serde(skip_serializing_if = "Option::is_none")] + pub managed_desired_enabled: Option, /// Marketplace the plugin came from. Empty string ("") for direct repo / URL / local installs. pub marketplace: String, /// Plugin name @@ -7693,6 +8545,46 @@ pub struct ManagedSettingsReadResult { pub settings_json: Option, } +/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// +///
+/// +/// **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 ManagedSettingsResolvedData { + /// Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. + pub bypass_permissions_disabled: bool, + /// Whether a session-local permissions layer injected by the SDK host was present + #[serde(skip_serializing_if = "Option::is_none")] + pub client_managed: Option, + /// Whether an actual device MDM/plist/registry/file managed-settings layer was present + pub device_managed: bool, + /// Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. + pub fail_closed: bool, + /// The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + pub managed_keys: Vec, + /// Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. + #[serde(skip_serializing_if = "Option::is_none")] + pub permissions_allow_intersected: Option, + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_helper_managed: Option, + /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_enabled_by_undetermined_policy: Option, + /// Whether the server (account/org) managed-settings layer was present + pub server_managed: bool, + /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. + #[serde(skip_serializing_if = "Option::is_none")] + pub settings: Option, + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + pub source: ManagedSettingsResolvedSource, +} + /// Result of registering a new marketplace. /// ///
@@ -7752,9 +8644,15 @@ pub struct MarketplaceBrowseResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MarketplaceInfo { + /// Whether the managed marketplace currently resolved into the runtime marketplace registry. Set to false when the desired managed entry is retained for governance visibility after loading or reconciliation failed. + #[serde(skip_serializing_if = "Option::is_none")] + pub available: Option, /// True when this is a default marketplace shipped with the runtime. Defaults are not removable. #[serde(skip_serializing_if = "Option::is_none")] pub is_default: Option, + /// Whether enterprise managed settings provide and control this marketplace entry. + #[serde(skip_serializing_if = "Option::is_none")] + pub managed: Option, /// Marketplace name (matches the @marketplace suffix in plugin specs) pub name: String, /// Human-readable description of where the marketplace data is fetched from (e.g. "GitHub: owner/repo"). @@ -8547,7 +9445,7 @@ pub struct McpHostState { pub pending_connections: Vec, } -/// One change applying the plan would make, described rather than serialised so the configuration payload stays behind the runtime boundary. +/// The configuration-change alternative for the transportChoices entry at the same index. Only the selected alternative is applied; entries are not cumulative. The payload stays behind the runtime boundary. /// ///
/// @@ -8662,7 +9560,7 @@ pub struct McpPlanTarget { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpInstallPlan { - /// The configuration changes installing would make, described rather than serialised, so the mutable configuration payload stays behind the runtime boundary. + /// Alternative configuration changes, with exactly one entry for each transportChoices entry in the same order. Only the entry for the subsequently selected transport applies; these are not cumulative writes. Payloads remain behind the runtime boundary. pub configuration_changes: Vec, /// Normalised identity of the server the plan would install. pub identity: McpPlanResourceIdentity, @@ -8679,7 +9577,7 @@ pub struct McpInstallPlan { pub recommended_transport_choice_id: Option, /// Whether applying this plan would require an MCP reload to take effect. Planning itself never reloads. pub reload_required: bool, - /// Whether the plan cannot be applied without further input, because a required value has no default or a secret must be supplied. + /// True only when every eligible transport choice needs additional values or secrets. False means at least one choice needs no additional configuration, not that every choice is ready. A later apply operation must validate the selected choice's own inputs, secrets and policy after explicit confirmation. pub requires_interactive_configuration: bool, /// Configuration scope and key the plan would write to. pub target: McpPlanTarget, @@ -9035,7 +9933,7 @@ pub struct McpPlanInstallSourceCandidate { pub candidate_handle: String, /// Discriminator: plan from a previously returned candidate pub kind: McpPlanInstallSourceCandidateKind, - /// The runtime- or authority-minted `searchId` returned with the search that produced this candidate. A search implementation binds it to private candidate-handle context; a planning implementation must verify that context before returning a plan. The unavailable planning implementation in this contract layer validates presence but does not claim the verification has occurred. It identifies a search rather than a person and must never be joined with user identity to re-identify anyone. + /// The runtime- or authority-minted `searchId` returned with the search that produced this candidate. Planning verifies the private correlation and atomically consumes a matching candidate before downstream work, including attempts that subsequently fail or report unavailable. A mismatched search does not consume the candidate. It identifies a search rather than a person and must never be joined with user identity to re-identify anyone. pub search_id: String, } @@ -9784,6 +10682,9 @@ pub struct McpServerConfigHttp { /// Telemetry-obfuscation policy for this server's tools. #[serde(skip_serializing_if = "Option::is_none")] pub safe_for_telemetry: Option, + /// Milliseconds this server may spend connecting before the CLI warns that it is taking longer than expected. Presentation only: it does not change how long the connection is allowed to take. + #[serde(skip_serializing_if = "Option::is_none")] + pub slow_connection_threshold_ms: Option, /// The origin of this server configuration. #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, @@ -9862,6 +10763,9 @@ pub(crate) struct McpServerConfigMemory { /// In-process MCP server instance. This value cannot cross a JSON-RPC boundary. #[doc(hidden)] pub(crate) server_instance: serde_json::Value, + /// Milliseconds this server may spend connecting before the CLI warns that it is taking longer than expected. Presentation only: it does not change how long the connection is allowed to take. + #[serde(skip_serializing_if = "Option::is_none")] + pub slow_connection_threshold_ms: Option, /// The origin of this server configuration. #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, @@ -9948,6 +10852,9 @@ pub struct McpServerConfigStdio { /// Telemetry-obfuscation policy for this server's tools. #[serde(skip_serializing_if = "Option::is_none")] pub safe_for_telemetry: Option, + /// Milliseconds this server may spend connecting before the CLI warns that it is taking longer than expected. Presentation only: it does not change how long the connection is allowed to take. + #[serde(skip_serializing_if = "Option::is_none")] + pub slow_connection_threshold_ms: Option, /// The origin of this server configuration. #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, @@ -10857,6 +11764,9 @@ pub struct Model { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModelApplyStartupOverlayRequest { + /// Effective default Auto routing preference from user and managed settings. Applies only to fresh sessions and never replaces a per-session selection. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Model explicitly selected by the CLI, when provided. #[serde(skip_serializing_if = "Option::is_none")] pub cli_model: Option, @@ -11296,6 +12206,9 @@ pub struct ModelSwitchToResult { /// Persistence failure encountered after applying the model switch. #[serde(skip_serializing_if = "Option::is_none")] pub persistence_error: Option, + /// Stable queue item identifier when this request was enqueued. Remains present if the item drains before the response is returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_id: Option, /// Lifecycle result for the requested switch #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, @@ -11593,6 +12506,9 @@ pub struct OptionsUpdateAdditionalContentExclusionPolicy { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PendingPermissionRequest { + /// Permission-recovery episode that authorized this request to surface for interactive attention + #[serde(skip_serializing_if = "Option::is_none")] + pub recovery_episode_id: Option, /// The user-facing permission prompt details (commands, write, read, mcp, url, memory, custom-tool, path, hook) pub request: PermissionPromptRequest, /// Unique identifier for the pending permission request @@ -13365,12 +14281,30 @@ pub struct PlanUpdateRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Plugin { + /// Opaque stable identity for a direct plugin source. + #[serde(skip_serializing_if = "Option::is_none")] + pub direct_source_id: Option, /// Whether the plugin is currently enabled pub enabled: bool, + /// Whether this managed desired plugin has an installed or live record. + #[serde(skip_serializing_if = "Option::is_none")] + pub installed: Option, + /// Absolute marketplace directory for a live plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub installed_from: Option, + /// Whether enterprise managed settings control this plugin. + #[serde(skip_serializing_if = "Option::is_none")] + pub managed: Option, + /// Enabled state required by enterprise managed settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub managed_desired_enabled: Option, /// Marketplace the plugin came from pub marketplace: String, /// Plugin name pub name: String, + /// Runtime plugin provenance, such as "builtin". + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, /// Installed version #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, @@ -14505,6 +15439,31 @@ pub struct PushAttachmentSelection { pub r#type: PushAttachmentSelectionType, } +/// Append to one pending steering message without changing its identity or delivery position. +/// +///
+/// +/// **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 QueueAppendSteeringRequest { + /// Mode captured at submission. Only steering messages in the same mode may be combined. + pub agent_mode: SendAgentMode, + /// Attachments to add after the message's existing attachments. An empty list preserves the existing attachments. + pub attachments: Vec, + /// Display text to append to the existing preview after a blank line. + pub display_prompt: String, + /// Expected current prompt, including any previous appends. The runtime applies plan-mode normalization before comparing and refuses a changed message. + pub expected_prompt: String, + /// Message identity returned by send, not the queue item id. Only unclaimed user steering messages are eligible. + pub message_id: String, + /// Text to append after a blank line. + pub prompt: String, +} + /// Inputs for starting a deferred-idle drain. /// ///
@@ -14949,6 +15908,9 @@ pub struct QueueSetDrainPausedRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct QueueSnapshotResult { + /// Queue item identifier of a model switch that has been dequeued but not yet applied. + #[serde(skip_serializing_if = "Option::is_none")] + pub in_flight_model_change_id: Option, /// Insertion orders for queued items, aligned with `items`. #[serde(skip_serializing_if = "Option::is_none")] pub item_orders: Option>, @@ -14996,6 +15958,23 @@ pub struct QueueUpdateTextResult { pub updated: bool, } +/// Conditional withdrawal of a single user message, before the runtime claims it for delivery. +/// +///
+/// +/// **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 QueueWithdrawMessageRequest { + /// The prompt originally sent. A message edited since submission is not withdrawn, so an obsolete draft cannot replace the edit. + pub expected_prompt: String, + /// Message identity returned by send, not the queue item id. Batch messages are not eligible. + pub message_id: String, +} + /// Event type to register consumer interest for, used by runtime gating logic. /// ///
@@ -15525,7 +16504,7 @@ pub struct SandboxConfigUserPolicyNetwork { /// Hosts denied by the built-in sandbox proxy. Deny rules take precedence over allowedHosts. A domain also denies all its subdomains. IP addresses match exactly; *.example.com matches strict subdomains, and * denies every host. #[serde(skip_serializing_if = "Option::is_none")] pub blocked_hosts: Option>, - /// HTTP(S) proxy for sandboxed traffic. With host rules, this is the built-in local proxy's upstream; credentials stay in the runtime, and Linux and macOS restrict the child to the local listener. Without host rules, Linux restricts egress to this endpoint but rejects credentials, and macOS proxying is cooperative. Windows enforcement depends on the application's networking stack. Configure credentials in the separate username/password fields. The transient local listener URL is never persisted. + /// HTTP(S) proxy for sandboxed traffic. This is the built-in local proxy's upstream: every sandboxed command reaches it through a loopback listener, so credentials stay in the runtime and never reach the child. On Windows the sandbox also needs local network access, because it reaches that listener over host loopback. Configure credentials in the separate username/password fields. The transient local listener URL is never persisted. #[serde(skip_serializing_if = "Option::is_none")] pub proxy: Option, } @@ -16771,7 +17750,7 @@ pub struct SessionFsSetProviderCapabilities { pub sqlite: Option, } -/// Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. +/// Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. A registered provider is authoritative for path interpretation and filesystem facts used by workspace permission validation. Paths are interpreted lexically; home-relative paths (`~` and `~/...`) and Windows drive-relative paths such as `C:foo` are unsupported. Until provider-side canonicalization is supported, providers must not expose symlinks inside allowed roots that escape those roots. /// ///
/// @@ -16787,7 +17766,7 @@ pub struct SessionFsSetProviderRequest { pub capabilities: Option, /// Path conventions used by this filesystem pub conventions: SessionFsSetProviderConventions, - /// Initial working directory for sessions + /// Absolute initial working directory for sessions. Registering the provider establishes this path as the root of its virtual namespace; the runtime does not require the provider to materialize or stat it before creating a session. pub initial_cwd: String, /// Path within each session's SessionFs where the runtime stores files for that session pub session_state_path: String, @@ -17293,7 +18272,7 @@ pub struct SessionManagedPermissions { /// Permission rules that block matching operations. Deny has highest precedence. #[serde(skip_serializing_if = "Option::is_none")] pub deny: Option>, - /// When set to `disable`, prevents bypass/allow-all permission modes. `allow-auto-only` blocks full allow-all but permits advisory auto-approval. Any other value is accepted rather than failing the session, but is enforced as `disable`: the key is only present to restrict something, so a mode this runtime cannot interpret fails closed to the most restrictive one it knows. Omit the key entirely to impose no restriction. + /// When set to `disable`, prevents bypass/allow-all permission modes. Advisory auto-approval remains available because normal prompt paths stay active. Any other value is accepted rather than failing the session, but is enforced as `disable`: the key is only present to restrict something, so a mode this runtime cannot interpret fails closed to the most restrictive one it knows. Omit the key entirely to impose no restriction. #[serde(skip_serializing_if = "Option::is_none")] pub disable_bypass_permissions_mode: Option, } @@ -17367,6 +18346,9 @@ pub struct SessionMetadataSnapshot { pub client_name: Option, /// The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot') pub current_mode: MetadataSnapshotCurrentMode, + /// Live indexed-search state for this session activation. Omitted by runtimes that do not expose indexed-search status; absence does not indicate enablement. + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_search: Option, /// User-provided name supplied at session construction (via `--name`), if any. Immutable after construction. #[serde(skip_serializing_if = "Option::is_none")] pub initial_name: Option, @@ -18101,6 +19083,51 @@ pub struct SessionOpenResult { pub status: SessionsOpenStatus, } +/// Plugin names (or specs) to disable in the session's authoritative working directory. +/// +///
+/// +/// **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 SessionPluginsDisableRequest { + /// Plugin names or "plugin@marketplace" specs to disable. Unknown names are ignored. Non-marketplace direct installs cannot be disabled via this API; uninstall them instead. Plugin-owned MCP servers are stopped in active sessions immediately; other plugin contributions remain available until each session reloads plugins. + pub names: Vec, +} + +/// Plugin names (or specs) to enable in the session's authoritative working directory. +/// +///
+/// +/// **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 SessionPluginsEnableRequest { + /// Plugin names or "plugin@marketplace" specs to enable. Unknown names are ignored. Non-marketplace direct installs are always enabled and cannot be toggled via this API. + pub names: Vec, +} + +/// Plugin source resolved relative to the session's authoritative working directory. +/// +///
+/// +/// **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 SessionPluginsInstallRequest { + /// Plugin install spec. Accepts the same forms as the CLI: "plugin@marketplace" (marketplace install), "owner/repo" or "owner/repo:subpath" (GitHub direct), an http/https/ssh URL, or a local path. Direct (non-marketplace) installs are deprecated and will produce a deprecationWarning in the result. + pub source: String, +} + /// Outcome of the prune operation: deleted IDs, dry-run candidates, skipped IDs, total bytes freed, and the dry-run flag. /// ///
@@ -20424,6 +21451,9 @@ pub struct TaskClientUpdateCancelled { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TaskCompleteData { + /// Structured blocker details when outcome is blocked + #[serde(skip_serializing_if = "Option::is_none")] + pub blocker: Option, /// Active autopilot objective ID evaluated by the completion reviewer #[serde(skip_serializing_if = "Option::is_none")] pub objective_id: Option, @@ -22213,6 +23243,946 @@ pub struct VisibilitySetResult { pub synced: bool, } +/// Parameters for cooperatively aborting a workflow body. +/// +///
+/// +/// **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 WorkflowAbortRequest { + /// Target session identifier + pub session_id: SessionId, + /// Workflow run identifier. + pub run_id: String, + /// Opaque token identifying the execution attempt to abort. + pub execution_token: String, +} + +/// Acknowledgement that a workflow request was accepted. +/// +///
+/// +/// **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 WorkflowAckResult {} + +/// Options for one workflow-scoped subagent call. +/// +///
+/// +/// **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 WorkflowAgentOptions { + /// Optional built-in or custom agent name whose definition configures the subagent. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent: Option, + /// Optional context tier override for the subagent. + #[serde(skip_serializing_if = "Option::is_none")] + pub context_tier: Option, + /// Optional label distinguishing otherwise identical memoized agent calls. + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + /// Optional model identifier for the subagent. + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + /// Optional reasoning effort override for the subagent. + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + /// Optional JSON Schema for structured agent output. + #[serde(skip_serializing_if = "Option::is_none")] + pub schema: Option, +} + +/// Parameters for one workflow-scoped subagent call. +/// +///
+/// +/// **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 WorkflowAgentRequest { + /// Opaque token identifying the current workflow execution attempt. + pub execution_token: String, + /// Subagent execution options. + pub opts: WorkflowAgentOptions, + /// Prompt to send to the subagent. + pub prompt: String, + /// Workflow run identifier that owns the subagent. + pub workflow_run_id: String, +} + +/// Result of one workflow-scoped subagent call. +/// +///
+/// +/// **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 WorkflowAgentResult { + /// Agent result, omitted when the agent produced no result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, +} + +/// Prompt-safe durable identity and live status for a direct workflow agent. +/// +///
+/// +/// **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 WorkflowAgentSummary { + /// Accumulated active agent time in milliseconds. + pub active_ms: i64, + /// Prompt-safe live activity text. + #[serde(skip_serializing_if = "Option::is_none")] + pub activity: Option, + /// Stable direct-agent identifier. + pub agent_id: String, + /// Registered agent type. + pub agent_type: String, + /// Epoch milliseconds when the agent completed. + #[serde(skip_serializing_if = "Option::is_none")] + pub completed_at: Option, + /// Friendly, non-unique name intended for display + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Friendly, non-unique name intended for display + pub label: String, + /// Phase identifier active when the agent was launched, or null. + pub phase_id: Option, + /// Model requested when the agent was launched. + #[serde(skip_serializing_if = "Option::is_none")] + pub requested_model: Option, + /// Concrete model resolved for the agent. + #[serde(skip_serializing_if = "Option::is_none")] + pub resolved_model: Option, + /// Owning workflow run identifier. + pub run_id: String, + /// Epoch milliseconds when the agent started. + #[serde(skip_serializing_if = "Option::is_none")] + pub started_at: Option, + /// Current durable or live agent status. + pub status: String, + /// Tool-call identifier that launched the agent. + pub tool_call_id: String, +} + +/// Parameters for cancelling a workflow run. +/// +///
+/// +/// **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 WorkflowCancelRequest { + /// Workflow run identifier. + pub run_id: String, +} + +/// Current workflow phase identity. +/// +///
+/// +/// **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 WorkflowCurrentPhase { + /// Current phase identifier. + pub id: String, + /// Zero-based declared phase ordinal, or null for an undeclared phase. + pub ordinal: Option, +} + +/// Declared or approved workflow resource ceilings. +/// +///
+/// +/// **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 WorkflowDeclaredLimits { + /// Maximum AI credits consumed by subagents and descendants. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_ai_credits: Option, + /// Maximum concurrently active subagents. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_concurrent_subagents: Option, + /// Maximum total subagents spawned by the run. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_total_subagents: Option, + /// Maximum accumulated active execution time in seconds. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout_seconds: Option, +} + +/// Parameters sent to the owning extension to execute a workflow closure. +/// +///
+/// +/// **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 WorkflowExecuteRequest { + /// Target session identifier + pub session_id: SessionId, + /// Registered workflow name. + pub name: String, + /// Workflow run identifier. + pub run_id: String, + /// Opaque token identifying this workflow execution attempt. + pub execution_token: String, + /// Workflow input value. + pub args: serde_json::Value, +} + +/// Result returned by an extension workflow closure. +/// +///
+/// +/// **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 WorkflowExecuteResult { + /// Workflow result value. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, +} + +/// Parameters for paging workflow progress. +/// +///
+/// +/// **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 WorkflowGetRunProgressRequest { + /// Exclusive forward cursor. + #[serde(skip_serializing_if = "Option::is_none")] + pub after_seq: Option, + /// Exclusive backward cursor. + #[serde(skip_serializing_if = "Option::is_none")] + pub before_seq: Option, + /// Maximum records to return. Defaults to 200 and is capped at 500. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// Optional phase identifier used to scope records and cursors. + #[serde(skip_serializing_if = "Option::is_none")] + pub phase_id: Option, + /// Workflow run identifier. + pub run_id: String, +} + +/// Parameters for retrieving a workflow run. +/// +///
+/// +/// **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 WorkflowGetRunRequest { + /// Workflow run identifier. + pub run_id: String, +} + +/// Parameters for reading a workflow journal entry. +/// +///
+/// +/// **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 WorkflowJournalGetRequest { + /// Opaque token identifying the current workflow execution attempt. + pub execution_token: String, + /// Namespaced journal key. + pub key: String, + /// Workflow run identifier. + pub run_id: String, +} + +/// Result of reading a workflow journal entry. +/// +///
+/// +/// **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 WorkflowJournalGetResult { + /// Whether the journal contained the requested key. + pub hit: bool, + /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. + #[serde(skip_serializing_if = "Option::is_none")] + pub result_json: Option, +} + +/// Parameters for storing a workflow journal entry. +/// +///
+/// +/// **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 WorkflowJournalPutRequest { + /// Opaque token identifying the current workflow execution attempt. + pub execution_token: String, + /// Namespaced journal key. + pub key: String, + /// JSON result to memoize. + pub result_json: serde_json::Value, + /// Workflow run identifier. + pub run_id: String, +} + +/// Parameters for paging workflow runs. +/// +///
+/// +/// **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 WorkflowListRunsRequest { + /// Exclusive forward cursor. + #[serde(skip_serializing_if = "Option::is_none")] + pub after_seq: Option, + /// Exclusive backward cursor. + #[serde(skip_serializing_if = "Option::is_none")] + pub before_seq: Option, + /// Maximum terminal runs to return. Defaults to 200 and is capped at 500. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +/// Durable workflow resource consumption. +/// +///
+/// +/// **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 WorkflowRunConsumed { + /// Accumulated active execution time in milliseconds. + pub active_ms: i64, + /// AI usage consumed by the run in nano-AIU. + pub nano_aiu: i64, + /// Total subagents spawned by the run. + pub subagents: i64, +} + +/// Prompt-safe terminal workflow outcome. +/// +///
+/// +/// **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 WorkflowRunTerminal { + /// Human-readable terminal error. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Machine-readable terminal failure. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure: Option, + /// Pause initiator metadata, or null when the run did not pause. + pub pause_info: Option, + /// Human-readable terminal reason. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Prompt-safe preview of the completed result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result_preview: Option, +} + +/// Durable workflow run summary with read-time live overlays. +/// +///
+/// +/// **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 WorkflowRunSummary { + /// Epoch milliseconds when the current active segment started, or null while inactive. + pub active_segment_started_at: Option, + /// Approved effective resource ceilings, or null until approved. + pub approved: Option, + /// Whether the durable run state currently passes runtime resume eligibility checks. + pub can_resume: bool, + /// Epoch milliseconds when the run completed, or null while nonterminal. + pub completed_at: Option, + /// Durable resource consumption. + pub consumed: WorkflowRunConsumed, + /// Epoch milliseconds when the run was created. + pub created_at: i64, + /// Current phase identity, or null before any phase is entered. + pub current_phase: Option, + /// Resource ceilings declared by the workflow. + pub declared_limits: WorkflowDeclaredLimits, + /// Number of phases declared by the workflow. + pub declared_phase_count: i64, + /// Human-readable workflow description. + pub description: String, + /// Number of direct workflow agents currently live. + pub live_agent_count: i64, + /// Epoch milliseconds when this live-overlay snapshot was observed. + pub observed_at: i64, + /// Monotonic durable run revision. + pub revision: i64, + /// Workflow run identifier. + pub run_id: String, + /// Epoch milliseconds when execution first started, or null before start. + pub started_at: Option, + /// Current workflow run status. + pub status: WorkflowRunStatus, + /// Terminal run outcome, or null while nonterminal. + pub terminal: Option, + /// Total direct workflow agents spawned across all attempts. + pub total_spawned_agent_count: i64, + /// Epoch milliseconds when the durable run was last updated. + pub updated_at: i64, + /// Registered workflow name. + pub workflow_name: String, +} + +/// A page of workflow runs in durable creation order. +/// +///
+/// +/// **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 WorkflowListRunsResult { + /// Whether terminal runs newer than this page exist. + #[serde(skip_serializing_if = "Option::is_none")] + pub has_more_newer: Option, + /// Newest terminal-run cursor in this page, or null when the terminal window is empty. + #[serde(skip_serializing_if = "Option::is_none")] + pub newest_seq: Option, + /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. + #[serde(skip_serializing_if = "Option::is_none")] + pub oldest_seq: Option, + /// Number of terminal runs older than this page. + #[serde(skip_serializing_if = "Option::is_none")] + pub omitted_older: Option, + /// Workflow run summaries in durable creation order. + pub runs: Vec, +} + +/// One ordered workflow progress line. +/// +///
+/// +/// **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 WorkflowLogLine { + /// Progress line kind. + pub kind: WorkflowLogLineKind, + /// Monotonic sequence number within the workflow run. + pub seq: i64, + /// Progress text. + pub text: String, +} + +/// Parameters for recording workflow progress. +/// +///
+/// +/// **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 WorkflowLogRequest { + /// Opaque token identifying the current workflow execution attempt. + pub execution_token: String, + /// Ordered progress lines to append. + pub lines: Vec, + /// Workflow run identifier. + pub run_id: String, +} + +/// Parameters for an owned durable pause 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 WorkflowPauseCheckpointRequest { + /// Opaque token identifying the execution attempt that reached the checkpoint. + pub execution_token: String, + /// Stable author-defined checkpoint key. + pub key: String, + /// Workflow run identifier. + pub run_id: String, +} + +/// +///
+/// +/// **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 WorkflowPauseCheckpointResult { + /// Whether this execution attempt must pause or may continue. + pub action: WorkflowPauseCheckpointAction, +} + +/// Parameters for pausing a running workflow. +/// +///
+/// +/// **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 WorkflowPauseRequest { + /// Workflow run identifier. + pub run_id: String, +} + +/// Durable lifecycle and timing for one workflow 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 WorkflowPhaseObservation { + /// Completed active time accumulated by this phase in milliseconds. + pub accumulated_active_ms: i64, + /// Epoch milliseconds when this phase completed; for a skipped phase, the synthetic skip timestamp (equal to `startedAt`). + #[serde(skip_serializing_if = "Option::is_none")] + pub completed_at: Option, + /// Current live active time for this phase in milliseconds. + pub current_active_ms: i64, + /// Optional human-readable phase detail. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// Number of times execution entered this phase. + pub entry_count: i64, + /// Phase identifier. + pub id: String, + /// Most recent run attempt that entered this phase, or `0` if the phase has never been entered. + pub last_entered_run_attempt: i64, + /// Direct agents in this phase that are currently live. + pub live_agent_count: i64, + /// Zero-based declared phase ordinal, or null for an undeclared phase. + pub ordinal: Option, + /// Epoch milliseconds when this phase first started; for a skipped phase, the synthetic skip timestamp (equal to `completedAt`). + #[serde(skip_serializing_if = "Option::is_none")] + pub started_at: Option, + /// Derived lifecycle state of the phase. + pub status: WorkflowPhaseStatus, + /// Human-readable phase title. + pub title: String, + /// Total direct agents associated with this phase. + pub total_agent_count: i64, +} + +/// One durable workflow progress record. +/// +///
+/// +/// **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 WorkflowProgressLine { + /// Resume attempt that emitted this record. + pub attempt: i64, + /// Progress record kind. + pub kind: WorkflowLogLineKind, + /// Phase active when the record was emitted, or null before any phase. + pub phase_id: Option, + /// Epoch milliseconds when the record was persisted. + pub recorded_at: i64, + /// Global monotonic sequence number within the run. + pub seq: i64, + /// Prompt-safe progress text. + pub text: String, +} + +/// A bidirectional page of workflow progress. +/// +///
+/// +/// **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 WorkflowProgressPage { + /// Whether progress records newer than this page exist. + pub has_more_newer: bool, + /// Whether progress records older than this page exist. + pub has_more_older: bool, + /// Newest sequence number in this page, or null when empty. + pub newest_seq: Option, + /// Oldest sequence number in this page, or null when empty. + pub oldest_seq: Option, + /// Progress records in sequence order. + pub records: Vec, + /// Run revision reflected by this page. + pub revision: i64, +} + +/// Wire-only per-invocation workflow resource ceiling overrides. +/// +///
+/// +/// **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 WorkflowRunLimits { + /// Maximum AI credits consumed by workflow subagents and their descendants. The post-paid ceiling is soft: parallel turns can settle beyond it before the run stops. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_ai_credits: Option, + /// Maximum number of workflow subagents that may run concurrently. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_concurrent_subagents: Option, + /// Maximum total number of workflow subagents that may be admitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_total_subagents: Option, + /// Maximum accumulated active-execution time in seconds. Active execution includes the entire extension body, subprocess waits, queued-agent waits, and sleeps; time between resumed attempts is not counted. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout_seconds: Option, +} + +/// Parameters for resuming a workflow run from its persisted identity. +/// +///
+/// +/// **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 WorkflowResumeRequest { + /// Optional per-invocation resource ceiling overrides. + #[serde(skip_serializing_if = "Option::is_none")] + pub limits: Option, + /// Whether to emit workflow phase names to the session transcript. + #[serde(skip_serializing_if = "Option::is_none")] + pub log_phase_names: Option, + /// Whether to notify the originating session when the workflow completes. + #[serde(skip_serializing_if = "Option::is_none")] + pub notify_on_complete: Option, + /// Workflow run identifier. + pub run_id: String, +} + +/// Complete current or terminal workflow run envelope. +/// +///
+/// +/// **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 WorkflowRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, + /// Error message for an errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Machine-readable failure details for a halted or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure: Option, + /// Structured pause initiator metadata for a paused attempt. + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_info: Option, + /// Reason for a halted or cancelled run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Completed workflow result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Workflow run identifier. + pub run_id: String, + /// Partial journal and progress snapshot for a halted, cancelled, or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot: Option, + /// Current or terminal workflow run status. + pub status: WorkflowRunStatus, +} + +/// Resolved persisted workflow identity and resumed run envelope. +/// +///
+/// +/// **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 WorkflowResumeResult { + /// Terminal resumed run envelope. + pub run: WorkflowRunResult, + /// Persisted workflow name resolved for the resumed run. + pub workflow_name: String, +} + +/// Full workflow run observability detail. +/// +///
+/// +/// **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 WorkflowRunDetail { + /// Epoch milliseconds when the current active segment started, or null while inactive. + pub active_segment_started_at: Option, + /// Durable identities and live statuses for direct workflow agents. + pub agents: Vec, + /// Approved effective resource ceilings, or null until approved. + pub approved: Option, + /// Whether the durable run state currently passes runtime resume eligibility checks. + pub can_resume: bool, + /// Epoch milliseconds when the run completed, or null while nonterminal. + pub completed_at: Option, + /// Durable resource consumption. + pub consumed: WorkflowRunConsumed, + /// Epoch milliseconds when the run was created. + pub created_at: i64, + /// Current phase identity, or null before any phase is entered. + pub current_phase: Option, + /// Resource ceilings declared by the workflow. + pub declared_limits: WorkflowDeclaredLimits, + /// Number of phases declared by the workflow. + pub declared_phase_count: i64, + /// Human-readable workflow description. + pub description: String, + /// Number of direct workflow agents currently live. + pub live_agent_count: i64, + /// Epoch milliseconds when this live-overlay snapshot was observed. + pub observed_at: i64, + /// Lifecycle and timing observations for each workflow phase. + pub phases: Vec, + /// Bidirectional page of durable workflow progress. + pub progress: WorkflowProgressPage, + /// Monotonic durable run revision. + pub revision: i64, + /// Workflow run identifier. + pub run_id: String, + /// Epoch milliseconds when execution first started, or null before start. + pub started_at: Option, + /// Current workflow run status. + pub status: WorkflowRunStatus, + /// Terminal run outcome, or null while nonterminal. + pub terminal: Option, + /// Total direct workflow agents spawned across all attempts. + pub total_spawned_agent_count: i64, + /// Epoch milliseconds when the durable run was last updated. + pub updated_at: i64, + /// Registered workflow name. + pub workflow_name: String, +} + +/// Options controlling workflow invocation. +/// +///
+/// +/// **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 WorkflowRunOptions { + /// Per-invocation resource ceiling overrides. + #[serde(skip_serializing_if = "Option::is_none")] + pub limits: Option, + /// Whether to emit workflow phase names to the session transcript. + #[serde(skip_serializing_if = "Option::is_none")] + pub log_phase_names: Option, + /// Whether to notify the originating session when the workflow completes. + #[serde(skip_serializing_if = "Option::is_none")] + pub notify_on_complete: Option, + /// Run identifier whose journal and progress should seed this resumed run. + #[serde(skip_serializing_if = "Option::is_none")] + pub resume_from_run_id: Option, +} + +/// Parameters for invoking a registered workflow. +/// +///
+/// +/// **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 WorkflowRunRequest { + /// Workflow input value. + pub args: serde_json::Value, + /// Registered workflow name. + pub name: String, + /// Workflow invocation options. + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, +} + +/// Internal parameters for resuming a workflow run from a tool. +/// +///
+/// +/// **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 WorkflowToolResumeRequest { + /// Optional per-invocation resource ceiling overrides. + #[serde(skip_serializing_if = "Option::is_none")] + pub limits: Option, + /// Workflow run identifier. + pub run_id: String, + /// Opaque identifier of the originating tool call. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, +} + +/// Options for an internal tool-originated workflow invocation. +/// +///
+/// +/// **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 WorkflowToolRunOptions { + /// Per-invocation resource ceiling overrides. + #[serde(skip_serializing_if = "Option::is_none")] + pub limits: Option, + /// Run identifier whose journal and progress should seed this resumed run. + #[serde(skip_serializing_if = "Option::is_none")] + pub resume_from_run_id: Option, +} + +/// Internal parameters for invoking a registered workflow from a tool. +/// +///
+/// +/// **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 WorkflowToolRunRequest { + /// Workflow input value. + pub args: serde_json::Value, + /// Registered workflow name. + pub name: String, + /// Tool-originated workflow invocation options. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) options: Option, + /// Opaque identifier of the originating tool call. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, +} + /// A single changed file and its unified diff. /// ///
@@ -22823,6 +24793,19 @@ pub struct WorkspacesWriteAutopilotObjectiveResult { pub operation: String, } +/// +///
+/// +/// **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 AhpRegisterEndpointResult { + pub endpoint_id: String, +} + /// List of Copilot models available to the resolved user, including capabilities and billing metadata. /// ///
@@ -23886,7 +25869,303 @@ pub struct SessionFactoryRunResult { pub status: FactoryRunStatus, } -/// Resolved persisted factory identity and resumed run envelope. +/// Resolved persisted factory identity and resumed run envelope. +/// +///
+/// +/// **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 SessionFactoryResumeResult { + /// Persisted factory name resolved for the resumed run. + pub factory_name: String, + /// Terminal resumed run envelope. + pub run: FactoryRunResult, +} + +/// Complete current or terminal factory run envelope. +/// +///
+/// +/// **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 SessionFactoryRunFromToolResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, + /// Error message for an errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Machine-readable failure details for a halted or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure: Option, + /// Structured pause initiator metadata for a paused attempt. + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_info: Option, + /// Reason for a halted or cancelled run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Completed factory result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Factory run identifier. + pub run_id: String, + /// Partial journal and progress snapshot for a halted, cancelled, or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot: Option, + /// Current or terminal factory run status. + pub status: FactoryRunStatus, +} + +/// Resolved persisted factory identity and resumed run envelope. +/// +///
+/// +/// **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 SessionFactoryResumeFromToolResult { + /// Persisted factory name resolved for the resumed run. + pub factory_name: String, + /// Terminal resumed run envelope. + pub run: FactoryRunResult, +} + +/// Complete current or terminal factory run envelope. +/// +///
+/// +/// **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 SessionFactoryGetRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, + /// Error message for an errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Machine-readable failure details for a halted or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure: Option, + /// Structured pause initiator metadata for a paused attempt. + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_info: Option, + /// Reason for a halted or cancelled run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Completed factory result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Factory run identifier. + pub run_id: String, + /// Partial journal and progress snapshot for a halted, cancelled, or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot: Option, + /// Current or terminal factory run status. + pub status: FactoryRunStatus, +} + +/// A page of factory runs in durable creation order. +/// +///
+/// +/// **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 SessionFactoryListRunsResult { + /// Whether terminal runs newer than this page exist. + #[serde(skip_serializing_if = "Option::is_none")] + pub has_more_newer: Option, + /// Newest terminal-run cursor in this page, or null when the terminal window is empty. + #[serde(skip_serializing_if = "Option::is_none")] + pub newest_seq: Option, + /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. + #[serde(skip_serializing_if = "Option::is_none")] + pub oldest_seq: Option, + /// Number of terminal runs older than this page. + #[serde(skip_serializing_if = "Option::is_none")] + pub omitted_older: Option, + /// Factory run summaries in durable creation order. + pub runs: Vec, +} + +/// Full factory run observability detail. +/// +///
+/// +/// **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 SessionFactoryGetRunDetailResult { + /// Epoch milliseconds when the current active segment started, or null while inactive. + pub active_segment_started_at: Option, + /// Durable identities and live statuses for direct factory agents. + pub agents: Vec, + /// Approved effective resource ceilings, or null until approved. + pub approved: Option, + /// Whether the durable run state currently passes runtime resume eligibility checks. + pub can_resume: bool, + /// Epoch milliseconds when the run completed, or null while nonterminal. + pub completed_at: Option, + /// Durable resource consumption. + pub consumed: FactoryRunConsumed, + /// Epoch milliseconds when the run was created. + pub created_at: i64, + /// Current phase identity, or null before any phase is entered. + pub current_phase: Option, + /// Resource ceilings declared by the factory. + pub declared_limits: FactoryDeclaredLimits, + /// Number of phases declared by the factory. + pub declared_phase_count: i64, + /// Human-readable factory description. + pub description: String, + /// Registered factory name. + pub factory_name: String, + /// Number of direct factory agents currently live. + pub live_agent_count: i64, + /// Epoch milliseconds when this live-overlay snapshot was observed. + pub observed_at: i64, + /// Lifecycle and timing observations for each factory phase. + pub phases: Vec, + /// Bidirectional page of durable factory progress. + pub progress: FactoryProgressPage, + /// Monotonic durable run revision. + pub revision: i64, + /// Factory run identifier. + pub run_id: String, + /// Epoch milliseconds when execution first started, or null before start. + pub started_at: Option, + /// Current factory run status. + pub status: FactoryRunStatus, + /// Terminal run outcome, or null while nonterminal. + pub terminal: Option, + /// Total direct factory agents spawned across all attempts. + pub total_spawned_agent_count: i64, + /// Epoch milliseconds when the durable run was last updated. + pub updated_at: i64, +} + +/// A bidirectional page of factory progress. +/// +///
+/// +/// **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 SessionFactoryGetRunProgressResult { + /// Whether progress records newer than this page exist. + pub has_more_newer: bool, + /// Whether progress records older than this page exist. + pub has_more_older: bool, + /// Newest sequence number in this page, or null when empty. + pub newest_seq: Option, + /// Oldest sequence number in this page, or null when empty. + pub oldest_seq: Option, + /// Progress records in sequence order. + pub records: Vec, + /// Run revision reflected by this page. + pub revision: i64, +} + +/// Complete current or terminal factory run envelope. +/// +///
+/// +/// **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 SessionFactoryCancelResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, + /// Error message for an errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Machine-readable failure details for a halted or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure: Option, + /// Structured pause initiator metadata for a paused attempt. + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_info: Option, + /// Reason for a halted or cancelled run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Completed factory result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Factory run identifier. + pub run_id: String, + /// Partial journal and progress snapshot for a halted, cancelled, or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot: Option, + /// Current or terminal factory run status. + pub status: FactoryRunStatus, +} + +/// Complete current or terminal factory run envelope. +/// +///
+/// +/// **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 SessionFactoryPauseResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, + /// Error message for an errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Machine-readable failure details for a halted or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure: Option, + /// Structured pause initiator metadata for a paused attempt. + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_info: Option, + /// Reason for a halted or cancelled run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Completed factory result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Factory run identifier. + pub run_id: String, + /// Partial journal and progress snapshot for a halted, cancelled, or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot: Option, + /// Current or terminal factory run status. + pub status: FactoryRunStatus, +} + /// ///
/// @@ -23896,14 +26175,125 @@ pub struct SessionFactoryRunResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryResumeResult { - /// Persisted factory name resolved for the resumed run. - pub factory_name: String, +pub struct SessionFactoryPauseAtCheckpointResult { + /// Whether this execution attempt must pause or may continue. + pub action: FactoryPauseCheckpointAction, +} + +/// Acknowledgement that a factory request was accepted. +/// +///
+/// +/// **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 SessionFactoryLogResult {} + +/// Result of one factory-scoped subagent call. +/// +///
+/// +/// **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 SessionFactoryAgentResult { + /// Agent result, omitted when the agent produced no result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, +} + +/// Result of reading a factory journal entry. +/// +///
+/// +/// **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 SessionFactoryJournalGetResult { + /// Whether the journal contained the requested key. + pub hit: bool, + /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. + #[serde(skip_serializing_if = "Option::is_none")] + pub result_json: Option, +} + +/// Acknowledgement that a factory request was accepted. +/// +///
+/// +/// **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 SessionFactoryJournalPutResult {} + +/// Complete current or terminal workflow run envelope. +/// +///
+/// +/// **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 SessionWorkflowRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, + /// Error message for an errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Machine-readable failure details for a halted or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure: Option, + /// Structured pause initiator metadata for a paused attempt. + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_info: Option, + /// Reason for a halted or cancelled run. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// Completed workflow result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Workflow run identifier. + pub run_id: String, + /// Partial journal and progress snapshot for a halted, cancelled, or errored run. + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot: Option, + /// Current or terminal workflow run status. + pub status: WorkflowRunStatus, +} + +/// Resolved persisted workflow identity and resumed run envelope. +/// +///
+/// +/// **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 SessionWorkflowResumeResult { /// Terminal resumed run envelope. - pub run: FactoryRunResult, + pub run: WorkflowRunResult, + /// Persisted workflow name resolved for the resumed run. + pub workflow_name: String, } -/// Complete current or terminal factory run envelope. +/// Complete current or terminal workflow run envelope. /// ///
/// @@ -23913,7 +26303,7 @@ pub struct SessionFactoryResumeResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryRunFromToolResult { +pub struct SessionWorkflowRunFromToolResult { /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. #[serde(skip_serializing_if = "Option::is_none")] pub attempt: Option, @@ -23929,19 +26319,19 @@ pub struct SessionFactoryRunFromToolResult { /// Reason for a halted or cancelled run. #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, - /// Completed factory result. + /// Completed workflow result. #[serde(skip_serializing_if = "Option::is_none")] pub result: Option, - /// Factory run identifier. + /// Workflow run identifier. pub run_id: String, /// Partial journal and progress snapshot for a halted, cancelled, or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, + /// Current or terminal workflow run status. + pub status: WorkflowRunStatus, } -/// Resolved persisted factory identity and resumed run envelope. +/// Resolved persisted workflow identity and resumed run envelope. /// ///
/// @@ -23951,14 +26341,14 @@ pub struct SessionFactoryRunFromToolResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryResumeFromToolResult { - /// Persisted factory name resolved for the resumed run. - pub factory_name: String, +pub struct SessionWorkflowResumeFromToolResult { /// Terminal resumed run envelope. - pub run: FactoryRunResult, + pub run: WorkflowRunResult, + /// Persisted workflow name resolved for the resumed run. + pub workflow_name: String, } -/// Complete current or terminal factory run envelope. +/// Complete current or terminal workflow run envelope. /// ///
/// @@ -23968,7 +26358,7 @@ pub struct SessionFactoryResumeFromToolResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryGetRunResult { +pub struct SessionWorkflowGetRunResult { /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. #[serde(skip_serializing_if = "Option::is_none")] pub attempt: Option, @@ -23984,19 +26374,19 @@ pub struct SessionFactoryGetRunResult { /// Reason for a halted or cancelled run. #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, - /// Completed factory result. + /// Completed workflow result. #[serde(skip_serializing_if = "Option::is_none")] pub result: Option, - /// Factory run identifier. + /// Workflow run identifier. pub run_id: String, /// Partial journal and progress snapshot for a halted, cancelled, or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, + /// Current or terminal workflow run status. + pub status: WorkflowRunStatus, } -/// A page of factory runs in durable creation order. +/// A page of workflow runs in durable creation order. /// ///
/// @@ -24006,7 +26396,7 @@ pub struct SessionFactoryGetRunResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryListRunsResult { +pub struct SessionWorkflowListRunsResult { /// Whether terminal runs newer than this page exist. #[serde(skip_serializing_if = "Option::is_none")] pub has_more_newer: Option, @@ -24019,11 +26409,11 @@ pub struct SessionFactoryListRunsResult { /// Number of terminal runs older than this page. #[serde(skip_serializing_if = "Option::is_none")] pub omitted_older: Option, - /// Factory run summaries in durable creation order. - pub runs: Vec, + /// Workflow run summaries in durable creation order. + pub runs: Vec, } -/// Full factory run observability detail. +/// Full workflow run observability detail. /// ///
/// @@ -24033,56 +26423,56 @@ pub struct SessionFactoryListRunsResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryGetRunDetailResult { +pub struct SessionWorkflowGetRunDetailResult { /// Epoch milliseconds when the current active segment started, or null while inactive. pub active_segment_started_at: Option, - /// Durable identities and live statuses for direct factory agents. - pub agents: Vec, + /// Durable identities and live statuses for direct workflow agents. + pub agents: Vec, /// Approved effective resource ceilings, or null until approved. - pub approved: Option, + pub approved: Option, /// Whether the durable run state currently passes runtime resume eligibility checks. pub can_resume: bool, /// Epoch milliseconds when the run completed, or null while nonterminal. pub completed_at: Option, /// Durable resource consumption. - pub consumed: FactoryRunConsumed, + pub consumed: WorkflowRunConsumed, /// Epoch milliseconds when the run was created. pub created_at: i64, /// Current phase identity, or null before any phase is entered. - pub current_phase: Option, - /// Resource ceilings declared by the factory. - pub declared_limits: FactoryDeclaredLimits, - /// Number of phases declared by the factory. + pub current_phase: Option, + /// Resource ceilings declared by the workflow. + pub declared_limits: WorkflowDeclaredLimits, + /// Number of phases declared by the workflow. pub declared_phase_count: i64, - /// Human-readable factory description. + /// Human-readable workflow description. pub description: String, - /// Registered factory name. - pub factory_name: String, - /// Number of direct factory agents currently live. + /// Number of direct workflow agents currently live. pub live_agent_count: i64, /// Epoch milliseconds when this live-overlay snapshot was observed. pub observed_at: i64, - /// Lifecycle and timing observations for each factory phase. - pub phases: Vec, - /// Bidirectional page of durable factory progress. - pub progress: FactoryProgressPage, + /// Lifecycle and timing observations for each workflow phase. + pub phases: Vec, + /// Bidirectional page of durable workflow progress. + pub progress: WorkflowProgressPage, /// Monotonic durable run revision. pub revision: i64, - /// Factory run identifier. + /// Workflow run identifier. pub run_id: String, /// Epoch milliseconds when execution first started, or null before start. pub started_at: Option, - /// Current factory run status. - pub status: FactoryRunStatus, + /// Current workflow run status. + pub status: WorkflowRunStatus, /// Terminal run outcome, or null while nonterminal. - pub terminal: Option, - /// Total direct factory agents spawned across all attempts. + pub terminal: Option, + /// Total direct workflow agents spawned across all attempts. pub total_spawned_agent_count: i64, /// Epoch milliseconds when the durable run was last updated. pub updated_at: i64, + /// Registered workflow name. + pub workflow_name: String, } -/// A bidirectional page of factory progress. +/// A bidirectional page of workflow progress. /// ///
/// @@ -24092,7 +26482,7 @@ pub struct SessionFactoryGetRunDetailResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryGetRunProgressResult { +pub struct SessionWorkflowGetRunProgressResult { /// Whether progress records newer than this page exist. pub has_more_newer: bool, /// Whether progress records older than this page exist. @@ -24102,12 +26492,12 @@ pub struct SessionFactoryGetRunProgressResult { /// Oldest sequence number in this page, or null when empty. pub oldest_seq: Option, /// Progress records in sequence order. - pub records: Vec, + pub records: Vec, /// Run revision reflected by this page. pub revision: i64, } -/// Complete current or terminal factory run envelope. +/// Complete current or terminal workflow run envelope. /// ///
/// @@ -24117,7 +26507,7 @@ pub struct SessionFactoryGetRunProgressResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryCancelResult { +pub struct SessionWorkflowCancelResult { /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. #[serde(skip_serializing_if = "Option::is_none")] pub attempt: Option, @@ -24133,19 +26523,19 @@ pub struct SessionFactoryCancelResult { /// Reason for a halted or cancelled run. #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, - /// Completed factory result. + /// Completed workflow result. #[serde(skip_serializing_if = "Option::is_none")] pub result: Option, - /// Factory run identifier. + /// Workflow run identifier. pub run_id: String, /// Partial journal and progress snapshot for a halted, cancelled, or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, + /// Current or terminal workflow run status. + pub status: WorkflowRunStatus, } -/// Complete current or terminal factory run envelope. +/// Complete current or terminal workflow run envelope. /// ///
/// @@ -24155,7 +26545,7 @@ pub struct SessionFactoryCancelResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryPauseResult { +pub struct SessionWorkflowPauseResult { /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. #[serde(skip_serializing_if = "Option::is_none")] pub attempt: Option, @@ -24171,16 +26561,16 @@ pub struct SessionFactoryPauseResult { /// Reason for a halted or cancelled run. #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, - /// Completed factory result. + /// Completed workflow result. #[serde(skip_serializing_if = "Option::is_none")] pub result: Option, - /// Factory run identifier. + /// Workflow run identifier. pub run_id: String, /// Partial journal and progress snapshot for a halted, cancelled, or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub snapshot: Option, - /// Current or terminal factory run status. - pub status: FactoryRunStatus, + /// Current or terminal workflow run status. + pub status: WorkflowRunStatus, } /// @@ -24192,12 +26582,12 @@ pub struct SessionFactoryPauseResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryPauseAtCheckpointResult { +pub struct SessionWorkflowPauseAtCheckpointResult { /// Whether this execution attempt must pause or may continue. - pub action: FactoryPauseCheckpointAction, + pub action: WorkflowPauseCheckpointAction, } -/// Acknowledgement that a factory request was accepted. +/// Acknowledgement that a workflow request was accepted. /// ///
/// @@ -24207,9 +26597,9 @@ pub struct SessionFactoryPauseAtCheckpointResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryLogResult {} +pub struct SessionWorkflowLogResult {} -/// Result of one factory-scoped subagent call. +/// Result of one workflow-scoped subagent call. /// ///
/// @@ -24219,13 +26609,13 @@ pub struct SessionFactoryLogResult {} ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryAgentResult { +pub struct SessionWorkflowAgentResult { /// Agent result, omitted when the agent produced no result. #[serde(skip_serializing_if = "Option::is_none")] pub result: Option, } -/// Result of reading a factory journal entry. +/// Result of reading a workflow journal entry. /// ///
/// @@ -24235,7 +26625,7 @@ pub struct SessionFactoryAgentResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryJournalGetResult { +pub struct SessionWorkflowJournalGetResult { /// Whether the journal contained the requested key. pub hit: bool, /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. @@ -24243,7 +26633,7 @@ pub struct SessionFactoryJournalGetResult { pub result_json: Option, } -/// Acknowledgement that a factory request was accepted. +/// Acknowledgement that a workflow request was accepted. /// ///
/// @@ -24253,7 +26643,7 @@ pub struct SessionFactoryJournalGetResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionFactoryJournalPutResult {} +pub struct SessionWorkflowJournalPutResult {} /// Identifies the target session. /// @@ -24333,6 +26723,9 @@ pub struct SessionModelSwitchToResult { /// Persistence failure encountered after applying the model switch. #[serde(skip_serializing_if = "Option::is_none")] pub persistence_error: Option, + /// Stable queue item identifier when this request was enqueued. Remains present if the item drains before the response is returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_id: Option, /// Lifecycle result for the requested switch #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, @@ -24400,6 +26793,9 @@ pub struct SessionModelApplyStartupOverlayResult { /// Persistence failure encountered after applying the model switch. #[serde(skip_serializing_if = "Option::is_none")] pub persistence_error: Option, + /// Stable queue item identifier when this request was enqueued. Remains present if the item drains before the response is returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_id: Option, /// Lifecycle result for the requested switch #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, @@ -26310,6 +28706,227 @@ pub struct SessionMcpResourcesListTemplatesResult { pub resource_templates: Vec, } +/// Identifies the target session. +/// +///
+/// +/// **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 SessionConnectorsGetCapabilitiesParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +/// +///
+/// +/// **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 SessionConnectorsGetCapabilitiesResult { + /// Connector API contract version. + pub api_version: i64, + /// Current session availability. Disabled availability is reported without making a Connector request. + pub availability: ConnectorAvailability, + /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. + pub consent_continuation: bool, + /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. + pub max_deadline_ms: i64, + /// Maximum accepted polling attempts for one continuation call. + pub max_poll_attempts: i64, + /// Maximum accepted delay in milliseconds between polling attempts. + pub max_poll_interval_ms: i64, + /// Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. + pub opaque_account_selection: bool, +} + +/// Identifies the target session. +/// +///
+/// +/// **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 SessionConnectorsGetStatusParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +/// +///
+/// +/// **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 SessionConnectorsGetStatusResult { + /// Opaque account selection pinned to this session, when one has been selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + /// Connector API contract version. + pub api_version: i64, + /// Current feature and session availability. + pub availability: ConnectorAvailability, + /// Latest validated catalog snapshot, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub catalog: Option, + /// Number of active opaque connection continuations. + pub pending_connections: i64, + /// Live MCP status for every Connector-owned runtime server. + pub runtime_servers: Vec, +} + +/// Validated Connector catalog snapshot cached by the session. +/// +///
+/// +/// **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 SessionConnectorsListResult { + /// Validated catalog entries in service order. + pub connectors: Vec, + /// Unix epoch milliseconds when this snapshot was accepted. + pub refreshed_at_ms: i64, + /// Monotonically increasing session-local catalog revision. + pub revision: i64, +} + +/// Validated Connector catalog snapshot cached by the session. +/// +///
+/// +/// **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 SessionConnectorsRefreshResult { + /// Validated catalog entries in service order. + pub connectors: Vec, + /// Unix epoch milliseconds when this snapshot was accepted. + pub refreshed_at_ms: i64, + /// Monotonically increasing session-local catalog revision. + pub revision: i64, +} + +/// Authoritative result after disconnect and MCP reconciliation. +/// +///
+/// +/// **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 SessionConnectorsDisconnectResult { + /// Whether the service accepted the idempotent disconnect. + pub disconnected: bool, + /// Fresh authoritative session state after removing Connector-owned MCP servers. + pub status: ConnectorStatus, +} + +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +/// +///
+/// +/// **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 SessionConnectorsReconcileResult { + /// Opaque account selection pinned to this session, when one has been selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_id: Option, + /// Connector API contract version. + pub api_version: i64, + /// Current feature and session availability. + pub availability: ConnectorAvailability, + /// Latest validated catalog snapshot, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub catalog: Option, + /// Number of active opaque connection continuations. + pub pending_connections: i64, + /// Live MCP status for every Connector-owned runtime server. + pub runtime_servers: Vec, +} + +/// Identifies the target session. +/// +///
+/// +/// **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 SessionManagedSettingsGetParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// +///
+/// +/// **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 SessionManagedSettingsGetResult { + /// Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. + pub bypass_permissions_disabled: bool, + /// Whether a session-local permissions layer injected by the SDK host was present + #[serde(skip_serializing_if = "Option::is_none")] + pub client_managed: Option, + /// Whether an actual device MDM/plist/registry/file managed-settings layer was present + pub device_managed: bool, + /// Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. + pub fail_closed: bool, + /// The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + pub managed_keys: Vec, + /// Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. + #[serde(skip_serializing_if = "Option::is_none")] + pub permissions_allow_intersected: Option, + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_helper_managed: Option, + /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_enabled_by_undetermined_policy: Option, + /// Whether the server (account/org) managed-settings layer was present + pub server_managed: bool, + /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. + #[serde(skip_serializing_if = "Option::is_none")] + pub settings: Option, + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + pub source: ManagedSettingsResolvedSource, +} + /// Identifies the target session. /// ///
@@ -26340,6 +28957,146 @@ pub struct SessionPluginsListResult { pub plugins: Vec, } +/// Result of installing a plugin. +/// +///
+/// +/// **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 SessionPluginsInstallResult { + /// Set when the install path is deprecated (e.g. direct repo / URL / local installs). Callers should surface this to end users. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecation_warning: Option, + /// The newly installed plugin's metadata + pub plugin: InstalledPluginInfo, + /// Optional post-install message provided by the plugin (e.g. setup instructions) + #[serde(skip_serializing_if = "Option::is_none")] + pub post_install_message: Option, + /// Number of skills discovered and installed from the plugin + pub skills_installed: i64, + /// Where the completed plugin tree was staged before atomic promotion + #[serde(skip_serializing_if = "Option::is_none")] + pub staging_mode: Option, +} + +/// Result of updating a single plugin. +/// +///
+/// +/// **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 SessionPluginsUpdateResult { + /// Version after the update, when reported by the plugin manifest + #[serde(skip_serializing_if = "Option::is_none")] + pub new_version: Option, + /// Version that was previously installed, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub previous_version: Option, + /// Number of skills discovered and installed after the update + pub skills_installed: i64, +} + +/// Identifies the target session. +/// +///
+/// +/// **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 SessionPluginsMarketplacesListParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// All registered marketplaces, including built-in defaults. +/// +///
+/// +/// **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 SessionPluginsMarketplacesListResult { + /// Registered marketplaces + pub marketplaces: Vec, +} + +/// Result of registering a new marketplace. +/// +///
+/// +/// **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 SessionPluginsMarketplacesAddResult { + /// Final name of the marketplace as resolved from its manifest + pub name: String, +} + +/// Outcome of the remove attempt, including dependent-plugin info when applicable. +/// +///
+/// +/// **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 SessionPluginsMarketplacesRemoveResult { + /// Names of installed plugins that prevented removal. Populated only when `removed=false`. + #[serde(skip_serializing_if = "Option::is_none")] + pub dependent_plugins: Option>, + /// True when the marketplace was actually removed. False when removal was skipped because the marketplace has dependent plugins and `force` was not set. + pub removed: bool, +} + +/// Plugins advertised by the marketplace. +/// +///
+/// +/// **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 SessionPluginsMarketplacesBrowseResult { + /// Plugins advertised by the marketplace + pub plugins: Vec, +} + +/// Result of refreshing one or more marketplace catalogs. +/// +///
+/// +/// **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 SessionPluginsMarketplacesRefreshResult { + /// Per-marketplace refresh results in deterministic order. + pub results: Vec, +} + /// A snapshot of the provider endpoint the session is currently configured to talk to. /// ///
@@ -26475,6 +29232,9 @@ pub struct SessionToolsGetBuiltinDescriptorsResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionToolsTaskCompleteEventDataResult { + /// Structured blocker details when outcome is blocked + #[serde(skip_serializing_if = "Option::is_none")] + pub blocker: Option, /// Active autopilot objective ID evaluated by the completion reviewer #[serde(skip_serializing_if = "Option::is_none")] pub objective_id: Option, @@ -27298,6 +30058,9 @@ pub struct SessionMetadataSnapshotResult { pub client_name: Option, /// The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot') pub current_mode: MetadataSnapshotCurrentMode, + /// Live indexed-search state for this session activation. Omitted by runtimes that do not expose indexed-search status; absence does not indicate enablement. + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_search: Option, /// User-provided name supplied at session construction (via `--name`), if any. Immutable after construction. #[serde(skip_serializing_if = "Option::is_none")] pub initial_name: Option, @@ -28043,6 +30806,9 @@ pub struct SessionQueueSnapshotParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionQueueSnapshotResult { + /// Queue item identifier of a model switch that has been dequeued but not yet applied. + #[serde(skip_serializing_if = "Option::is_none")] + pub in_flight_model_change_id: Option, /// Insertion orders for queued items, aligned with `items`. #[serde(skip_serializing_if = "Option::is_none")] pub item_orders: Option>, @@ -28115,6 +30881,36 @@ pub struct SessionQueueUpdateTextResult { pub updated: bool, } +/// Result of removing a queued item. +/// +///
+/// +/// **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 SessionQueueWithdrawMessageResult { + /// True when the addressed item was removed. + pub removed: bool, +} + +/// Result of editing a queued message. +/// +///
+/// +/// **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 SessionQueueAppendSteeringResult { + /// True when the stored text changed. + pub updated: bool, +} + /// Result of duplicating a queued item. /// ///
@@ -28779,6 +31575,18 @@ pub struct ProviderTokenGetTokenResult { #[serde(rename_all = "camelCase")] pub struct FactoryAbortResult {} +/// Acknowledgement that a workflow request was accepted. +/// +///
+/// +/// **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 WorkflowAbortResult {} + /// Identifies the target session. /// ///
@@ -28846,6 +31654,26 @@ pub type CardDigestValue = String; ///
pub type CatalogCapabilityId = String; +/// Canonical catalogue resource identity. The runtime rewrites accepted urn:ai and urn:air identifiers to urn:air, lowercases the authority, and preserves the remaining resource components. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+pub type CatalogResourceIdentity = String; + +/// Bounded source-declared resource version, preserved exactly after validation and never inferred. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+pub type CatalogResourceVersion = String; + /// Client-owned, case-sensitive string metadata persisted with a local session. Clients should namespace keys by owner. Keys must be non-empty and at most 256 UTF-8 bytes; keys under `copilot/` and `github/` are reserved. Values may contain at most 16 KiB of UTF-8 data. A bag may contain at most 128 entries and its serialized sidecar may contain at most 64 KiB. The runtime stores but never interprets these values. /// ///
@@ -29704,36 +32532,124 @@ pub enum CardDigestAlgorithm { Unknown, } -/// AI skills are discovery-only and cannot be installed through this surface +/// Explicit Agent Plugin compatibility declared by exact catalog tags. Clients must not infer these values from display text or other metadata. +/// +///
+/// +/// **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 CatalogAiSkillCandidateInstallability { - #[serde(rename = "not-installable-kind")] +pub enum CatalogAgentPluginCompatibilityTag { + /// The plugin contributes at least one GitHub Copilot Canvas. + #[serde(rename = "canvas")] + Canvas, + /// The plugin depends on Canvas for its intended functionality. + #[serde(rename = "canvas-only")] + CanvasOnly, + /// The plugin targets GitHub Copilot. + #[serde(rename = "github-copilot")] + GitHubCopilot, + /// Unknown variant for forward compatibility. #[default] - NotInstallableKind, + #[serde(other)] + Unknown, } -/// Discriminator: this candidate describes an AI skill +/// Discriminator for an Agent Plugin candidate +/// +///
+/// +/// **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 CatalogAiSkillCandidateKind { - #[serde(rename = "ai-skill")] +pub enum CatalogAgentPluginCandidateKind { + /// An Agent Plugin. + #[serde(rename = "plugin")] + Plugin, + /// Unknown variant for forward compatibility. #[default] - AiSkill, + #[serde(other)] + Unknown, } -/// Media type of the underlying AI skill card +/// Canonical Agent Plugin media type +/// +///
+/// +/// **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 CatalogAiSkillCandidateMediaType { - #[serde(rename = "application/ai-skill")] +pub enum CatalogAgentPluginMediaType { + /// A GitHub Copilot Agent Plugin descriptor. + #[serde(rename = "application/vnd.github.copilot-plugin")] + ApplicationVndGitHubCopilotPlugin, + /// Unknown variant for forward compatibility. #[default] - ApplicationAiSkill, + #[serde(other)] + Unknown, } -/// Media type advertised for the referenced AI skill card +/// Typed non-installable state for an AI skill candidate +/// +///
+/// +/// **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 CatalogAiSkillCandidateProvenanceMediaType { - #[serde(rename = "application/ai-skill")] +pub enum CatalogAiSkillInstallability { + /// AI skills are discovery-only on this surface. + #[serde(rename = "not-installable-kind")] + NotInstallableKind, + /// Unknown variant for forward compatibility. #[default] + #[serde(other)] + Unknown, +} + +/// Discriminator for an AI skill candidate +/// +///
+/// +/// **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 CatalogAiSkillCandidateKind { + /// An AI skill. + #[serde(rename = "ai-skill")] + AiSkill, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Canonical AI skill media type +/// +///
+/// +/// **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 CatalogAiSkillMediaType { + /// An AI skill card. + #[serde(rename = "application/ai-skill")] ApplicationAiSkill, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, } /// Discriminator: the card is URL-backed, and carries no embedded data @@ -29822,12 +32738,23 @@ pub enum CatalogMcpServerInstallability { Unknown, } -/// Discriminator: this candidate describes an MCP server +/// Discriminator for an MCP server candidate +/// +///
+/// +/// **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 CatalogMcpServerCandidateKind { + /// An MCP server. #[serde(rename = "mcp-server")] - #[default] McpServer, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, } /// JSON MCP card media type accepted for install planning @@ -29852,7 +32779,7 @@ pub enum McpServerCardMediaType { Unknown, } -/// One inert catalog result, represented as an MCP server or discovery-only AI skill variant so kind, media type, provenance, and installability cannot contradict each other. +/// One inert catalog result, represented as an MCP server, discovery-only AI skill, or opt-in Agent Plugin variant so kind, media type, provenance, and available operations cannot contradict each other. /// ///
/// @@ -29865,6 +32792,7 @@ pub enum McpServerCardMediaType { pub enum CatalogCandidate { McpServer(CatalogMcpServerCandidate), AiSkill(CatalogAiSkillCandidate), + Plugin(CatalogAgentPluginCandidate), } /// What kind of resource a catalog candidate describes @@ -29883,6 +32811,9 @@ pub enum CatalogCandidateKind { /// An AI skill, which is discoverable but not installable through this surface. #[serde(rename = "ai-skill")] AiSkill, + /// An inert Agent Plugin candidate, available only when explicitly requested. + #[serde(rename = "plugin")] + Plugin, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -29908,15 +32839,24 @@ pub enum CatalogCapability { /// Understands `application/ai-skill` candidates as discovery-only and typed non-installable. #[serde(rename = "ai-skill-discovery")] AiSkillDiscovery, + /// Understands opt-in `application/vnd.github.copilot-plugin` candidates and their typed identity, version, source, and compatibility fields. + #[serde(rename = "agent-plugin-discovery")] + AgentPluginDiscovery, /// Understands side-effect-free MCP install-plan requests, results, and plan handles; `planning-unavailable` separately reports that planning is not enabled. #[serde(rename = "mcp-install-planning")] McpInstallPlanning, /// Understands plans that enumerate every eligible transport rather than a single preferred one. #[serde(rename = "multiple-transport-choice")] MultipleTransportChoice, + /// Understands explicit numbered navigation and authority-reported pagination metadata with opaque tokens. Advertised and granted only when requested. + #[serde(rename = "catalog-search-pagination")] + CatalogSearchPagination, /// Understands versioned candidate trust snapshots. Protocol-3 callers must require this capability before the runtime adds the optional snapshot field. #[serde(rename = "trust-snapshot")] TrustSnapshot, + /// Understands exact candidate selection through model-safe opaque references and host-only candidate-handle hand-off. + #[serde(rename = "catalog-selection")] + CatalogSelection, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -29947,7 +32887,7 @@ pub enum CatalogContractViolationReason { /// A result carried neither a URL nor embedded data, when exactly one is required. #[serde(rename = "neither-url-nor-data")] NeitherUrlNorData, - /// Two results claimed the same normalised identity. + /// Two results claimed the same collision key: normalised identity for existing kinds, or normalised identity and declared version for Agent Plugins. #[serde(rename = "duplicate-identity")] DuplicateIdentity, /// A result declared no media type, or one this contract does not model. @@ -29975,6 +32915,9 @@ pub enum CatalogHandleType { /// An install plan handle. #[serde(rename = "plan")] Plan, + /// A model-safe reference to one retained search candidate. + #[serde(rename = "selection")] + Selection, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -29999,7 +32942,7 @@ pub enum CatalogHandleRejectedErrorKind { ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum CatalogHandleRejectionReason { - /// The handle is unparseable, unknown, or was issued for a different operation. + /// The handle is unparseable or unknown. #[serde(rename = "invalid")] Invalid, /// The handle's time to live has elapsed. @@ -30008,16 +32951,22 @@ pub enum CatalogHandleRejectionReason { /// The handle has already been used, and handles are single-use. #[serde(rename = "replayed")] Replayed, - /// The handle was issued by a different runtime instance. + /// The handle was issued by a different runtime instance or session. #[serde(rename = "foreign")] Foreign, + /// The handle was issued for another catalog operation. + #[serde(rename = "wrong-kind")] + WrongKind, + /// The supplied search identifier does not match the retained candidate. + #[serde(rename = "search-mismatch")] + SearchMismatch, /// Unknown variant for forward compatibility. #[default] #[serde(other)] Unknown, } -/// Which request field was rejected before any work was done +/// Which request field was rejected locally or by the catalog authority /// ///
/// @@ -30048,6 +32997,18 @@ pub enum CatalogInvalidRequestField { /// The requested configuration scope is not one this runtime writes. #[serde(rename = "scope")] Scope, + /// The pagination token or target was invalid, or the authority rejected the continuation. Repeat the search without page. + #[serde(rename = "page")] + Page, + /// The locally owned session identifier was missing or malformed. + #[serde(rename = "sessionId")] + SessionId, + /// The opaque selection reference was missing or malformed. + #[serde(rename = "selectionRef")] + SelectionRef, + /// The terminal selection outcome was missing or unsupported. + #[serde(rename = "outcome")] + Outcome, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -30089,6 +33050,9 @@ pub enum CatalogMediaType { /// An AI skill card. Representable and searchable, but typed non-installable. #[serde(rename = "application/ai-skill")] ApplicationAiSkill, + /// An inert Agent Plugin descriptor. + #[serde(rename = "application/vnd.github.copilot-plugin")] + ApplicationVndGitHubCopilotPlugin, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -30282,6 +33246,25 @@ pub enum McpPlanPolicySource { Unknown, } +/// Relationship of the backend-reported count to the complete query result set. +/// +///
+/// +/// **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 CatalogSearchTotalCountRelation { + /// No exact/full-query or lower-bound guarantee is available. + #[serde(rename = "unknown")] + UnknownValue, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Discriminator: the search completed #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum CatalogSearchSucceededKind { @@ -30364,6 +33347,9 @@ pub enum CatalogUnavailableReason { /// Install planning is not wired up on this runtime build. #[serde(rename = "planning-unavailable")] PlanningUnavailable, + /// Exact candidate selection is not available in this session or runtime. + #[serde(rename = "selection-unavailable")] + SelectionUnavailable, /// No catalog authority is configured for this runtime. #[serde(rename = "authority-not-configured")] AuthorityNotConfigured, @@ -30400,6 +33386,131 @@ pub enum CatalogSearchResult { Unavailable(CatalogUnavailableError), } +/// Discriminator: selection was cancelled +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CatalogSelectionCancelledKind { + #[serde(rename = "cancelled")] + #[default] + Cancelled, +} + +/// Terminal outcome declared for a retained catalog selection group +/// +///
+/// +/// **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 CatalogSelectionDecision { + /// Choose the candidate named by selectionRef. + #[serde(rename = "selected")] + Selected, + /// Explicitly decline every candidate in the search. + #[serde(rename = "declined")] + Declined, + /// Cancel the selection interaction without choosing a candidate. + #[serde(rename = "cancelled")] + Cancelled, + /// Declare that the host's live interaction deadline elapsed while the reference remained valid. + #[serde(rename = "timed-out")] + TimedOut, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Discriminator: the candidates were declined +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CatalogSelectionDeclinedKind { + #[serde(rename = "declined")] + #[default] + Declined, +} + +/// Discriminator for this typed selection rejection +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CatalogSelectionForeignKind { + #[serde(rename = "foreign")] + #[default] + Foreign, +} + +/// Discriminator for this typed selection rejection +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CatalogSelectionInvalidKind { + #[serde(rename = "invalid")] + #[default] + Invalid, +} + +/// Discriminator for this typed selection rejection +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CatalogSelectionReplayedKind { + #[serde(rename = "replayed")] + #[default] + Replayed, +} + +/// Discriminator: one candidate was selected +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CatalogSelectionSelectedKind { + #[serde(rename = "selected")] + #[default] + Selected, +} + +/// Discriminator: the host's live interaction timed out +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CatalogSelectionTimedOutKind { + #[serde(rename = "timed-out")] + #[default] + TimedOut, +} + +/// Discriminator for this typed selection rejection +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CatalogSelectionStaleKind { + #[serde(rename = "stale")] + #[default] + Stale, +} + +/// Discriminator for this typed selection rejection +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CatalogSelectionWrongKindKind { + #[serde(rename = "wrong-kind")] + #[default] + WrongKind, +} + +/// Typed outcome of catalog.select. Only the selected host result carries a fresh candidate handle; the model-facing projection removes both that handle and searchId. +/// +///
+/// +/// **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, Serialize, Deserialize)] +#[serde(untagged)] +pub enum CatalogSelectionResult { + Selected(CatalogSelectionSelected), + Declined(CatalogSelectionDeclined), + Cancelled(CatalogSelectionCancelled), + TimedOut(CatalogSelectionTimedOut), + Invalid(CatalogSelectionInvalid), + Stale(CatalogSelectionStale), + Replayed(CatalogSelectionReplayed), + Foreign(CatalogSelectionForeign), + WrongKind(CatalogSelectionWrongKind), + NegotiationRefused(CatalogNegotiationRefusedError), + InvalidRequest(CatalogInvalidRequestError), + Unavailable(CatalogUnavailableError), +} + /// Authority-computed exposure eligibility, kept separate from tier. The current tier-only Agent Finder response maps to `unknown`, never to a locally inferred eligibility. /// ///
@@ -30779,6 +33890,139 @@ pub enum ConnectedRemoteSessionMetadataKind { Unknown, } +/// Availability of the EXPERIMENTAL session connector API. +/// +///
+/// +/// **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 ConnectorAvailability { + /// The resolved Connector feature is enabled and Connector requests are permitted. + #[serde(rename = "enabled")] + Enabled, + /// The resolved Connector feature is off. No Connector service request is made while disabled. + #[serde(rename = "disabled")] + Disabled, + /// The session has no eligible host-owned GitHub account or does not support local Connector projection. + #[serde(rename = "unavailable")] + Unavailable, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Authoritative service connection state for one Connector. +/// +///
+/// +/// **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 ConnectorCatalogStatus { + /// The Connector is available but not connected. + #[serde(rename = "not_connected")] + NotConnected, + /// The Connector service is still completing connection or consent. + #[serde(rename = "pending")] + Pending, + /// The Connector is connected and may contribute MCP servers. + #[serde(rename = "connected")] + Connected, + /// The Connector service reports an unusable connection. + #[serde(rename = "error")] + Error, + /// The service returned a future or unrecognized state. + #[serde(rename = "unknown")] + UnknownValue, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// The service is connected and the session MCP graph was reconciled. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorConnectResultConnectedKind { + #[serde(rename = "connected")] + #[default] + Connected, +} + +/// Live MCP status of one Connector-owned runtime server. +/// +///
+/// +/// **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 ConnectorMcpStatus { + /// The server is connected and its tools are available. + #[serde(rename = "connected")] + Connected, + /// The server connection is still being established. + #[serde(rename = "pending")] + Pending, + /// The server requires refreshed GitHub authorization. + #[serde(rename = "needs_auth")] + NeedsAuth, + /// The server failed to connect or initialize. + #[serde(rename = "failed")] + Failed, + /// The server is intentionally stopped, including when managed policy blocks it. + #[serde(rename = "stopped")] + Stopped, + /// The server is configured but explicitly disabled. + #[serde(rename = "disabled")] + Disabled, + /// The Connector currently has no live server configuration. + #[serde(rename = "not_configured")] + NotConfigured, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Host-owned consent is required before bounded continuation can complete. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorConnectResultConsentRequiredKind { + #[serde(rename = "consent_required")] + #[default] + ConsentRequired, +} + +/// The service is still completing the connection without a consent URL. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ConnectorConnectResultPendingKind { + #[serde(rename = "pending")] + #[default] + Pending, +} + +/// Typed result of initiating or continuing a Connector connection. +/// +///
+/// +/// **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, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ConnectorConnectResult { + Connected(ConnectorConnectResultConnected), + ConsentRequired(ConnectorConnectResultConsentRequired), + Pending(ConnectorConnectResultPending), +} + /// Closed set of public task kinds a connection can negotiate. /// ///
@@ -31738,6 +34982,37 @@ pub enum HistoryRewindOutcome { Unknown, } +/// Live indexed-search state for this session activation, never inferred from persisted 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 IndexedSearchState { + /// Indexed search is not active for this session. + #[serde(rename = "disabled")] + Disabled, + /// Indexed-search startup is in progress. + #[serde(rename = "starting")] + Starting, + /// The indexed-search server started successfully; its index may still be warming. + #[serde(rename = "enabled")] + Enabled, + /// The indexed-search server and its index are ready. + #[serde(rename = "ready")] + Ready, + /// Indexed-search startup or the active server failed. + #[serde(rename = "failed")] + Failed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Constant value. Always "github". #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum InstalledPluginSourceGitHubSource { @@ -36054,6 +39329,189 @@ pub enum UISessionLimitsExhaustedResponseAction { Unknown, } +/// Execution-critical workflow storage operation. +/// +///
+/// +/// **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 WorkflowDurableOperation { + /// Creating the durable run and declared phases. + #[serde(rename = "createRun")] + CreateRun, + /// Persisting the transition to running. + #[serde(rename = "markRunStarted")] + MarkRunStarted, + /// Persisting the terminal run envelope. + #[serde(rename = "finishRun")] + FinishRun, + /// Persisting subagent admission accounting. + #[serde(rename = "reserveAgent")] + ReserveAgent, + /// Rolling back an uncommitted subagent admission. + #[serde(rename = "releaseAgent")] + ReleaseAgent, + /// Persisting an idempotent model-usage charge. + #[serde(rename = "chargeCredit")] + ChargeCredit, + /// Persisting active execution time. + #[serde(rename = "addElapsed")] + AddElapsed, + /// Reading the authoritative AI-credit total. + #[serde(rename = "reconcileCreditTotal")] + ReconcileCreditTotal, + /// Reading a journal entry without treating storage failure as a cache miss. + #[serde(rename = "journalGet")] + JournalGet, + /// Persisting a journal entry before reporting success. + #[serde(rename = "journalPut")] + JournalPut, + /// Renewing the durable owner lease that proves this process still owns the run. + #[serde(rename = "refreshLease")] + RefreshLease, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Current or terminal state of a workflow run. +/// +///
+/// +/// **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 WorkflowRunStatus { + /// The run was minted and is awaiting approval. + #[serde(rename = "pending")] + Pending, + /// The run is executing. + #[serde(rename = "running")] + Running, + /// The run completed successfully. + #[serde(rename = "completed")] + Completed, + /// The run was interrupted while resource budget remained. + #[serde(rename = "halted")] + Halted, + /// The current attempt stopped intentionally and the run may be resumed. + #[serde(rename = "paused")] + Paused, + /// The run was cancelled before completion. + #[serde(rename = "cancelled")] + Cancelled, + /// The workflow body failed or reached a cumulative resource ceiling. + #[serde(rename = "error")] + Error, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Kind of workflow progress line. +/// +///
+/// +/// **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 WorkflowLogLineKind { + /// A narrator log line. + #[serde(rename = "log")] + Log, + /// A named workflow phase marker. + #[serde(rename = "phase")] + Phase, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Action the runtime selected for a durable workflow pause 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, PartialEq, Eq, Serialize, Deserialize)] +pub enum WorkflowPauseCheckpointAction { + /// The checkpoint was committed by a prior paused attempt, so execution may continue. + #[serde(rename = "continue")] + Continue, + /// This attempt claimed the checkpoint and must cooperatively stop. + #[serde(rename = "pause")] + Pause, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Derived lifecycle state of a workflow 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 WorkflowPhaseStatus { + /// The phase has not been entered yet. + #[serde(rename = "pending")] + Pending, + /// The phase is currently entered and accumulating active time. + #[serde(rename = "active")] + Active, + /// The phase was entered and has since been closed. + #[serde(rename = "completed")] + Completed, + /// The phase was never entered because a later phase was entered or the run reached a terminal state. + #[serde(rename = "skipped")] + Skipped, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Cumulative resource ceiling that stopped a workflow run. +/// +///
+/// +/// **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 WorkflowRunFailureKind { + /// The run admitted the approved maximum total number of subagents. + #[serde(rename = "maxTotalSubagents")] + MaxTotalSubagents, + /// The run reached the approved accumulated active-execution time in seconds. + #[serde(rename = "timeoutSeconds")] + TimeoutSeconds, + /// The run's settled subagent model usage exceeded the approved AI-credit ceiling, or no headroom remained for another subagent. + #[serde(rename = "maxAiCredits")] + MaxAiCredits, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Type of change represented by this file diff. /// ///
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index bbde719b6a..eed458d65f 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -43,6 +43,13 @@ impl<'a> ClientRpc<'a> { } } + /// `ahp.*` sub-namespace. + pub fn ahp(&self) -> ClientRpcAhp<'a> { + ClientRpcAhp { + client: self.client, + } + } + /// `catalog.*` sub-namespace. pub fn catalog(&self) -> ClientRpcCatalog<'a> { ClientRpcCatalog { @@ -515,6 +522,167 @@ impl<'a> ClientRpcAgents<'a> { } } +/// `ahp.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcAhp<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcAhp<'a> { + /// Registers an application-owned, transport-neutral AHP agent endpoint. Callback flags refer to executable handlers retained by the SDK client; no listener is opened by the runtime. + /// + /// Wire method: `ahp.registerEndpoint`. + /// + /// # Parameters + /// + /// * `params` - Endpoint policy and the callbacks installed on its owning SDK connection. + /// + ///
+ /// + /// **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(crate) async fn register_endpoint( + &self, + params: AhpRegisterEndpointRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AHP_REGISTERENDPOINT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Opens an independent logical AHP connection on an endpoint owned by this SDK connection. + /// + /// Wire method: `ahp.openConnection`. + /// + ///
+ /// + /// **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(crate) async fn open_connection( + &self, + params: AhpEndpointRef, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AHP_OPENCONNECTION, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Admits one complete opaque AHP message to a logical connection. Success acknowledges admission, not completion; AHP responses arrive through ahp.message. Message size and retained queue limits are enforced by the runtime. + /// + /// Wire method: `ahp.send`. + /// + ///
+ /// + /// **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(crate) async fn send(&self, params: AhpMessage) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AHP_SEND, Some(wire_params)) + .await?; + Ok(()) + } + + /// Idempotently closes one logical AHP connection and releases its participation without closing the original session owner. + /// + /// Wire method: `ahp.closeConnection`. + /// + ///
+ /// + /// **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(crate) async fn close_connection(&self, params: AhpConnectionRef) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AHP_CLOSECONNECTION, Some(wire_params)) + .await?; + Ok(()) + } + + /// Idempotently closes an application-owned AHP endpoint and all of its logical connections. + /// + /// Wire method: `ahp.disposeEndpoint`. + /// + ///
+ /// + /// **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(crate) async fn dispose_endpoint(&self, params: AhpEndpointRef) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AHP_DISPOSEENDPOINT, Some(wire_params)) + .await?; + Ok(()) + } + + /// Re-evaluates this endpoint's session exposure policy and removes access and subscriptions for sessions no longer exposed. + /// + /// Wire method: `ahp.refreshExposure`. + /// + ///
+ /// + /// **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(crate) async fn refresh_exposure(&self, params: AhpEndpointRef) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AHP_REFRESHEXPOSURE, Some(wire_params)) + .await?; + Ok(()) + } + + /// Updates the endpoint's application-supplied agent catalog and customizations without changing session configuration. + /// + /// Wire method: `ahp.setCapabilities`. + /// + ///
+ /// + /// **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(crate) async fn set_capabilities( + &self, + params: AhpSetCapabilitiesRequest, + ) -> Result<(), Error> { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::AHP_SETCAPABILITIES, Some(wire_params)) + .await?; + Ok(()) + } +} + /// `catalog.*` RPCs. #[derive(Clone, Copy)] pub struct ClientRpcCatalog<'a> { @@ -549,6 +717,37 @@ impl<'a> ClientRpcCatalog<'a> { .await?; Ok(serde_json::from_value(_value)?) } + + /// Terminates one retained catalog selection group. A selected outcome returns the native host a fresh single-use candidate handle plus the original searchId for a later explicit mcp.planInstall call; non-selected outcomes release the group without producing a planning input. Candidate state, cards, URLs, credentials and private identifiers remain inside the runtime. The model-facing catalog_select tool projects the result separately and never exposes the candidate handle or searchId. + /// + /// Wire method: `catalog.select`. + /// + /// # Parameters + /// + /// * `params` - Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. + /// + /// # Returns + /// + /// Typed outcome of catalog.select. Only the selected host result carries a fresh candidate handle; the model-facing projection removes both that handle and searchId. + /// + ///
+ /// + /// **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 select( + &self, + params: CatalogSelectionRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::CATALOG_SELECT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } } /// `commands.*` RPCs. @@ -1750,7 +1949,7 @@ impl<'a> ClientRpcSessionFs<'a> { /// /// # Parameters /// - /// * `params` - Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. + /// * `params` - Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. A registered provider is authoritative for path interpretation and filesystem facts used by workspace permission validation. Paths are interpreted lexically; home-relative paths (`~` and `~/...`) and Windows drive-relative paths such as `C:foo` are unsupported. Until provider-side canonicalization is supported, providers must not expose symlinks inside allowed roots that escape those roots. /// /// # Returns /// @@ -3125,6 +3324,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.connectors.*` sub-namespace. + pub fn connectors(&self) -> SessionRpcConnectors<'a> { + SessionRpcConnectors { + session: self.session, + } + } + /// `session.contentExclusion.*` sub-namespace. pub fn content_exclusion(&self) -> SessionRpcContentExclusion<'a> { SessionRpcContentExclusion { @@ -3202,6 +3408,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.managedSettings.*` sub-namespace. + pub fn managed_settings(&self) -> SessionRpcManagedSettings<'a> { + SessionRpcManagedSettings { + session: self.session, + } + } + /// `session.mcp.*` sub-namespace. pub fn mcp(&self) -> SessionRpcMcp<'a> { SessionRpcMcp { @@ -3363,6 +3576,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.workflow.*` sub-namespace. + pub fn workflow(&self) -> SessionRpcWorkflow<'a> { + SessionRpcWorkflow { + session: self.session, + } + } + /// `session.workspaces.*` sub-namespace. pub fn workspaces(&self) -> SessionRpcWorkspaces<'a> { SessionRpcWorkspaces { @@ -4447,24 +4667,20 @@ impl<'a> SessionRpcCompletions<'a> { } } -/// `session.contentExclusion.*` RPCs. +/// `session.connectors.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcContentExclusion<'a> { +pub struct SessionRpcConnectors<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcContentExclusion<'a> { - /// Checks local file system absolute paths within the session working directory against its content-exclusion policy. Results preserve input order. Unsupported paths/filesystems and unavailable policy evaluation return available false, and callers must treat every requested path as excluded. - /// - /// Wire method: `session.contentExclusion.checkPaths`. - /// - /// # Parameters +impl<'a> SessionRpcConnectors<'a> { + /// Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request. /// - /// * `params` - Local file system absolute paths within the session working directory to check against its content-exclusion policy. + /// Wire method: `session.connectors.getCapabilities`. /// /// # Returns /// - /// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. + /// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. /// ///
/// @@ -4473,42 +4689,26 @@ impl<'a> SessionRpcContentExclusion<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn check_paths( - &self, - params: ContentExclusionCheckPathsRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn get_capabilities(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_CONTENTEXCLUSION_CHECKPATHS, + rpc_methods::SESSION_CONNECTORS_GETCAPABILITIES, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.debug.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcDebug<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcDebug<'a> { - /// Collects a session debug log bundle into a local archive or staging directory. Logs are redacted by default; redaction can be configured per caller-provided diagnostic entry. The runtime includes session-owned logs by default and accepts caller-provided diagnostic entries so host applications can add their own files without changing this API shape. - /// - /// Wire method: `session.debug.collectLogs`. + /// Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. /// - /// # Parameters - /// - /// * `params` - Options for collecting a session debug bundle with configurable redaction. + /// Wire method: `session.connectors.getStatus`. /// /// # Returns /// - /// Result of collecting a session debug bundle. + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. /// ///
/// @@ -4517,39 +4717,27 @@ impl<'a> SessionRpcDebug<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn collect_logs( - &self, - params: DebugCollectLogsRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn get_status(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_DEBUG_COLLECTLOGS, Some(wire_params)) + .call(rpc_methods::SESSION_CONNECTORS_GETSTATUS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.eventLog.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcEventLog<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcEventLog<'a> { - /// Reads a batch of session events from a cursor, optionally waiting for new events. Supports tail-first reads via `direction: backward`. + /// Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog. /// - /// Wire method: `session.eventLog.read`. + /// Wire method: `session.connectors.list`. /// /// # Parameters /// - /// * `params` - Cursor, batch size, and optional long-poll/filter parameters for reading session events. + /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. /// /// # Returns /// - /// Batch of session events returned by a read, with cursor and continuation metadata. + /// Validated Connector catalog snapshot cached by the session. /// ///
/// @@ -4558,24 +4746,31 @@ impl<'a> SessionRpcEventLog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn read(&self, params: EventLogReadRequest) -> Result { + pub async fn list( + &self, + params: ConnectorAccountRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_EVENTLOG_READ, Some(wire_params)) + .call(rpc_methods::SESSION_CONNECTORS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns a snapshot of the current tail cursor without consuming events. + /// Refreshes and validates the Connector catalog for the pinned opaque account selection. /// - /// Wire method: `session.eventLog.tail`. + /// Wire method: `session.connectors.refresh`. + /// + /// # Parameters + /// + /// * `params` - Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. /// /// # Returns /// - /// Snapshot of the current tail cursor without returning any events. Use this when a consumer wants to subscribe to live events going forward without first paginating through the entire persisted history (which would happen if `read` were called without a cursor on a long-lived session). + /// Validated Connector catalog snapshot cached by the session. /// ///
/// @@ -4584,27 +4779,31 @@ impl<'a> SessionRpcEventLog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn tail(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn refresh( + &self, + params: ConnectorAccountRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_EVENTLOG_TAIL, Some(wire_params)) + .call(rpc_methods::SESSION_CONNECTORS_REFRESH, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Registers consumer interest in an event type for runtime gating purposes. + /// Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID. /// - /// Wire method: `session.eventLog.registerInterest`. + /// Wire method: `session.connectors.connect`. /// /// # Parameters /// - /// * `params` - Event type to register consumer interest for, used by runtime gating logic. + /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. /// /// # Returns /// - /// Opaque handle representing an event-type interest registration. + /// Typed result of initiating or continuing a Connector connection. /// ///
/// @@ -4613,34 +4812,31 @@ impl<'a> SessionRpcEventLog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn register_interest( + pub async fn connect( &self, - params: RegisterEventInterestParams, - ) -> Result { + params: ConnectorConnectRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_EVENTLOG_REGISTERINTEREST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_CONNECTORS_CONNECT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Releases a consumer's previously-registered interest in an event type. + /// Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. /// - /// Wire method: `session.eventLog.releaseInterest`. + /// Wire method: `session.connectors.reconnect`. /// /// # Parameters /// - /// * `params` - Opaque handle previously returned by `registerInterest` to release. + /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Typed result of initiating or continuing a Connector connection. /// ///
/// @@ -4649,38 +4845,31 @@ impl<'a> SessionRpcEventLog<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn release_interest( + pub async fn reconnect( &self, - params: ReleaseEventInterestParams, - ) -> Result { + params: ConnectorConnectRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_EVENTLOG_RELEASEINTEREST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_CONNECTORS_RECONNECT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `session.extensions.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcExtensions<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcExtensions<'a> { - /// Lists extensions discovered for the session and their current status. + /// Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. /// - /// Wire method: `session.extensions.list`. + /// Wire method: `session.connectors.continueConnection`. + /// + /// # Parameters + /// + /// * `params` - Explicitly bounded continuation of a pending Connector connection. /// /// # Returns /// - /// Extensions discovered for the session, with their current status. + /// Typed result of initiating or continuing a Connector connection. /// ///
/// @@ -4689,23 +4878,34 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn continue_connection( + &self, + params: ConnectorContinueRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_EXTENSIONS_LIST, Some(wire_params)) + .call( + rpc_methods::SESSION_CONNECTORS_CONTINUECONNECTION, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Enables an extension for the session. + /// Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. /// - /// Wire method: `session.extensions.enable`. + /// Wire method: `session.connectors.disconnect`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifier to enable for the session. + /// * `params` - Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + /// + /// # Returns + /// + /// Authoritative result after disconnect and MCP reconciliation. /// ///
/// @@ -4714,24 +4914,34 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn enable(&self, params: ExtensionsEnableRequest) -> Result<(), Error> { + pub async fn disconnect( + &self, + params: ConnectorConnectRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_EXTENSIONS_ENABLE, Some(wire_params)) + .call( + rpc_methods::SESSION_CONNECTORS_DISCONNECT, + Some(wire_params), + ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Disables an extension for the session. + /// Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. /// - /// Wire method: `session.extensions.disable`. + /// Wire method: `session.connectors.reconcile`. /// /// # Parameters /// - /// * `params` - Source-qualified extension identifier to disable for the session. + /// * `params` - Requests authoritative Connector-to-MCP reconciliation for the pinned account. + /// + /// # Returns + /// + /// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. /// ///
/// @@ -4740,45 +4950,39 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn disable(&self, params: ExtensionsDisableRequest) -> Result<(), Error> { + pub async fn reconcile( + &self, + params: ConnectorReconcileRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_EXTENSIONS_DISABLE, Some(wire_params)) + .call(rpc_methods::SESSION_CONNECTORS_RECONCILE, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } +} - /// Reloads extension definitions and processes for the session. - /// - /// Wire method: `session.extensions.reload`. - /// - ///
- /// - /// **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 reload(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); - let _value = self - .session - .client() - .call(rpc_methods::SESSION_EXTENSIONS_RELOAD, Some(wire_params)) - .await?; - Ok(()) - } +/// `session.contentExclusion.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcContentExclusion<'a> { + pub(crate) session: &'a Session, +} - /// Push attachments into the next user-message turn from an extension. The host should surface them as composer pills and forward them via the next session.send call. Callable only by extension-owned connections. +impl<'a> SessionRpcContentExclusion<'a> { + /// Checks local file system absolute paths within the session working directory against its content-exclusion policy. Results preserve input order. Unsupported paths/filesystems and unavailable policy evaluation return available false, and callers must treat every requested path as excluded. /// - /// Wire method: `session.extensions.sendAttachmentsToMessage`. + /// Wire method: `session.contentExclusion.checkPaths`. /// /// # Parameters /// - /// * `params` - Parameters for session.extensions.sendAttachmentsToMessage. + /// * `params` - Local file system absolute paths within the session working directory to check against its content-exclusion policy. + /// + /// # Returns + /// + /// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. /// ///
/// @@ -4787,49 +4991,42 @@ impl<'a> SessionRpcExtensions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn send_attachments_to_message( + pub async fn check_paths( &self, - params: SendAttachmentsToMessageParams, - ) -> Result<(), Error> { + params: ContentExclusionCheckPathsRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_EXTENSIONS_SENDATTACHMENTSTOMESSAGE, + rpc_methods::SESSION_CONTENTEXCLUSION_CHECKPATHS, Some(wire_params), ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } } -/// `session.factory.*` RPCs. +/// `session.debug.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcFactory<'a> { +pub struct SessionRpcDebug<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcFactory<'a> { - /// `session.factory.journal.*` sub-namespace. - pub fn journal(&self) -> SessionRpcFactoryJournal<'a> { - SessionRpcFactoryJournal { - session: self.session, - } - } - - /// Runs a registered factory by name at the top level. +impl<'a> SessionRpcDebug<'a> { + /// Collects a session debug log bundle into a local archive or staging directory. Logs are redacted by default; redaction can be configured per caller-provided diagnostic entry. The runtime includes session-owned logs by default and accepts caller-provided diagnostic entries so host applications can add their own files without changing this API shape. /// - /// Wire method: `session.factory.run`. + /// Wire method: `session.debug.collectLogs`. /// /// # Parameters /// - /// * `params` - Parameters for invoking a registered factory. + /// * `params` - Options for collecting a session debug bundle with configurable redaction. /// /// # Returns /// - /// Complete current or terminal factory run envelope. + /// Result of collecting a session debug bundle. /// ///
/// @@ -4838,28 +5035,39 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn run(&self, params: FactoryRunRequest) -> Result { + pub async fn collect_logs( + &self, + params: DebugCollectLogsRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_RUN, Some(wire_params)) + .call(rpc_methods::SESSION_DEBUG_COLLECTLOGS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Resumes a factory run using its persisted name, arguments, journal, and accounting. +/// `session.eventLog.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcEventLog<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcEventLog<'a> { + /// Reads a batch of session events from a cursor, optionally waiting for new events. Supports tail-first reads via `direction: backward`. /// - /// Wire method: `session.factory.resume`. + /// Wire method: `session.eventLog.read`. /// /// # Parameters /// - /// * `params` - Parameters for resuming a factory run from its persisted identity. + /// * `params` - Cursor, batch size, and optional long-poll/filter parameters for reading session events. /// /// # Returns /// - /// Resolved persisted factory identity and resumed run envelope. + /// Batch of session events returned by a read, with cursor and continuation metadata. /// ///
/// @@ -4868,28 +5076,24 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn resume(&self, params: FactoryResumeRequest) -> Result { + pub async fn read(&self, params: EventLogReadRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_RESUME, Some(wire_params)) + .call(rpc_methods::SESSION_EVENTLOG_READ, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Internal tool-originated factory invocation. - /// - /// Wire method: `session.factory.runFromTool`. - /// - /// # Parameters + /// Returns a snapshot of the current tail cursor without consuming events. /// - /// * `params` - Internal parameters for invoking a registered factory from a tool. + /// Wire method: `session.eventLog.tail`. /// /// # Returns /// - /// Complete current or terminal factory run envelope. + /// Snapshot of the current tail cursor without returning any events. Use this when a consumer wants to subscribe to live events going forward without first paginating through the entire persisted history (which would happen if `read` were called without a cursor on a long-lived session). /// ///
/// @@ -4898,31 +5102,27 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn run_from_tool( - &self, - params: FactoryToolRunRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn tail(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_RUNFROMTOOL, Some(wire_params)) + .call(rpc_methods::SESSION_EVENTLOG_TAIL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Internal tool-originated factory resume. + /// Registers consumer interest in an event type for runtime gating purposes. /// - /// Wire method: `session.factory.resumeFromTool`. + /// Wire method: `session.eventLog.registerInterest`. /// /// # Parameters /// - /// * `params` - Internal parameters for resuming a factory run from a tool. + /// * `params` - Event type to register consumer interest for, used by runtime gating logic. /// /// # Returns /// - /// Resolved persisted factory identity and resumed run envelope. + /// Opaque handle representing an event-type interest registration. /// ///
/// @@ -4931,34 +5131,34 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn resume_from_tool( + pub async fn register_interest( &self, - params: FactoryToolResumeRequest, - ) -> Result { + params: RegisterEventInterestParams, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_FACTORY_RESUMEFROMTOOL, + rpc_methods::SESSION_EVENTLOG_REGISTERINTEREST, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Gets the current or settled envelope for a factory run. + /// Releases a consumer's previously-registered interest in an event type. /// - /// Wire method: `session.factory.getRun`. + /// Wire method: `session.eventLog.releaseInterest`. /// /// # Parameters /// - /// * `params` - Parameters for retrieving a factory run. + /// * `params` - Opaque handle previously returned by `registerInterest` to release. /// /// # Returns /// - /// Complete current or terminal factory run envelope. + /// Indicates whether the operation succeeded. /// ///
/// @@ -4967,28 +5167,38 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_run(&self, params: FactoryGetRunRequest) -> Result { + pub async fn release_interest( + &self, + params: ReleaseEventInterestParams, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_GETRUN, Some(wire_params)) + .call( + rpc_methods::SESSION_EVENTLOG_RELEASEINTEREST, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Lists durable factory runs for this session in creation order. - /// - /// Wire method: `session.factory.listRuns`. - /// - /// # Parameters +/// `session.extensions.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcExtensions<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcExtensions<'a> { + /// Lists extensions discovered for the session and their current status. /// - /// * `params` - Parameters for paging factory runs. + /// Wire method: `session.extensions.list`. /// /// # Returns /// - /// A page of factory runs in durable creation order. + /// Extensions discovered for the session, with their current status. /// ///
/// @@ -4997,31 +5207,23 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list_runs( - &self, - params: FactoryListRunsRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_LISTRUNS, Some(wire_params)) + .call(rpc_methods::SESSION_EXTENSIONS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Gets durable and live observability detail for one factory run. + /// Enables an extension for the session. /// - /// Wire method: `session.factory.getRunDetail`. + /// Wire method: `session.extensions.enable`. /// /// # Parameters /// - /// * `params` - Parameters for retrieving a factory run. - /// - /// # Returns - /// - /// Full factory run observability detail. + /// * `params` - Source-qualified extension identifier to enable for the session. /// ///
/// @@ -5030,31 +5232,24 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_run_detail( - &self, - params: FactoryGetRunRequest, - ) -> Result { + pub async fn enable(&self, params: ExtensionsEnableRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_GETRUNDETAIL, Some(wire_params)) + .call(rpc_methods::SESSION_EXTENSIONS_ENABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Pages durable progress for one factory run. + /// Disables an extension for the session. /// - /// Wire method: `session.factory.getRunProgress`. + /// Wire method: `session.extensions.disable`. /// /// # Parameters /// - /// * `params` - Parameters for paging factory progress. - /// - /// # Returns - /// - /// A bidirectional page of factory progress. + /// * `params` - Source-qualified extension identifier to disable for the session. /// ///
/// @@ -5063,34 +5258,20 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_run_progress( - &self, - params: FactoryGetRunProgressRequest, - ) -> Result { + pub async fn disable(&self, params: ExtensionsDisableRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_FACTORY_GETRUNPROGRESS, - Some(wire_params), - ) + .call(rpc_methods::SESSION_EXTENSIONS_DISABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Requests cancellation of a factory run and returns its run envelope. - /// - /// Wire method: `session.factory.cancel`. - /// - /// # Parameters - /// - /// * `params` - Parameters for cancelling a factory run. - /// - /// # Returns + /// Reloads extension definitions and processes for the session. /// - /// Complete current or terminal factory run envelope. + /// Wire method: `session.extensions.reload`. /// ///
/// @@ -5099,28 +5280,23 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn cancel(&self, params: FactoryCancelRequest) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn reload(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_CANCEL, Some(wire_params)) + .call(rpc_methods::SESSION_EXTENSIONS_RELOAD, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Pauses a running factory and returns its settled run envelope. + /// Push attachments into the next user-message turn from an extension. The host should surface them as composer pills and forward them via the next session.send call. Callable only by extension-owned connections. /// - /// Wire method: `session.factory.pause`. + /// Wire method: `session.extensions.sendAttachmentsToMessage`. /// /// # Parameters /// - /// * `params` - Parameters for pausing a running factory. - /// - /// # Returns - /// - /// Complete current or terminal factory run envelope. + /// * `params` - Parameters for session.extensions.sendAttachmentsToMessage. /// ///
/// @@ -5129,24 +5305,49 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn pause(&self, params: FactoryPauseRequest) -> Result { + pub async fn send_attachments_to_message( + &self, + params: SendAttachmentsToMessageParams, + ) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_PAUSE, Some(wire_params)) + .call( + rpc_methods::SESSION_EXTENSIONS_SENDATTACHMENTSTOMESSAGE, + Some(wire_params), + ) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } +} - /// Atomically pauses an owned factory attempt at a durable checkpoint. +/// `session.factory.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcFactory<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcFactory<'a> { + /// `session.factory.journal.*` sub-namespace. + pub fn journal(&self) -> SessionRpcFactoryJournal<'a> { + SessionRpcFactoryJournal { + session: self.session, + } + } + + /// Runs a registered factory by name at the top level. /// - /// Wire method: `session.factory.pauseAtCheckpoint`. + /// Wire method: `session.factory.run`. /// /// # Parameters /// - /// * `params` - Parameters for an owned durable pause checkpoint. + /// * `params` - Parameters for invoking a registered factory. + /// + /// # Returns + /// + /// Complete current or terminal factory run envelope. /// ///
/// @@ -5155,34 +5356,28 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn pause_at_checkpoint( - &self, - params: FactoryPauseCheckpointRequest, - ) -> Result { + pub async fn run(&self, params: FactoryRunRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_FACTORY_PAUSEATCHECKPOINT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_FACTORY_RUN, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Records a batch of ordered factory progress lines. + /// Resumes a factory run using its persisted name, arguments, journal, and accounting. /// - /// Wire method: `session.factory.log`. + /// Wire method: `session.factory.resume`. /// /// # Parameters /// - /// * `params` - Parameters for recording factory progress. + /// * `params` - Parameters for resuming a factory run from its persisted identity. /// /// # Returns /// - /// Acknowledgement that a factory request was accepted. + /// Resolved persisted factory identity and resumed run envelope. /// ///
/// @@ -5191,28 +5386,28 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn log(&self, params: FactoryLogRequest) -> Result { + pub async fn resume(&self, params: FactoryResumeRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_LOG, Some(wire_params)) + .call(rpc_methods::SESSION_FACTORY_RESUME, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Runs one factory-scoped subagent and returns its result. + /// Internal tool-originated factory invocation. /// - /// Wire method: `session.factory.agent`. + /// Wire method: `session.factory.runFromTool`. /// /// # Parameters /// - /// * `params` - Parameters for one factory-scoped subagent call. + /// * `params` - Internal parameters for invoking a registered factory from a tool. /// /// # Returns /// - /// Result of one factory-scoped subagent call. + /// Complete current or terminal factory run envelope. /// ///
/// @@ -5221,36 +5416,31 @@ impl<'a> SessionRpcFactory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn agent(&self, params: FactoryAgentRequest) -> Result { + pub(crate) async fn run_from_tool( + &self, + params: FactoryToolRunRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_AGENT, Some(wire_params)) + .call(rpc_methods::SESSION_FACTORY_RUNFROMTOOL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.factory.journal.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcFactoryJournal<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcFactoryJournal<'a> { - /// Reads a memoized factory journal entry. + /// Internal tool-originated factory resume. /// - /// Wire method: `session.factory.journal.get`. + /// Wire method: `session.factory.resumeFromTool`. /// /// # Parameters /// - /// * `params` - Parameters for reading a factory journal entry. + /// * `params` - Internal parameters for resuming a factory run from a tool. /// /// # Returns /// - /// Result of reading a factory journal entry. + /// Resolved persisted factory identity and resumed run envelope. /// ///
/// @@ -5259,31 +5449,34 @@ impl<'a> SessionRpcFactoryJournal<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get( + pub(crate) async fn resume_from_tool( &self, - params: FactoryJournalGetRequest, - ) -> Result { + params: FactoryToolResumeRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_JOURNAL_GET, Some(wire_params)) + .call( + rpc_methods::SESSION_FACTORY_RESUMEFROMTOOL, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Stores a memoized factory journal entry. + /// Gets the current or settled envelope for a factory run. /// - /// Wire method: `session.factory.journal.put`. + /// Wire method: `session.factory.getRun`. /// /// # Parameters /// - /// * `params` - Parameters for storing a factory journal entry. + /// * `params` - Parameters for retrieving a factory run. /// /// # Returns /// - /// Acknowledgement that a factory request was accepted. + /// Complete current or terminal factory run envelope. /// ///
/// @@ -5292,36 +5485,28 @@ impl<'a> SessionRpcFactoryJournal<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn put(&self, params: FactoryJournalPutRequest) -> Result { + pub async fn get_run(&self, params: FactoryGetRunRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FACTORY_JOURNAL_PUT, Some(wire_params)) + .call(rpc_methods::SESSION_FACTORY_GETRUN, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.fleet.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcFleet<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcFleet<'a> { - /// Starts fleet mode by submitting the fleet orchestration prompt to the session. + /// Lists durable factory runs for this session in creation order. /// - /// Wire method: `session.fleet.start`. + /// Wire method: `session.factory.listRuns`. /// /// # Parameters /// - /// * `params` - Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. + /// * `params` - Parameters for paging factory runs. /// /// # Returns /// - /// Indicates whether fleet mode was successfully activated. + /// A page of factory runs in durable creation order. /// ///
/// @@ -5330,32 +5515,31 @@ impl<'a> SessionRpcFleet<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn start(&self, params: FleetStartRequest) -> Result { + pub async fn list_runs( + &self, + params: FactoryListRunsRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_FLEET_START, Some(wire_params)) + .call(rpc_methods::SESSION_FACTORY_LISTRUNS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.gitHubAuth.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcGitHubAuth<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcGitHubAuth<'a> { - /// Gets authentication status and account metadata for the session. + /// Gets durable and live observability detail for one factory run. /// - /// Wire method: `session.gitHubAuth.getStatus`. + /// Wire method: `session.factory.getRunDetail`. + /// + /// # Parameters + /// + /// * `params` - Parameters for retrieving a factory run. /// /// # Returns /// - /// Authentication status and account metadata for the session. + /// Full factory run observability detail. /// ///
/// @@ -5364,27 +5548,31 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_status(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn get_run_detail( + &self, + params: FactoryGetRunRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_GITHUBAUTH_GETSTATUS, Some(wire_params)) + .call(rpc_methods::SESSION_FACTORY_GETRUNDETAIL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Updates the session's auth credentials used for outbound model and API requests. + /// Pages durable progress for one factory run. /// - /// Wire method: `session.gitHubAuth.setCredentials`. + /// Wire method: `session.factory.getRunProgress`. /// /// # Parameters /// - /// * `params` - New auth credentials to install on the session. Omit to leave credentials unchanged. + /// * `params` - Parameters for paging factory progress. /// /// # Returns /// - /// Indicates whether the credential update succeeded. + /// A bidirectional page of factory progress. /// ///
/// @@ -5393,30 +5581,34 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_credentials( + pub async fn get_run_progress( &self, - params: SessionSetCredentialsParams, - ) -> Result { + params: FactoryGetRunProgressRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_GITHUBAUTH_SETCREDENTIALS, + rpc_methods::SESSION_FACTORY_GETRUNPROGRESS, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Gets the current authentication information for internal session hosts. + /// Requests cancellation of a factory run and returns its run envelope. /// - /// Wire method: `session.gitHubAuth.getCurrentAuthInfo`. + /// Wire method: `session.factory.cancel`. + /// + /// # Parameters + /// + /// * `params` - Parameters for cancelling a factory run. /// /// # Returns /// - /// Current authentication information, or null when no authentication is active. + /// Complete current or terminal factory run envelope. /// ///
/// @@ -5425,26 +5617,28 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn get_current_auth_info(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn cancel(&self, params: FactoryCancelRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_GITHUBAUTH_GETCURRENTAUTHINFO, - Some(wire_params), - ) + .call(rpc_methods::SESSION_FACTORY_CANCEL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Gets all authentication accounts available to the internal session host. + /// Pauses a running factory and returns its settled run envelope. /// - /// Wire method: `session.gitHubAuth.getAllAuthAvailable`. + /// Wire method: `session.factory.pause`. + /// + /// # Parameters + /// + /// * `params` - Parameters for pausing a running factory. /// /// # Returns /// - /// Authentication accounts available to the internal session host. + /// Complete current or terminal factory run envelope. /// ///
/// @@ -5453,28 +5647,24 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn get_all_auth_available( - &self, - ) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); - let _value = self - .session + pub async fn pause(&self, params: FactoryPauseRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session .client() - .call( - rpc_methods::SESSION_GITHUBAUTH_GETALLAUTHAVAILABLE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_FACTORY_PAUSE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Refreshes Copilot account metadata for the current authentication. + /// Atomically pauses an owned factory attempt at a durable checkpoint. /// - /// Wire method: `session.gitHubAuth.refreshCopilotUser`. + /// Wire method: `session.factory.pauseAtCheckpoint`. /// - /// # Returns + /// # Parameters /// - /// Current authentication information, or null when no authentication is active. + /// * `params` - Parameters for an owned durable pause checkpoint. /// ///
/// @@ -5483,30 +5673,34 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn refresh_copilot_user(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub(crate) async fn pause_at_checkpoint( + &self, + params: FactoryPauseCheckpointRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_GITHUBAUTH_REFRESHCOPILOTUSER, + rpc_methods::SESSION_FACTORY_PAUSEATCHECKPOINT, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Logs in a GitHub user through the internal session host. + /// Records a batch of ordered factory progress lines. /// - /// Wire method: `session.gitHubAuth.login`. + /// Wire method: `session.factory.log`. /// /// # Parameters /// - /// * `params` - Internal GitHub login parameters. + /// * `params` - Parameters for recording factory progress. /// /// # Returns /// - /// Authentication credentials accepted only at native protocol ingress. Runtime outputs use credential-free `AuthIdentity` metadata. + /// Acknowledgement that a factory request was accepted. /// ///
/// @@ -5515,24 +5709,28 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn login(&self, params: SessionAuthLoginRequest) -> Result { + pub async fn log(&self, params: FactoryLogRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_GITHUBAUTH_LOGIN, Some(wire_params)) + .call(rpc_methods::SESSION_FACTORY_LOG, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Switches the session to another available authentication. + /// Runs one factory-scoped subagent and returns its result. /// - /// Wire method: `session.gitHubAuth.switchToAuth`. + /// Wire method: `session.factory.agent`. /// /// # Parameters /// - /// * `params` - Parameters for switching the session's active authentication. + /// * `params` - Parameters for one factory-scoped subagent call. + /// + /// # Returns + /// + /// Result of one factory-scoped subagent call. /// ///
/// @@ -5541,30 +5739,36 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn switch_to_auth( - &self, - params: SessionAuthSwitchRequest, - ) -> Result<(), Error> { + pub async fn agent(&self, params: FactoryAgentRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_GITHUBAUTH_SWITCHTOAUTH, - Some(wire_params), - ) + .call(rpc_methods::SESSION_FACTORY_AGENT, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } +} - /// Logs out the session's current GitHub authentication. +/// `session.factory.journal.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcFactoryJournal<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcFactoryJournal<'a> { + /// Reads a memoized factory journal entry. /// - /// Wire method: `session.gitHubAuth.logout`. + /// Wire method: `session.factory.journal.get`. + /// + /// # Parameters + /// + /// * `params` - Parameters for reading a factory journal entry. /// /// # Returns /// - /// Whether the current authentication was logged out. + /// Result of reading a factory journal entry. /// ///
/// @@ -5573,27 +5777,31 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn logout(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn get( + &self, + params: FactoryJournalGetRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_GITHUBAUTH_LOGOUT, Some(wire_params)) + .call(rpc_methods::SESSION_FACTORY_JOURNAL_GET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Logs out a specific GitHub authentication. + /// Stores a memoized factory journal entry. /// - /// Wire method: `session.gitHubAuth.logoutUser`. + /// Wire method: `session.factory.journal.put`. /// /// # Parameters /// - /// * `params` - Parameters identifying a GitHub authentication to log out. + /// * `params` - Parameters for storing a factory journal entry. /// /// # Returns /// - /// Whether the requested authentication was logged out. + /// Acknowledgement that a factory request was accepted. /// ///
/// @@ -5602,30 +5810,36 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn logout_user( - &self, - params: SessionAuthLogoutUserRequest, - ) -> Result { + pub async fn put(&self, params: FactoryJournalPutRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_GITHUBAUTH_LOGOUTUSER, - Some(wire_params), - ) + .call(rpc_methods::SESSION_FACTORY_JOURNAL_PUT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Gets validation errors from the most recent authentication attempt. +/// `session.fleet.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcFleet<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcFleet<'a> { + /// Starts fleet mode by submitting the fleet orchestration prompt to the session. /// - /// Wire method: `session.gitHubAuth.lastAuthErrors`. + /// Wire method: `session.fleet.start`. + /// + /// # Parameters + /// + /// * `params` - Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. /// /// # Returns /// - /// Validation errors from the most recent authentication attempt. + /// Indicates whether fleet mode was successfully activated. /// ///
/// @@ -5634,34 +5848,32 @@ impl<'a> SessionRpcGitHubAuth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn last_auth_errors(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn start(&self, params: FleetStartRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_GITHUBAUTH_LASTAUTHERRORS, - Some(wire_params), - ) + .call(rpc_methods::SESSION_FLEET_START, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `session.history.*` RPCs. +/// `session.gitHubAuth.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcHistory<'a> { +pub struct SessionRpcGitHubAuth<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcHistory<'a> { - /// Compacts the session history to reduce context usage. +impl<'a> SessionRpcGitHubAuth<'a> { + /// Gets authentication status and account metadata for the session. /// - /// Wire method: `session.history.compact`. + /// Wire method: `session.gitHubAuth.getStatus`. /// /// # Returns /// - /// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. + /// Authentication status and account metadata for the session. /// ///
/// @@ -5670,27 +5882,27 @@ impl<'a> SessionRpcHistory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn compact(&self) -> Result { + pub async fn get_status(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_HISTORY_COMPACT, Some(wire_params)) + .call(rpc_methods::SESSION_GITHUBAUTH_GETSTATUS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Compacts the session history to reduce context usage. + /// Updates the session's auth credentials used for outbound model and API requests. /// - /// Wire method: `session.history.compact`. + /// Wire method: `session.gitHubAuth.setCredentials`. /// /// # Parameters /// - /// * `params` - Optional compaction parameters. + /// * `params` - New auth credentials to install on the session. Omit to leave credentials unchanged. /// /// # Returns /// - /// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. + /// Indicates whether the credential update succeeded. /// ///
/// @@ -5699,31 +5911,30 @@ impl<'a> SessionRpcHistory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn compact_with_params( + pub async fn set_credentials( &self, - params: HistoryCompactRequest, - ) -> Result { + params: SessionSetCredentialsParams, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_HISTORY_COMPACT, Some(wire_params)) + .call( + rpc_methods::SESSION_GITHUBAUTH_SETCREDENTIALS, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Truncates persisted session history to a specific event. - /// - /// Wire method: `session.history.truncate`. - /// - /// # Parameters + /// Gets the current authentication information for internal session hosts. /// - /// * `params` - Identifier of the event to truncate to; this event and all later events are removed. + /// Wire method: `session.gitHubAuth.getCurrentAuthInfo`. /// /// # Returns /// - /// Number of events that were removed by the truncation. + /// Current authentication information, or null when no authentication is active. /// ///
/// @@ -5732,27 +5943,26 @@ impl<'a> SessionRpcHistory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn truncate( - &self, - params: HistoryTruncateRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub(crate) async fn get_current_auth_info(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_HISTORY_TRUNCATE, Some(wire_params)) + .call( + rpc_methods::SESSION_GITHUBAUTH_GETCURRENTAUTHINFO, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Lists the user turns that the session can rewind to. Never rejects for a busy session: rewind reads need the session's file-change captures to be settled, so a session that still holds active work answers with `unavailableReason: "session-busy"` and no points, which the caller can retry. + /// Gets all authentication accounts available to the internal session host. /// - /// Wire method: `session.history.listRewindPoints`. + /// Wire method: `session.gitHubAuth.getAllAuthAvailable`. /// /// # Returns /// - /// Rewind points and file-change-tracking availability for the session. + /// Authentication accounts available to the internal session host. /// ///
/// @@ -5761,30 +5971,28 @@ impl<'a> SessionRpcHistory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list_rewind_points(&self) -> Result { + pub(crate) async fn get_all_auth_available( + &self, + ) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_HISTORY_LISTREWINDPOINTS, + rpc_methods::SESSION_GITHUBAUTH_GETALLAUTHAVAILABLE, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Previews the files that a conversation-and-files rewind would restore. - /// - /// Wire method: `session.history.previewRewind`. - /// - /// # Parameters + /// Refreshes Copilot account metadata for the current authentication. /// - /// * `params` - Event boundary to preview for conversation-and-files rewind. + /// Wire method: `session.gitHubAuth.refreshCopilotUser`. /// /// # Returns /// - /// Files and aggregate changes for a prospective rewind. + /// Current authentication information, or null when no authentication is active. /// ///
/// @@ -5793,34 +6001,30 @@ impl<'a> SessionRpcHistory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn preview_rewind( - &self, - params: HistoryPreviewRewindRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub(crate) async fn refresh_copilot_user(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_HISTORY_PREVIEWREWIND, + rpc_methods::SESSION_GITHUBAUTH_REFRESHCOPILOTUSER, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Rewinds the session conversation, optionally restoring files changed by the discarded turns. Not crash-atomic: file restore and conversation truncation are separate stores, applied in that order, so a process crash between them can leave the workspace rewound while the conversation still contains the discarded turns. There is no recovery journal; re-running the same rewind is the recovery path for a crash before truncation lands, since file restore is idempotent (already-restored files are reported as skipped) and truncation is re-derived from the still-retained boundary event. After truncation lands that boundary no longer exists, so the same request is rejected; the only stage that can still be outstanding is snapshot pruning, whose failure leaves orphan snapshots the capture store tolerates. The reverse inconsistency cannot occur, because truncation is never applied before file restore succeeds. + /// Logs in a GitHub user through the internal session host. /// - /// Wire method: `session.history.rewind`. + /// Wire method: `session.gitHubAuth.login`. /// /// # Parameters /// - /// * `params` - Boundary and mode for rewinding session history. + /// * `params` - Internal GitHub login parameters. /// /// # Returns /// - /// Structured outcome of a rewind request. + /// Authentication credentials accepted only at native protocol ingress. Runtime outputs use credential-free `AuthIdentity` metadata. /// ///
/// @@ -5829,24 +6033,24 @@ impl<'a> SessionRpcHistory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn rewind(&self, params: HistoryRewindRequest) -> Result { + pub(crate) async fn login(&self, params: SessionAuthLoginRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_HISTORY_REWIND, Some(wire_params)) + .call(rpc_methods::SESSION_GITHUBAUTH_LOGIN, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Cancels any in-progress background compaction on a local session. + /// Switches the session to another available authentication. /// - /// Wire method: `session.history.cancelBackgroundCompaction`. + /// Wire method: `session.gitHubAuth.switchToAuth`. /// - /// # Returns + /// # Parameters /// - /// Indicates whether an in-progress background compaction was cancelled. + /// * `params` - Parameters for switching the session's active authentication. /// ///
/// @@ -5855,58 +6059,30 @@ impl<'a> SessionRpcHistory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn cancel_background_compaction( + pub(crate) async fn switch_to_auth( &self, - ) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + params: SessionAuthSwitchRequest, + ) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_HISTORY_CANCELBACKGROUNDCOMPACTION, + rpc_methods::SESSION_GITHUBAUTH_SWITCHTOAUTH, Some(wire_params), ) .await?; - Ok(serde_json::from_value(_value)?) - } - - /// Aborts any in-progress manual compaction on a local session. - /// - /// Wire method: `session.history.abortManualCompaction`. - /// - /// # Returns - /// - /// Indicates whether an in-progress manual compaction was aborted. - /// - ///
- /// - /// **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 abort_manual_compaction( - &self, - ) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); - let _value = self - .session - .client() - .call( - rpc_methods::SESSION_HISTORY_ABORTMANUALCOMPACTION, - Some(wire_params), - ) - .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Produces a markdown summary of the session's conversation context for hand-off scenarios. + /// Logs out the session's current GitHub authentication. /// - /// Wire method: `session.history.summarizeForHandoff`. + /// Wire method: `session.gitHubAuth.logout`. /// /// # Returns /// - /// Markdown summary of the conversation context (empty when not available). + /// Whether the current authentication was logged out. /// ///
/// @@ -5915,30 +6091,27 @@ impl<'a> SessionRpcHistory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn summarize_for_handoff(&self) -> Result { + pub(crate) async fn logout(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_HISTORY_SUMMARIZEFORHANDOFF, - Some(wire_params), - ) + .call(rpc_methods::SESSION_GITHUBAUTH_LOGOUT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Clears the session's conversation history, keeping only system and developer messages, and seeds the fresh context window with a first user message. Must be called from inside a tool handler: the clear has to drop the results of the tool calls its wipe orphans, and it rejects when no tool call is in flight. + /// Logs out a specific GitHub authentication. /// - /// Wire method: `session.history.clearContext`. + /// Wire method: `session.gitHubAuth.logoutUser`. /// /// # Parameters /// - /// * `params` - Parameters for clearing the conversation and seeding the window that replaces it. + /// * `params` - Parameters identifying a GitHub authentication to log out. /// /// # Returns /// - /// What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. + /// Whether the requested authentication was logged out. /// ///
/// @@ -5947,35 +6120,30 @@ impl<'a> SessionRpcHistory<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn clear_context( + pub(crate) async fn logout_user( &self, - params: HistoryClearContextRequest, - ) -> Result { + params: SessionAuthLogoutUserRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_HISTORY_CLEARCONTEXT, Some(wire_params)) + .call( + rpc_methods::SESSION_GITHUBAUTH_LOGOUTUSER, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.instructions.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcInstructions<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcInstructions<'a> { - /// Gets instruction sources loaded for the session. + /// Gets validation errors from the most recent authentication attempt. /// - /// Wire method: `session.instructions.getSources`. + /// Wire method: `session.gitHubAuth.lastAuthErrors`. /// /// # Returns /// - /// Instruction sources loaded for the session, in merge order. + /// Validation errors from the most recent authentication attempt. /// ///
/// @@ -5984,13 +6152,13 @@ impl<'a> SessionRpcInstructions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_sources(&self) -> Result { + pub(crate) async fn last_auth_errors(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_INSTRUCTIONS_GETSOURCES, + rpc_methods::SESSION_GITHUBAUTH_LASTAUTHERRORS, Some(wire_params), ) .await?; @@ -5998,20 +6166,20 @@ impl<'a> SessionRpcInstructions<'a> { } } -/// `session.limitPrediction.*` RPCs. +/// `session.history.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcLimitPrediction<'a> { +pub struct SessionRpcHistory<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcLimitPrediction<'a> { - /// Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. +impl<'a> SessionRpcHistory<'a> { + /// Compacts the session history to reduce context usage. /// - /// Wire method: `session.limitPrediction.predict`. + /// Wire method: `session.history.compact`. /// /// # Returns /// - /// Prediction result. Available results include prediction details; unavailable results include an explicit reason. + /// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. /// ///
/// @@ -6020,30 +6188,27 @@ impl<'a> SessionRpcLimitPrediction<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn predict(&self) -> Result { + pub async fn compact(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_LIMITPREDICTION_PREDICT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_HISTORY_COMPACT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. + /// Compacts the session history to reduce context usage. /// - /// Wire method: `session.limitPrediction.predict`. + /// Wire method: `session.history.compact`. /// /// # Parameters /// - /// * `params` - Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. + /// * `params` - Optional compaction parameters. /// /// # Returns /// - /// Prediction result. Available results include prediction details; unavailable results include an explicit reason. + /// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. /// ///
/// @@ -6052,38 +6217,31 @@ impl<'a> SessionRpcLimitPrediction<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn predict_with_params( + pub async fn compact_with_params( &self, - params: SessionLimitPredictionRequest, - ) -> Result { + params: HistoryCompactRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_LIMITPREDICTION_PREDICT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_HISTORY_COMPACT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.lsp.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcLsp<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcLsp<'a> { - /// Loads the merged LSP configuration set for the session's working directory. + /// Truncates persisted session history to a specific event. /// - /// Wire method: `session.lsp.initialize`. + /// Wire method: `session.history.truncate`. /// /// # Parameters /// - /// * `params` - Parameters for (re)loading the merged LSP configuration set. + /// * `params` - Identifier of the event to truncate to; this event and all later events are removed. + /// + /// # Returns + /// + /// Number of events that were removed by the truncation. /// ///
/// @@ -6092,60 +6250,27 @@ impl<'a> SessionRpcLsp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn initialize(&self, params: LspInitializeRequest) -> Result<(), Error> { + pub async fn truncate( + &self, + params: HistoryTruncateRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_LSP_INITIALIZE, Some(wire_params)) + .call(rpc_methods::SESSION_HISTORY_TRUNCATE, Some(wire_params)) .await?; - Ok(()) - } -} - -/// `session.mcp.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcMcp<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcMcp<'a> { - /// `session.mcp.apps.*` sub-namespace. - pub fn apps(&self) -> SessionRpcMcpApps<'a> { - SessionRpcMcpApps { - session: self.session, - } - } - - /// `session.mcp.headers.*` sub-namespace. - pub fn headers(&self) -> SessionRpcMcpHeaders<'a> { - SessionRpcMcpHeaders { - session: self.session, - } - } - - /// `session.mcp.oauth.*` sub-namespace. - pub fn oauth(&self) -> SessionRpcMcpOauth<'a> { - SessionRpcMcpOauth { - session: self.session, - } - } - - /// `session.mcp.resources.*` sub-namespace. - pub fn resources(&self) -> SessionRpcMcpResources<'a> { - SessionRpcMcpResources { - session: self.session, - } + Ok(serde_json::from_value(_value)?) } - /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. + /// Lists the user turns that the session can rewind to. Never rejects for a busy session: rewind reads need the session's file-change captures to be settled, so a session that still holds active work answers with `unavailableReason: "session-busy"` and no points, which the caller can retry. /// - /// Wire method: `session.mcp.list`. + /// Wire method: `session.history.listRewindPoints`. /// /// # Returns /// - /// MCP servers configured for the session, with their connection status and host-level state. + /// Rewind points and file-change-tracking availability for the session. /// ///
/// @@ -6154,27 +6279,30 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list(&self) -> Result { + pub async fn list_rewind_points(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_LIST, Some(wire_params)) + .call( + rpc_methods::SESSION_HISTORY_LISTREWINDPOINTS, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. + /// Previews the files that a conversation-and-files rewind would restore. /// - /// Wire method: `session.mcp.listTools`. + /// Wire method: `session.history.previewRewind`. /// /// # Parameters /// - /// * `params` - Server name whose tool list should be returned. + /// * `params` - Event boundary to preview for conversation-and-files rewind. /// /// # Returns /// - /// Tools exposed by the connected MCP server. Throws when the server is not connected. + /// Files and aggregate changes for a prospective rewind. /// ///
/// @@ -6183,27 +6311,34 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list_tools( + pub async fn preview_rewind( &self, - params: McpListToolsRequest, - ) -> Result { + params: HistoryPreviewRewindRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_LISTTOOLS, Some(wire_params)) + .call( + rpc_methods::SESSION_HISTORY_PREVIEWREWIND, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Enables an MCP server for the session. + /// Rewinds the session conversation, optionally restoring files changed by the discarded turns. Not crash-atomic: file restore and conversation truncation are separate stores, applied in that order, so a process crash between them can leave the workspace rewound while the conversation still contains the discarded turns. There is no recovery journal; re-running the same rewind is the recovery path for a crash before truncation lands, since file restore is idempotent (already-restored files are reported as skipped) and truncation is re-derived from the still-retained boundary event. After truncation lands that boundary no longer exists, so the same request is rejected; the only stage that can still be outstanding is snapshot pruning, whose failure leaves orphan snapshots the capture store tolerates. The reverse inconsistency cannot occur, because truncation is never applied before file restore succeeds. /// - /// Wire method: `session.mcp.enable`. + /// Wire method: `session.history.rewind`. /// /// # Parameters /// - /// * `params` - Name of the MCP server to enable for the session. + /// * `params` - Boundary and mode for rewinding session history. + /// + /// # Returns + /// + /// Structured outcome of a rewind request. /// ///
/// @@ -6212,24 +6347,24 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn enable(&self, params: McpEnableRequest) -> Result<(), Error> { + pub async fn rewind(&self, params: HistoryRewindRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_ENABLE, Some(wire_params)) + .call(rpc_methods::SESSION_HISTORY_REWIND, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Disables an MCP server for the session. + /// Cancels any in-progress background compaction on a local session. /// - /// Wire method: `session.mcp.disable`. + /// Wire method: `session.history.cancelBackgroundCompaction`. /// - /// # Parameters + /// # Returns /// - /// * `params` - Name of the MCP server to disable for the session. + /// Indicates whether an in-progress background compaction was cancelled. /// ///
/// @@ -6238,20 +6373,28 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn disable(&self, params: McpDisableRequest) -> Result<(), Error> { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn cancel_background_compaction( + &self, + ) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_DISABLE, Some(wire_params)) + .call( + rpc_methods::SESSION_HISTORY_CANCELBACKGROUNDCOMPACTION, + Some(wire_params), + ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Reloads MCP server connections for the session. + /// Aborts any in-progress manual compaction on a local session. /// - /// Wire method: `session.mcp.reload`. + /// Wire method: `session.history.abortManualCompaction`. + /// + /// # Returns + /// + /// Indicates whether an in-progress manual compaction was aborted. /// ///
/// @@ -6260,23 +6403,28 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn reload(&self) -> Result<(), Error> { + pub async fn abort_manual_compaction( + &self, + ) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_RELOAD, Some(wire_params)) + .call( + rpc_methods::SESSION_HISTORY_ABORTMANUALCOMPACTION, + Some(wire_params), + ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Releases any turns waiting on an in-flight MCP load without cancelling the load, letting the agent proceed while MCP servers finish connecting in the background. No-op when no MCP load is in flight or waiting turns were already released. + /// Produces a markdown summary of the session's conversation context for hand-off scenarios. /// - /// Wire method: `session.mcp.moveLoadingToBackground`. + /// Wire method: `session.history.summarizeForHandoff`. /// /// # Returns /// - /// Result of moving in-flight MCP loading to the background. + /// Markdown summary of the conversation context (empty when not available). /// ///
/// @@ -6285,32 +6433,30 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn move_loading_to_background( - &self, - ) -> Result { + pub async fn summarize_for_handoff(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_MCP_MOVELOADINGTOBACKGROUND, + rpc_methods::SESSION_HISTORY_SUMMARIZEFORHANDOFF, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Reloads MCP server connections for the session with an explicit host-provided configuration. + /// Clears the session's conversation history, keeping only system and developer messages, and seeds the fresh context window with a first user message. Must be called from inside a tool handler: the clear has to drop the results of the tool calls its wipe orphans, and it rejects when no tool call is in flight. /// - /// Wire method: `session.mcp.reloadWithConfig`. + /// Wire method: `session.history.clearContext`. /// /// # Parameters /// - /// * `params` - Opaque MCP reload configuration. + /// * `params` - Parameters for clearing the conversation and seeding the window that replaces it. /// /// # Returns /// - /// MCP server startup filtering result. + /// What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. /// ///
/// @@ -6319,31 +6465,35 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn reload_with_config( + pub async fn clear_context( &self, - params: McpReloadWithConfigRequest, - ) -> Result { + params: HistoryClearContextRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_RELOADWITHCONFIG, Some(wire_params)) + .call(rpc_methods::SESSION_HISTORY_CLEARCONTEXT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Runs an MCP sampling inference on behalf of an MCP server. - /// - /// Wire method: `session.mcp.executeSampling`. - /// - /// # Parameters +/// `session.instructions.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcInstructions<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcInstructions<'a> { + /// Gets instruction sources loaded for the session. /// - /// * `params` - Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. + /// Wire method: `session.instructions.getSources`. /// /// # Returns /// - /// Outcome of an MCP sampling execution: success result, failure error, or cancellation. + /// Instruction sources loaded for the session, in merge order. /// ///
/// @@ -6352,31 +6502,34 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn execute_sampling( - &self, - params: McpExecuteSamplingParams, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn get_sources(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_EXECUTESAMPLING, Some(wire_params)) + .call( + rpc_methods::SESSION_INSTRUCTIONS_GETSOURCES, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Cancels an in-flight MCP sampling execution by request ID. - /// - /// Wire method: `session.mcp.cancelSamplingExecution`. - /// - /// # Parameters +/// `session.limitPrediction.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcLimitPrediction<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcLimitPrediction<'a> { + /// Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. /// - /// * `params` - The requestId previously passed to executeSampling that should be cancelled. + /// Wire method: `session.limitPrediction.predict`. /// /// # Returns /// - /// Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. + /// Prediction result. Available results include prediction details; unavailable results include an explicit reason. /// ///
/// @@ -6385,34 +6538,30 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn cancel_sampling_execution( - &self, - params: McpCancelSamplingExecutionParams, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn predict(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_MCP_CANCELSAMPLINGEXECUTION, + rpc_methods::SESSION_LIMITPREDICTION_PREDICT, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Sets how environment-variable values supplied to MCP servers are resolved (direct or indirect). + /// Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. /// - /// Wire method: `session.mcp.setEnvValueMode`. + /// Wire method: `session.limitPrediction.predict`. /// /// # Parameters /// - /// * `params` - Mode controlling how MCP server env values are resolved (`direct` or `indirect`). + /// * `params` - Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. /// /// # Returns /// - /// Env-value mode recorded on the session after the update. + /// Prediction result. Available results include prediction details; unavailable results include an explicit reason. /// ///
/// @@ -6421,27 +6570,38 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_env_value_mode( + pub async fn predict_with_params( &self, - params: McpSetEnvValueModeParams, - ) -> Result { + params: SessionLimitPredictionRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_SETENVVALUEMODE, Some(wire_params)) + .call( + rpc_methods::SESSION_LIMITPREDICTION_PREDICT, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Removes the auto-managed `github` MCP server when present. +/// `session.lsp.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcLsp<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcLsp<'a> { + /// Loads the merged LSP configuration set for the session's working directory. /// - /// Wire method: `session.mcp.removeGitHub`. + /// Wire method: `session.lsp.initialize`. /// - /// # Returns + /// # Parameters /// - /// Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove). + /// * `params` - Parameters for (re)loading the merged LSP configuration set. /// ///
/// @@ -6450,27 +6610,32 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn remove_git_hub(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn initialize(&self, params: LspInitializeRequest) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_REMOVEGITHUB, Some(wire_params)) + .call(rpc_methods::SESSION_LSP_INITIALIZE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } +} - /// Configures the built-in GitHub MCP server for the session's current auth context. - /// - /// Wire method: `session.mcp.configureGitHub`. - /// - /// # Parameters +/// `session.managedSettings.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcManagedSettings<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcManagedSettings<'a> { + /// Waits for the live session's in-flight managed-settings application, then returns the retained effective snapshot used by runtime enforcement and by `session.managed_settings_resolved`. It does not perform another account, device, or server resolution, and rejects when resolution has not produced a snapshot. /// - /// * `params` - Credential-free authentication identity used to configure GitHub MCP. + /// Wire method: `session.managedSettings.get`. /// /// # Returns /// - /// Result of configuring GitHub MCP. + /// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. /// ///
/// @@ -6479,27 +6644,59 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn configure_git_hub( - &self, - params: McpConfigureGitHubRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn get(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_CONFIGUREGITHUB, Some(wire_params)) + .call(rpc_methods::SESSION_MANAGEDSETTINGS_GET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Starts an individual MCP server on the live session. Omit `config` for a config-free start-by-name of an already-configured server (reuses the server's already-registered configuration); supply `config` to start from a caller-supplied configuration. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. +/// `session.mcp.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMcp<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMcp<'a> { + /// `session.mcp.apps.*` sub-namespace. + pub fn apps(&self) -> SessionRpcMcpApps<'a> { + SessionRpcMcpApps { + session: self.session, + } + } + + /// `session.mcp.headers.*` sub-namespace. + pub fn headers(&self) -> SessionRpcMcpHeaders<'a> { + SessionRpcMcpHeaders { + session: self.session, + } + } + + /// `session.mcp.oauth.*` sub-namespace. + pub fn oauth(&self) -> SessionRpcMcpOauth<'a> { + SessionRpcMcpOauth { + session: self.session, + } + } + + /// `session.mcp.resources.*` sub-namespace. + pub fn resources(&self) -> SessionRpcMcpResources<'a> { + SessionRpcMcpResources { + session: self.session, + } + } + + /// Lists MCP servers configured for the session, their connection status, and host-level state. The host-level state (disabled/filtered servers, failed/needs-auth/pending connections, mcp3p policy, full config) is empty/zero when no MCP host has been initialized for the session. /// - /// Wire method: `session.mcp.startServer`. + /// Wire method: `session.mcp.list`. /// - /// # Parameters + /// # Returns /// - /// * `params` - Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. + /// MCP servers configured for the session, with their connection status and host-level state. /// ///
/// @@ -6508,24 +6705,27 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn start_server(&self, params: McpStartServerRequest) -> Result<(), Error> { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_STARTSERVER, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_LIST, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). + /// Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session. /// - /// Wire method: `session.mcp.restartServer`. + /// Wire method: `session.mcp.listTools`. /// /// # Parameters /// - /// * `params` - Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. + /// * `params` - Server name whose tool list should be returned. + /// + /// # Returns + /// + /// Tools exposed by the connected MCP server. Throws when the server is not connected. /// ///
/// @@ -6534,24 +6734,27 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn restart_server(&self, params: McpRestartServerRequest) -> Result<(), Error> { + pub async fn list_tools( + &self, + params: McpListToolsRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_RESTARTSERVER, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_LISTTOOLS, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Stops an individual MCP server on the session's host. + /// Enables an MCP server for the session. /// - /// Wire method: `session.mcp.stopServer`. + /// Wire method: `session.mcp.enable`. /// /// # Parameters /// - /// * `params` - Server name for an individual MCP server stop. + /// * `params` - Name of the MCP server to enable for the session. /// ///
/// @@ -6560,24 +6763,24 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn stop_server(&self, params: McpStopServerRequest) -> Result<(), Error> { + pub async fn enable(&self, params: McpEnableRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_STOPSERVER, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_ENABLE, Some(wire_params)) .await?; Ok(()) } - /// Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. + /// Disables an MCP server for the session. /// - /// Wire method: `session.mcp.registerExternalClient`. + /// Wire method: `session.mcp.disable`. /// /// # Parameters /// - /// * `params` - Registration parameters for an external MCP client. + /// * `params` - Name of the MCP server to disable for the session. /// ///
/// @@ -6586,30 +6789,20 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn register_external_client( - &self, - params: McpRegisterExternalClientRequest, - ) -> Result<(), Error> { + pub async fn disable(&self, params: McpDisableRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_REGISTEREXTERNALCLIENT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_DISABLE, Some(wire_params)) .await?; Ok(()) } - /// Unregisters a previously registered external MCP client by server name. Marked internal as the paired companion of `registerExternalClient`: only in-process callers that registered a client this way can meaningfully unregister it. Disappears alongside `registerExternalClient`: once external clients are described to the runtime as config rather than handed in as instances, lifecycle (including deregistration) is owned entirely by the runtime. - /// - /// Wire method: `session.mcp.unregisterExternalClient`. - /// - /// # Parameters + /// Reloads MCP server connections for the session. /// - /// * `params` - Server name identifying the external client to remove. + /// Wire method: `session.mcp.reload`. /// ///
/// @@ -6618,34 +6811,23 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn unregister_external_client( - &self, - params: McpUnregisterExternalClientRequest, - ) -> Result<(), Error> { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn reload(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_UNREGISTEREXTERNALCLIENT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_RELOAD, Some(wire_params)) .await?; Ok(()) } - /// Checks whether a named MCP server is currently running on the session's host. - /// - /// Wire method: `session.mcp.isServerRunning`. - /// - /// # Parameters + /// Releases any turns waiting on an in-flight MCP load without cancelling the load, letting the agent proceed while MCP servers finish connecting in the background. No-op when no MCP load is in flight or waiting turns were already released. /// - /// * `params` - Server name to check running status for. + /// Wire method: `session.mcp.moveLoadingToBackground`. /// /// # Returns /// - /// Whether the named MCP server is running. + /// Result of moving in-flight MCP loading to the background. /// ///
/// @@ -6654,39 +6836,32 @@ impl<'a> SessionRpcMcp<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn is_server_running( + pub async fn move_loading_to_background( &self, - params: McpIsServerRunningRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + ) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_ISSERVERRUNNING, Some(wire_params)) + .call( + rpc_methods::SESSION_MCP_MOVELOADINGTOBACKGROUND, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.mcp.apps.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcMcpApps<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcMcpApps<'a> { - /// Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability. + /// Reloads MCP server connections for the session with an explicit host-provided configuration. /// - /// Wire method: `session.mcp.apps.readResource`. + /// Wire method: `session.mcp.reloadWithConfig`. /// /// # Parameters /// - /// * `params` - MCP server and resource URI to fetch. + /// * `params` - Opaque MCP reload configuration. /// /// # Returns /// - /// Resource contents returned by the MCP server. + /// MCP server startup filtering result. /// ///
/// @@ -6695,34 +6870,31 @@ impl<'a> SessionRpcMcpApps<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn read_resource( + pub(crate) async fn reload_with_config( &self, - params: McpAppsReadResourceRequest, - ) -> Result { + params: McpReloadWithConfigRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_APPS_READRESOURCE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_RELOADWITHCONFIG, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. + /// Runs an MCP sampling inference on behalf of an MCP server. /// - /// Wire method: `session.mcp.apps.listTools`. + /// Wire method: `session.mcp.executeSampling`. /// /// # Parameters /// - /// * `params` - MCP server to list app-callable tools for. + /// * `params` - Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. /// /// # Returns /// - /// App-callable tools from the named MCP server. + /// Outcome of an MCP sampling execution: success result, failure error, or cancellation. /// ///
/// @@ -6731,31 +6903,31 @@ impl<'a> SessionRpcMcpApps<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list_tools( + pub async fn execute_sampling( &self, - params: McpAppsListToolsRequest, - ) -> Result { + params: McpExecuteSamplingParams, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_APPS_LISTTOOLS, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_EXECUTESAMPLING, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. + /// Cancels an in-flight MCP sampling execution by request ID. /// - /// Wire method: `session.mcp.apps.callTool`. + /// Wire method: `session.mcp.cancelSamplingExecution`. /// /// # Parameters /// - /// * `params` - MCP server, tool name, and arguments to invoke from an MCP App view. + /// * `params` - The requestId previously passed to executeSampling that should be cancelled. /// /// # Returns /// - /// Standard MCP CallToolResult + /// Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. /// ///
/// @@ -6764,27 +6936,34 @@ impl<'a> SessionRpcMcpApps<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn call_tool( + pub async fn cancel_sampling_execution( &self, - params: McpAppsCallToolRequest, - ) -> Result { + params: McpCancelSamplingExecutionParams, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_APPS_CALLTOOL, Some(wire_params)) + .call( + rpc_methods::SESSION_MCP_CANCELSAMPLINGEXECUTION, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. + /// Sets how environment-variable values supplied to MCP servers are resolved (direct or indirect). /// - /// Wire method: `session.mcp.apps.setHostContext`. + /// Wire method: `session.mcp.setEnvValueMode`. /// /// # Parameters /// - /// * `params` - Host context to advertise to MCP App guests. + /// * `params` - Mode controlling how MCP server env values are resolved (`direct` or `indirect`). + /// + /// # Returns + /// + /// Env-value mode recorded on the session after the update. /// ///
/// @@ -6793,30 +6972,27 @@ impl<'a> SessionRpcMcpApps<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_host_context( + pub async fn set_env_value_mode( &self, - params: McpAppsSetHostContextRequest, - ) -> Result<(), Error> { + params: McpSetEnvValueModeParams, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_APPS_SETHOSTCONTEXT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_SETENVVALUEMODE, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Read the current host context advertised to MCP App guests. + /// Removes the auto-managed `github` MCP server when present. /// - /// Wire method: `session.mcp.apps.getHostContext`. + /// Wire method: `session.mcp.removeGitHub`. /// /// # Returns /// - /// Current host context advertised to MCP App guests. + /// Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove). /// ///
/// @@ -6825,30 +7001,27 @@ impl<'a> SessionRpcMcpApps<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_host_context(&self) -> Result { + pub async fn remove_git_hub(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_APPS_GETHOSTCONTEXT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_REMOVEGITHUB, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, feature-flag state, advertised extension, and how many tools have `_meta.ui` populated. + /// Configures the built-in GitHub MCP server for the session's current auth context. /// - /// Wire method: `session.mcp.apps.diagnose`. + /// Wire method: `session.mcp.configureGitHub`. /// /// # Parameters /// - /// * `params` - MCP server to diagnose MCP Apps wiring for. + /// * `params` - Credential-free authentication identity used to configure GitHub MCP. /// /// # Returns /// - /// Diagnostic snapshot of MCP Apps wiring for the named server. + /// Result of configuring GitHub MCP. /// ///
/// @@ -6857,39 +7030,27 @@ impl<'a> SessionRpcMcpApps<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn diagnose( + pub(crate) async fn configure_git_hub( &self, - params: McpAppsDiagnoseRequest, - ) -> Result { + params: McpConfigureGitHubRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_APPS_DIAGNOSE, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_CONFIGUREGITHUB, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `session.mcp.headers.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcMcpHeaders<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcMcpHeaders<'a> { - /// Responds to a pending MCP dynamic headers refresh request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide short-lived per-server headers or to indicate that no dynamic headers are available for this refresh. + /// Starts an individual MCP server on the live session. Omit `config` for a config-free start-by-name of an already-configured server (reuses the server's already-registered configuration); supply `config` to start from a caller-supplied configuration. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. /// - /// Wire method: `session.mcp.headers.handlePendingHeadersRefreshRequest`. + /// Wire method: `session.mcp.startServer`. /// /// # Parameters /// - /// * `params` - MCP headers refresh request id and the host response. - /// - /// # Returns - /// - /// Indicates whether the pending MCP headers refresh response was accepted. + /// * `params` - Server name and optional configuration for an individual MCP server start. Omit `config` for a config-free start-by-name of an already-configured server. /// ///
/// @@ -6898,42 +7059,24 @@ impl<'a> SessionRpcMcpHeaders<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_headers_refresh_request( - &self, - params: McpHeadersHandlePendingHeadersRefreshRequestRequest, - ) -> Result { + pub async fn start_server(&self, params: McpStartServerRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_HEADERS_HANDLEPENDINGHEADERSREFRESHREQUEST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_STARTSERVER, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } -} - -/// `session.mcp.oauth.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcMcpOauth<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcMcpOauth<'a> { - /// Resolves a pending MCP OAuth request with a host-provided token or cancellation. The pending request is emitted as mcp.oauth_required with the data necessary to authorize the request. + /// Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). /// - /// Wire method: `session.mcp.oauth.handlePendingRequest`. + /// Wire method: `session.mcp.restartServer`. /// /// # Parameters /// - /// * `params` - Pending MCP OAuth request ID and host-provided token or cancellation response. - /// - /// # Returns - /// - /// Indicates whether the pending MCP OAuth response was accepted. + /// * `params` - Server name and optional replacement configuration for an individual MCP server restart. Omit `config` for a config-free restart-by-name of an already-configured server. /// ///
/// @@ -6942,30 +7085,24 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_request( - &self, - params: McpOauthHandlePendingRequest, - ) -> Result { + pub async fn restart_server(&self, params: McpRestartServerRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_OAUTH_HANDLEPENDINGREQUEST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_RESTARTSERVER, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Notifies the session that MCP OAuth authentication succeeded and updated credentials were persisted, so cached tool definitions can be refreshed. + /// Stops an individual MCP server on the session's host. /// - /// Wire method: `session.mcp.oauth.authenticationStateChanged`. + /// Wire method: `session.mcp.stopServer`. /// /// # Parameters /// - /// * `params` - Identifies the MCP server whose persisted OAuth credentials were updated. + /// * `params` - Server name for an individual MCP server stop. /// ///
/// @@ -6974,34 +7111,24 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn authentication_state_changed( - &self, - params: McpOauthAuthenticationStateChangedRequest, - ) -> Result<(), Error> { + pub async fn stop_server(&self, params: McpStopServerRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_OAUTH_AUTHENTICATIONSTATECHANGED, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_STOPSERVER, Some(wire_params)) .await?; Ok(()) } - /// Starts OAuth authentication for a remote MCP server. + /// Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. /// - /// Wire method: `session.mcp.oauth.login`. + /// Wire method: `session.mcp.registerExternalClient`. /// /// # Parameters /// - /// * `params` - Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. - /// - /// # Returns - /// - /// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. + /// * `params` - Registration parameters for an external MCP client. /// ///
/// @@ -7010,28 +7137,30 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn login(&self, params: McpOauthLoginRequest) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub(crate) async fn register_external_client( + &self, + params: McpRegisterExternalClientRequest, + ) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_OAUTH_LOGIN, Some(wire_params)) + .call( + rpc_methods::SESSION_MCP_REGISTEREXTERNALCLIENT, + Some(wire_params), + ) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Passively probes a configured remote MCP server to classify whether OAuth is required or a cached/override token is accepted. Does not start OAuth, emit pending OAuth requests, or mutate MCP connection state. + /// Unregisters a previously registered external MCP client by server name. Marked internal as the paired companion of `registerExternalClient`: only in-process callers that registered a client this way can meaningfully unregister it. Disappears alongside `registerExternalClient`: once external clients are described to the runtime as config rather than handed in as instances, lifecycle (including deregistration) is owned entirely by the runtime. /// - /// Wire method: `session.mcp.oauth.probe`. + /// Wire method: `session.mcp.unregisterExternalClient`. /// /// # Parameters /// - /// * `params` - Remote MCP server name for a passive OAuth status probe. - /// - /// # Returns - /// - /// Passive MCP OAuth probe result. `authenticated` means the server accepted the probe request while an OAuth-origin access token was attached; it does not prove the server required or independently validated that token. The probe does not make a second unauthenticated request. Failed is an expected probe-domain outcome; JSON-RPC errors are reserved for API-call failures. + /// * `params` - Server name identifying the external client to remove. /// ///
/// @@ -7040,28 +7169,34 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn probe(&self, params: McpOauthProbeRequest) -> Result { + pub(crate) async fn unregister_external_client( + &self, + params: McpUnregisterExternalClientRequest, + ) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_OAUTH_PROBE, Some(wire_params)) + .call( + rpc_methods::SESSION_MCP_UNREGISTEREXTERNALCLIENT, + Some(wire_params), + ) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Responds to a pending MCP OAuth authorization request by its request id. + /// Checks whether a named MCP server is currently running on the session's host. /// - /// Wire method: `session.mcp.oauth.respond`. + /// Wire method: `session.mcp.isServerRunning`. /// /// # Parameters /// - /// * `params` - Pending MCP OAuth request id to respond to. + /// * `params` - Server name to check running status for. /// /// # Returns /// - /// Indicates whether the pending MCP OAuth response was accepted. + /// Whether the named MCP server is running. /// ///
/// @@ -7070,31 +7205,31 @@ impl<'a> SessionRpcMcpOauth<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn respond( + pub async fn is_server_running( &self, - params: McpOauthRespondRequest, - ) -> Result { + params: McpIsServerRunningRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_OAUTH_RESPOND, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_ISSERVERRUNNING, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `session.mcp.resources.*` RPCs. +/// `session.mcp.apps.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcMcpResources<'a> { +pub struct SessionRpcMcpApps<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcMcpResources<'a> { - /// Fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`). +impl<'a> SessionRpcMcpApps<'a> { + /// Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability. /// - /// Wire method: `session.mcp.resources.read`. + /// Wire method: `session.mcp.apps.readResource`. /// /// # Parameters /// @@ -7111,31 +7246,34 @@ impl<'a> SessionRpcMcpResources<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn read( + pub async fn read_resource( &self, - params: McpResourcesReadRequest, - ) -> Result { + params: McpAppsReadResourceRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_RESOURCES_READ, Some(wire_params)) + .call( + rpc_methods::SESSION_MCP_APPS_READRESOURCE, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + /// List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. /// - /// Wire method: `session.mcp.resources.list`. + /// Wire method: `session.mcp.apps.listTools`. /// /// # Parameters /// - /// * `params` - MCP server whose resources to enumerate. + /// * `params` - MCP server to list app-callable tools for. /// /// # Returns /// - /// One page of resources advertised by the named MCP server. + /// App-callable tools from the named MCP server. /// ///
/// @@ -7144,31 +7282,31 @@ impl<'a> SessionRpcMcpResources<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list( + pub async fn list_tools( &self, - params: McpResourcesListRequest, - ) -> Result { + params: McpAppsListToolsRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MCP_RESOURCES_LIST, Some(wire_params)) + .call(rpc_methods::SESSION_MCP_APPS_LISTTOOLS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + /// Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. /// - /// Wire method: `session.mcp.resources.listTemplates`. + /// Wire method: `session.mcp.apps.callTool`. /// /// # Parameters /// - /// * `params` - MCP server whose resource templates to enumerate. + /// * `params` - MCP server, tool name, and arguments to invoke from an MCP App view. /// /// # Returns /// - /// One page of resource templates advertised by the named MCP server. + /// Standard MCP CallToolResult /// ///
/// @@ -7177,38 +7315,27 @@ impl<'a> SessionRpcMcpResources<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list_templates( + pub async fn call_tool( &self, - params: McpResourcesListTemplatesRequest, - ) -> Result { + params: McpAppsCallToolRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MCP_RESOURCES_LISTTEMPLATES, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_APPS_CALLTOOL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.metadata.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcMetadata<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcMetadata<'a> { - /// Returns a snapshot of the session's identifying metadata, mode, agent, and remote info. + /// Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. /// - /// Wire method: `session.metadata.snapshot`. + /// Wire method: `session.mcp.apps.setHostContext`. /// - /// # Returns + /// # Parameters /// - /// Point-in-time snapshot of slow-changing session identifier and state fields + /// * `params` - Host context to advertise to MCP App guests. /// ///
/// @@ -7217,23 +7344,30 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn snapshot(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn set_host_context( + &self, + params: McpAppsSetHostContextRequest, + ) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_METADATA_SNAPSHOT, Some(wire_params)) + .call( + rpc_methods::SESSION_MCP_APPS_SETHOSTCONTEXT, + Some(wire_params), + ) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Returns the client-owned string metadata persisted with this local session. The metadata is not included in model context, events, telemetry, snapshots, or remote exports. + /// Read the current host context advertised to MCP App guests. /// - /// Wire method: `session.metadata.getClientMetadata`. + /// Wire method: `session.mcp.apps.getHostContext`. /// /// # Returns /// - /// Client-owned, case-sensitive string metadata persisted with a local session. Clients should namespace keys by owner. Keys must be non-empty and at most 256 UTF-8 bytes; keys under `copilot/` and `github/` are reserved. Values may contain at most 16 KiB of UTF-8 data. A bag may contain at most 128 entries and its serialized sidecar may contain at most 64 KiB. The runtime stores but never interprets these values. + /// Current host context advertised to MCP App guests. /// ///
/// @@ -7242,30 +7376,30 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_client_metadata(&self) -> Result { + pub async fn get_host_context(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_METADATA_GETCLIENTMETADATA, + rpc_methods::SESSION_MCP_APPS_GETHOSTCONTEXT, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Atomically patches the client-owned string metadata persisted with this local session and returns the committed bag. + /// Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, feature-flag state, advertised extension, and how many tools have `_meta.ui` populated. /// - /// Wire method: `session.metadata.updateClientMetadata`. + /// Wire method: `session.mcp.apps.diagnose`. /// /// # Parameters /// - /// * `params` - Atomic patch for client-owned session metadata. Operations apply in clear, remove, then set order. The resulting bag must satisfy the ClientMetadata entry and serialized-size limits. Local storage coordinates concurrent runtime processes; custom SessionFs providers must serialize writers that access the same session from multiple processes. + /// * `params` - MCP server to diagnose MCP Apps wiring for. /// /// # Returns /// - /// Client-owned, case-sensitive string metadata persisted with a local session. Clients should namespace keys by owner. Keys must be non-empty and at most 256 UTF-8 bytes; keys under `copilot/` and `github/` are reserved. Values may contain at most 16 KiB of UTF-8 data. A bag may contain at most 128 entries and its serialized sidecar may contain at most 64 KiB. The runtime stores but never interprets these values. + /// Diagnostic snapshot of MCP Apps wiring for the named server. /// ///
/// @@ -7274,30 +7408,39 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn update_client_metadata( + pub async fn diagnose( &self, - params: MetadataUpdateClientMetadataRequest, - ) -> Result { + params: McpAppsDiagnoseRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_METADATA_UPDATECLIENTMETADATA, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_APPS_DIAGNOSE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Reports whether the local session is currently processing user/agent messages. +/// `session.mcp.headers.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMcpHeaders<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMcpHeaders<'a> { + /// Responds to a pending MCP dynamic headers refresh request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide short-lived per-server headers or to indicate that no dynamic headers are available for this refresh. /// - /// Wire method: `session.metadata.isProcessing`. + /// Wire method: `session.mcp.headers.handlePendingHeadersRefreshRequest`. + /// + /// # Parameters + /// + /// * `params` - MCP headers refresh request id and the host response. /// /// # Returns /// - /// Indicates whether the local session is currently processing a turn or background continuation. + /// Indicates whether the pending MCP headers refresh response was accepted. /// ///
/// @@ -7306,26 +7449,42 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn is_processing(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn handle_pending_headers_refresh_request( + &self, + params: McpHeadersHandlePendingHeadersRefreshRequestRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_METADATA_ISPROCESSING, + rpc_methods::SESSION_MCP_HEADERS_HANDLEPENDINGHEADERSREFRESHREQUEST, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Returns a snapshot of activity flags for the session. +/// `session.mcp.oauth.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMcpOauth<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMcpOauth<'a> { + /// Resolves a pending MCP OAuth request with a host-provided token or cancellation. The pending request is emitted as mcp.oauth_required with the data necessary to authorize the request. /// - /// Wire method: `session.metadata.activity`. + /// Wire method: `session.mcp.oauth.handlePendingRequest`. + /// + /// # Parameters + /// + /// * `params` - Pending MCP OAuth request ID and host-provided token or cancellation response. /// /// # Returns /// - /// Current activity flags for the session. + /// Indicates whether the pending MCP OAuth response was accepted. /// ///
/// @@ -7334,27 +7493,30 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn activity(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn handle_pending_request( + &self, + params: McpOauthHandlePendingRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_METADATA_ACTIVITY, Some(wire_params)) + .call( + rpc_methods::SESSION_MCP_OAUTH_HANDLEPENDINGREQUEST, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the token breakdown for the session's current context window for a given model. + /// Notifies the session that MCP OAuth authentication succeeded and updated credentials were persisted, so cached tool definitions can be refreshed. /// - /// Wire method: `session.metadata.contextInfo`. + /// Wire method: `session.mcp.oauth.authenticationStateChanged`. /// /// # Parameters /// - /// * `params` - Model identifier and token limits used to compute the context-info breakdown. - /// - /// # Returns - /// - /// Token breakdown for the session's current context window, or null if uninitialized. + /// * `params` - Identifies the MCP server whose persisted OAuth credentials were updated. /// ///
/// @@ -7363,27 +7525,34 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn context_info( + pub async fn authentication_state_changed( &self, - params: MetadataContextInfoRequest, - ) -> Result { + params: McpOauthAuthenticationStateChangedRequest, + ) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_METADATA_CONTEXTINFO, Some(wire_params)) + .call( + rpc_methods::SESSION_MCP_OAUTH_AUTHENTICATIONSTATECHANGED, + Some(wire_params), + ) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Returns the experimental per-source attribution breakdown of the session's current context window as a flat list of entries (skills, subagents, MCP servers, built-in tools, plugin rollups, system/tool-definition costs, with nesting via parentId), plus the successful compaction count. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. Returns null until the session has initialized its system prompt and tool metadata. + /// Starts OAuth authentication for a remote MCP server. /// - /// Wire method: `session.metadata.getContextAttribution`. + /// Wire method: `session.mcp.oauth.login`. /// - /// # Returns + /// # Parameters /// - /// Per-source attribution breakdown for the session's current context window, or null if uninitialized. + /// * `params` - Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. + /// + /// # Returns + /// + /// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. /// ///
/// @@ -7392,30 +7561,28 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_context_attribution(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn login(&self, params: McpOauthLoginRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_METADATA_GETCONTEXTATTRIBUTION, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_OAUTH_LOGIN, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the largest individual messages currently in the session's context window, most-expensive first. Companion to `metadata.getContextAttribution`. Returns an empty list until the session has initialized. + /// Passively probes a configured remote MCP server to classify whether OAuth is required or a cached/override token is accepted. Does not start OAuth, emit pending OAuth requests, or mutate MCP connection state. /// - /// Wire method: `session.metadata.getContextHeaviestMessages`. + /// Wire method: `session.mcp.oauth.probe`. /// /// # Parameters /// - /// * `params` - Parameters for the heaviest-messages query. + /// * `params` - Remote MCP server name for a passive OAuth status probe. /// /// # Returns /// - /// The heaviest individual messages in the session's context window, most-expensive first. + /// Passive MCP OAuth probe result. `authenticated` means the server accepted the probe request while an OAuth-origin access token was attached; it does not prove the server required or independently validated that token. The probe does not make a second unauthenticated request. Failed is an expected probe-domain outcome; JSON-RPC errors are reserved for API-call failures. /// ///
/// @@ -7424,34 +7591,28 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_context_heaviest_messages( - &self, - params: MetadataContextHeaviestMessagesRequest, - ) -> Result { + pub async fn probe(&self, params: McpOauthProbeRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_METADATA_GETCONTEXTHEAVIESTMESSAGES, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_OAUTH_PROBE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Records a working-directory/git context change and emits a `session.context_changed` event. For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event): a local session's working directory is authoritative and is moved via `metadata.setWorkingDirectory` (or an SDK `session.resume` that supplies a `workingDirectory`), not by this method. + /// Responds to a pending MCP OAuth authorization request by its request id. /// - /// Wire method: `session.metadata.recordContextChange`. + /// Wire method: `session.mcp.oauth.respond`. /// /// # Parameters /// - /// * `params` - Updated working-directory/git context to record on the session. + /// * `params` - Pending MCP OAuth request id to respond to. /// /// # Returns /// - /// Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode). For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event); move a local session's working directory via `metadata.setWorkingDirectory` instead. + /// Indicates whether the pending MCP OAuth response was accepted. /// ///
/// @@ -7460,34 +7621,39 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn record_context_change( + pub async fn respond( &self, - params: MetadataRecordContextChangeRequest, - ) -> Result { + params: McpOauthRespondRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_METADATA_RECORDCONTEXTCHANGE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_OAUTH_RESPOND, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. +/// `session.mcp.resources.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMcpResources<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMcpResources<'a> { + /// Fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`). /// - /// Wire method: `session.metadata.setWorkingDirectory`. + /// Wire method: `session.mcp.resources.read`. /// /// # Parameters /// - /// * `params` - Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. + /// * `params` - MCP server and resource URI to fetch. /// /// # Returns /// - /// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. + /// Resource contents returned by the MCP server. /// ///
/// @@ -7496,34 +7662,31 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_working_directory( + pub async fn read( &self, - params: MetadataSetWorkingDirectoryRequest, - ) -> Result { + params: McpResourcesReadRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_METADATA_SETWORKINGDIRECTORY, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_RESOURCES_READ, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Re-tokenizes the session's existing messages against a model and returns aggregate token totals. + /// Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. /// - /// Wire method: `session.metadata.recomputeContextTokens`. + /// Wire method: `session.mcp.resources.list`. /// /// # Parameters /// - /// * `params` - Model identifier to use when re-tokenizing the session's existing messages. + /// * `params` - MCP server whose resources to enumerate. /// /// # Returns /// - /// Re-tokenize the session's existing messages against `modelId` and return the token totals. Useful for hosts that want an initial estimate of context usage on session resume, before the next agent turn fires `session.context_info_changed` events. Returns zeros for an empty session. + /// One page of resources advertised by the named MCP server. /// ///
/// @@ -7532,38 +7695,31 @@ impl<'a> SessionRpcMetadata<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn recompute_context_tokens( + pub async fn list( &self, - params: MetadataRecomputeContextTokensRequest, - ) -> Result { + params: McpResourcesListRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_METADATA_RECOMPUTECONTEXTTOKENS, - Some(wire_params), - ) + .call(rpc_methods::SESSION_MCP_RESOURCES_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.mode.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcMode<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcMode<'a> { - /// Gets the current agent interaction mode. + /// Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. /// - /// Wire method: `session.mode.get`. + /// Wire method: `session.mcp.resources.listTemplates`. + /// + /// # Parameters + /// + /// * `params` - MCP server whose resource templates to enumerate. /// /// # Returns /// - /// The session mode the agent is operating in + /// One page of resource templates advertised by the named MCP server. /// ///
/// @@ -7572,27 +7728,38 @@ impl<'a> SessionRpcMode<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn list_templates( + &self, + params: McpResourcesListTemplatesRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MODE_GET, Some(wire_params)) + .call( + rpc_methods::SESSION_MCP_RESOURCES_LISTTEMPLATES, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Sets the current agent interaction mode. - /// - /// Wire method: `session.mode.set`. - /// - /// # Parameters +/// `session.metadata.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMetadata<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMetadata<'a> { + /// Returns a snapshot of the session's identifying metadata, mode, agent, and remote info. /// - /// * `params` - Agent interaction mode to apply to the session. + /// Wire method: `session.metadata.snapshot`. /// /// # Returns /// - /// Outcome of a session mode change, including any model switch it triggered and follow-up the host must perform. + /// Point-in-time snapshot of slow-changing session identifier and state fields /// ///
/// @@ -7601,32 +7768,23 @@ impl<'a> SessionRpcMode<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set(&self, params: ModeSetRequest) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn snapshot(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MODE_SET, Some(wire_params)) + .call(rpc_methods::SESSION_METADATA_SNAPSHOT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.model.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcModel<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcModel<'a> { - /// Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. + /// Returns the client-owned string metadata persisted with this local session. The metadata is not included in model context, events, telemetry, snapshots, or remote exports. /// - /// Wire method: `session.model.getCurrent`. + /// Wire method: `session.metadata.getClientMetadata`. /// /// # Returns /// - /// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + /// Client-owned, case-sensitive string metadata persisted with a local session. Clients should namespace keys by owner. Keys must be non-empty and at most 256 UTF-8 bytes; keys under `copilot/` and `github/` are reserved. Values may contain at most 16 KiB of UTF-8 data. A bag may contain at most 128 entries and its serialized sidecar may contain at most 64 KiB. The runtime stores but never interprets these values. /// ///
/// @@ -7635,27 +7793,30 @@ impl<'a> SessionRpcModel<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_current(&self) -> Result { + pub async fn get_client_metadata(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MODEL_GETCURRENT, Some(wire_params)) + .call( + rpc_methods::SESSION_METADATA_GETCLIENTMETADATA, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Switches the session to a model and optional reasoning configuration. + /// Atomically patches the client-owned string metadata persisted with this local session and returns the committed bag. /// - /// Wire method: `session.model.switchTo`. + /// Wire method: `session.metadata.updateClientMetadata`. /// /// # Parameters /// - /// * `params` - Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. + /// * `params` - Atomic patch for client-owned session metadata. Operations apply in clear, remove, then set order. The resulting bag must satisfy the ClientMetadata entry and serialized-size limits. Local storage coordinates concurrent runtime processes; custom SessionFs providers must serialize writers that access the same session from multiple processes. /// /// # Returns /// - /// The model identifier active on the session after the switch. + /// Client-owned, case-sensitive string metadata persisted with a local session. Clients should namespace keys by owner. Keys must be non-empty and at most 256 UTF-8 bytes; keys under `copilot/` and `github/` are reserved. Values may contain at most 16 KiB of UTF-8 data. A bag may contain at most 128 entries and its serialized sidecar may contain at most 64 KiB. The runtime stores but never interprets these values. /// ///
/// @@ -7664,31 +7825,30 @@ impl<'a> SessionRpcModel<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn switch_to( + pub async fn update_client_metadata( &self, - params: ModelSwitchToRequest, - ) -> Result { + params: MetadataUpdateClientMetadataRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MODEL_SWITCHTO, Some(wire_params)) + .call( + rpc_methods::SESSION_METADATA_UPDATECLIENTMETADATA, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. - /// - /// Wire method: `session.model.switchAutoTier`. - /// - /// # Parameters + /// Reports whether the local session is currently processing user/agent messages. /// - /// * `params` - An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + /// Wire method: `session.metadata.isProcessing`. /// /// # Returns /// - /// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + /// Indicates whether the local session is currently processing a turn or background continuation. /// ///
/// @@ -7697,31 +7857,26 @@ impl<'a> SessionRpcModel<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn switch_auto_tier( - &self, - params: ModelSwitchAutoTierRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn is_processing(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_MODEL_SWITCHAUTOTIER, Some(wire_params)) + .call( + rpc_methods::SESSION_METADATA_ISPROCESSING, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Resolves and applies organization-managed and repository model overlays. - /// - /// Wire method: `session.model.applyStartupOverlay`. - /// - /// # Parameters + /// Returns a snapshot of activity flags for the session. /// - /// * `params` - Managed, repository, and CLI model overrides to overlay onto the session at startup. + /// Wire method: `session.metadata.activity`. /// /// # Returns /// - /// The model identifier active on the session after the switch. + /// Current activity flags for the session. /// ///
/// @@ -7730,34 +7885,27 @@ impl<'a> SessionRpcModel<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn apply_startup_overlay( - &self, - params: ModelApplyStartupOverlayRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn activity(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_MODEL_APPLYSTARTUPOVERLAY, - Some(wire_params), - ) + .call(rpc_methods::SESSION_METADATA_ACTIVITY, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Replaces or clears the host-supplied model allowlist for a running session. + /// Returns the token breakdown for the session's current context window for a given model. /// - /// Wire method: `session.model.setAllowedModels`. + /// Wire method: `session.metadata.contextInfo`. /// /// # Parameters /// - /// * `params` - Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. + /// * `params` - Model identifier and token limits used to compute the context-info breakdown. /// /// # Returns /// - /// The applied host allowlist and effective session model policy after intersection. + /// Token breakdown for the session's current context window, or null if uninitialized. /// ///
/// @@ -7766,34 +7914,1267 @@ impl<'a> SessionRpcModel<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_allowed_models( + pub async fn context_info( &self, - params: ModelSetAllowedModelsRequest, - ) -> Result { + params: MetadataContextInfoRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_MODEL_SETALLOWEDMODELS, + .call(rpc_methods::SESSION_METADATA_CONTEXTINFO, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the experimental per-source attribution breakdown of the session's current context window as a flat list of entries (skills, subagents, MCP servers, built-in tools, plugin rollups, system/tool-definition costs, with nesting via parentId), plus the successful compaction count. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. Returns null until the session has initialized its system prompt and tool metadata. + /// + /// Wire method: `session.metadata.getContextAttribution`. + /// + /// # Returns + /// + /// Per-source attribution breakdown for the session's current context window, or null if uninitialized. + /// + ///
+ /// + /// **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 get_context_attribution(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_METADATA_GETCONTEXTATTRIBUTION, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the largest individual messages currently in the session's context window, most-expensive first. Companion to `metadata.getContextAttribution`. Returns an empty list until the session has initialized. + /// + /// Wire method: `session.metadata.getContextHeaviestMessages`. + /// + /// # Parameters + /// + /// * `params` - Parameters for the heaviest-messages query. + /// + /// # Returns + /// + /// The heaviest individual messages in the session's context window, most-expensive first. + /// + ///
+ /// + /// **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 get_context_heaviest_messages( + &self, + params: MetadataContextHeaviestMessagesRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_METADATA_GETCONTEXTHEAVIESTMESSAGES, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Records a working-directory/git context change and emits a `session.context_changed` event. For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event): a local session's working directory is authoritative and is moved via `metadata.setWorkingDirectory` (or an SDK `session.resume` that supplies a `workingDirectory`), not by this method. + /// + /// Wire method: `session.metadata.recordContextChange`. + /// + /// # Parameters + /// + /// * `params` - Updated working-directory/git context to record on the session. + /// + /// # Returns + /// + /// Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode). For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event); move a local session's working directory via `metadata.setWorkingDirectory` instead. + /// + ///
+ /// + /// **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 record_context_change( + &self, + params: MetadataRecordContextChangeRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_METADATA_RECORDCONTEXTCHANGE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. + /// + /// Wire method: `session.metadata.setWorkingDirectory`. + /// + /// # Parameters + /// + /// * `params` - Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is. + /// + /// # Returns + /// + /// Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path. + /// + ///
+ /// + /// **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 set_working_directory( + &self, + params: MetadataSetWorkingDirectoryRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_METADATA_SETWORKINGDIRECTORY, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Re-tokenizes the session's existing messages against a model and returns aggregate token totals. + /// + /// Wire method: `session.metadata.recomputeContextTokens`. + /// + /// # Parameters + /// + /// * `params` - Model identifier to use when re-tokenizing the session's existing messages. + /// + /// # Returns + /// + /// Re-tokenize the session's existing messages against `modelId` and return the token totals. Useful for hosts that want an initial estimate of context usage on session resume, before the next agent turn fires `session.context_info_changed` events. Returns zeros for an empty session. + /// + ///
+ /// + /// **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 recompute_context_tokens( + &self, + params: MetadataRecomputeContextTokensRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_METADATA_RECOMPUTECONTEXTTOKENS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.mode.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMode<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMode<'a> { + /// Gets the current agent interaction mode. + /// + /// Wire method: `session.mode.get`. + /// + /// # Returns + /// + /// The session mode the agent is operating in + /// + ///
+ /// + /// **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 get(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MODE_GET, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Sets the current agent interaction mode. + /// + /// Wire method: `session.mode.set`. + /// + /// # Parameters + /// + /// * `params` - Agent interaction mode to apply to the session. + /// + /// # Returns + /// + /// Outcome of a session mode change, including any model switch it triggered and follow-up the host must perform. + /// + ///
+ /// + /// **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 set(&self, params: ModeSetRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MODE_SET, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.model.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcModel<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcModel<'a> { + /// Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. + /// + /// Wire method: `session.model.getCurrent`. + /// + /// # Returns + /// + /// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + /// + ///
+ /// + /// **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 get_current(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MODEL_GETCURRENT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Switches the session to a model and optional reasoning configuration. + /// + /// Wire method: `session.model.switchTo`. + /// + /// # Parameters + /// + /// * `params` - Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. + /// + /// # Returns + /// + /// The model identifier active on the session after the switch. + /// + ///
+ /// + /// **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 switch_to( + &self, + params: ModelSwitchToRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MODEL_SWITCHTO, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. + /// + /// Wire method: `session.model.switchAutoTier`. + /// + /// # Parameters + /// + /// * `params` - An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + /// + /// # Returns + /// + /// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + /// + ///
+ /// + /// **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 switch_auto_tier( + &self, + params: ModelSwitchAutoTierRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MODEL_SWITCHAUTOTIER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Resolves and applies organization-managed and repository model overlays. + /// + /// Wire method: `session.model.applyStartupOverlay`. + /// + /// # Parameters + /// + /// * `params` - Managed, repository, and CLI model overrides to overlay onto the session at startup. + /// + /// # Returns + /// + /// The model identifier active on the session after the switch. + /// + ///
+ /// + /// **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(crate) async fn apply_startup_overlay( + &self, + params: ModelApplyStartupOverlayRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MODEL_APPLYSTARTUPOVERLAY, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Replaces or clears the host-supplied model allowlist for a running session. + /// + /// Wire method: `session.model.setAllowedModels`. + /// + /// # Parameters + /// + /// * `params` - Host-supplied exact model selection IDs to allow for this running session. CAPI IDs are intersected with repository `.github/allowed_models.txt` policy; provider-qualified IDs remain exempt from repository-only policy but are restricted by this host list. Omit or pass null to clear the host restriction; an explicit empty or disjoint list is rejected. Validation and pre-selection fallback failures preserve the previous restriction. Failures after a fallback selection commits retain the new restriction and selected model; callers should inspect current session state after such an error. + /// + /// # Returns + /// + /// The applied host allowlist and effective session model policy after intersection. + /// + ///
+ /// + /// **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 set_allowed_models( + &self, + params: ModelSetAllowedModelsRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MODEL_SETALLOWEDMODELS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Updates the session's reasoning effort without changing the selected model. + /// + /// Wire method: `session.model.setReasoningEffort`. + /// + /// # Parameters + /// + /// * `params` - Reasoning effort level to apply to the currently selected model. + /// + /// # Returns + /// + /// Update the session's reasoning effort without changing the selected model. Use `switchTo` instead when you also need to change the model. The runtime stores the effort on the session and applies it to subsequent turns. + /// + ///
+ /// + /// **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 set_reasoning_effort( + &self, + params: ModelSetReasoningEffortRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MODEL_SETREASONINGEFFORT, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. + /// + /// Wire method: `session.model.list`. + /// + /// # Returns + /// + /// The list of models available to this session. + /// + ///
+ /// + /// **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 list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MODEL_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. + /// + /// Wire method: `session.model.list`. + /// + /// # Parameters + /// + /// * `params` - Optional listing options. + /// + /// # Returns + /// + /// The list of models available to this session. + /// + ///
+ /// + /// **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 list_with_params( + &self, + params: ModelListRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MODEL_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.name.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcName<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcName<'a> { + /// Gets the session's friendly name. + /// + /// Wire method: `session.name.get`. + /// + /// # Returns + /// + /// The session's friendly name, or null when not yet set. + /// + ///
+ /// + /// **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 get(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_NAME_GET, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Sets the session's friendly name. + /// + /// Wire method: `session.name.set`. + /// + /// # Parameters + /// + /// * `params` - New friendly name to apply to the session. + /// + ///
+ /// + /// **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 set(&self, params: NameSetRequest) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_NAME_SET, Some(wire_params)) + .await?; + Ok(()) + } + + /// Persists an auto-generated session summary as the session's name when no user-set name exists. + /// + /// Wire method: `session.name.setAuto`. + /// + /// # Parameters + /// + /// * `params` - Auto-generated session summary to apply as the session's name when no user-set name exists. + /// + /// # Returns + /// + /// Indicates whether the auto-generated summary was applied as the session's name. + /// + ///
+ /// + /// **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 set_auto(&self, params: NameSetAutoRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_NAME_SETAUTO, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.options.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcOptions<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcOptions<'a> { + /// Patches the genuinely-mutable subset of session options. + /// + /// Wire method: `session.options.update`. + /// + /// # Parameters + /// + /// * `params` - Patch of mutable session options to apply to the running session. + /// + /// # Returns + /// + /// Indicates whether the session options patch was applied successfully. + /// + ///
+ /// + /// **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 update( + &self, + params: SessionUpdateOptionsParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_OPTIONS_UPDATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.permissions.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcPermissions<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcPermissions<'a> { + /// `session.permissions.folderTrust.*` sub-namespace. + pub fn folder_trust(&self) -> SessionRpcPermissionsFolderTrust<'a> { + SessionRpcPermissionsFolderTrust { + session: self.session, + } + } + + /// `session.permissions.locations.*` sub-namespace. + pub fn locations(&self) -> SessionRpcPermissionsLocations<'a> { + SessionRpcPermissionsLocations { + session: self.session, + } + } + + /// `session.permissions.paths.*` sub-namespace. + pub fn paths(&self) -> SessionRpcPermissionsPaths<'a> { + SessionRpcPermissionsPaths { + session: self.session, + } + } + + /// `session.permissions.urls.*` sub-namespace. + pub fn urls(&self) -> SessionRpcPermissionsUrls<'a> { + SessionRpcPermissionsUrls { + session: self.session, + } + } + + /// Replaces selected permission policy fields (rules, paths, URLs, exclusions, allow-all flags) on the session. + /// + /// Wire method: `session.permissions.configure`. + /// + /// # Parameters + /// + /// * `params` - Patch of permission policy fields to apply (omit a field to leave it unchanged). + /// + /// # Returns + /// + /// Indicates whether the operation succeeded. + /// + ///
+ /// + /// **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 configure( + &self, + params: PermissionsConfigureParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_CONFIGURE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Provides a decision for a pending tool permission request. + /// + /// Wire method: `session.permissions.handlePendingPermissionRequest`. + /// + /// # Parameters + /// + /// * `params` - Pending permission request ID and the decision to apply (approve/reject and scope). + /// + /// # Returns + /// + /// Indicates whether the permission decision was applied; false when the request was already resolved. + /// + ///
+ /// + /// **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 handle_pending_permission_request( + &self, + params: PermissionDecisionRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Reconstructs the set of pending tool permission requests from the session's event history. + /// + /// Wire method: `session.permissions.pendingRequests`. + /// + /// # Returns + /// + /// List of pending permission requests reconstructed from event history. + /// + ///
+ /// + /// **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 pending_requests(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_PENDINGREQUESTS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Enables or disables automatic approval of tool permission requests for the session. + /// + /// Wire method: `session.permissions.setApproveAll`. + /// + /// # Parameters + /// + /// * `params` - Allow-all toggle for tool permission requests, with an optional telemetry source. + /// + /// # Returns + /// + /// Indicates whether the operation succeeded. + /// + ///
+ /// + /// **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 set_approve_all( + &self, + params: PermissionsSetApproveAllRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_SETAPPROVEALL, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Sets the permission mode for the session. `manual` follows the normal approval flow, `assisted` attaches LLM safety recommendations, and `allow-all` automatically approves permission requests. The result returns the authoritative post-mutation mode so callers can update local state without racing the `session.permissions_changed` notification. + /// + /// Wire method: `session.permissions.setMode`. + /// + /// # Parameters + /// + /// * `params` - Permission mode to apply for the session. + /// + /// # Returns + /// + /// Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. + /// + ///
+ /// + /// **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 set_mode( + &self, + params: PermissionsSetModeRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PERMISSIONS_SETMODE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the current permission mode for the session. + /// + /// Wire method: `session.permissions.getMode`. + /// + /// # Returns + /// + /// Current permission mode. + /// + ///
+ /// + /// **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 get_mode(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PERMISSIONS_GETMODE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Adds or removes session-scoped or location-scoped permission rules. + /// + /// Wire method: `session.permissions.modifyRules`. + /// + /// # Parameters + /// + /// * `params` - Scope and add/remove instructions for modifying session- or location-scoped permission rules. + /// + /// # Returns + /// + /// Indicates whether the operation succeeded. + /// + ///
+ /// + /// **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 modify_rules( + &self, + params: PermissionsModifyRulesParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_MODIFYRULES, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Sets whether the client wants permission prompts bridged into session events. + /// + /// Wire method: `session.permissions.setRequired`. + /// + /// # Parameters + /// + /// * `params` - Toggles whether permission prompts should be bridged into session events for this client. + /// + /// # Returns + /// + /// Indicates whether the operation succeeded. + /// + ///
+ /// + /// **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 set_required( + &self, + params: PermissionsSetRequiredRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_SETREQUIRED, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Clears session-scoped tool permission approvals. + /// + /// Wire method: `session.permissions.resetSessionApprovals`. + /// + /// # Parameters + /// + /// * `params` - Clears session-scoped tool permission approvals, and optionally the location-scoped ones. + /// + /// # Returns + /// + /// Indicates whether the operation succeeded. + /// + ///
+ /// + /// **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 reset_session_approvals( + &self, + params: PermissionsResetSessionApprovalsRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_RESETSESSIONAPPROVALS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Notifies the runtime that a permission prompt UI has been shown to the user. + /// + /// Wire method: `session.permissions.notifyPromptShown`. + /// + /// # Parameters + /// + /// * `params` - Notification payload describing the permission prompt that the client just rendered. + /// + /// # Returns + /// + /// Indicates whether the operation succeeded. + /// + ///
+ /// + /// **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 notify_prompt_shown( + &self, + params: PermissionPromptShownNotification, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_NOTIFYPROMPTSHOWN, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.permissions.folderTrust.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcPermissionsFolderTrust<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcPermissionsFolderTrust<'a> { + /// Reports whether a folder is trusted according to the user's folder trust state. + /// + /// Wire method: `session.permissions.folderTrust.isTrusted`. + /// + /// # Parameters + /// + /// * `params` - Folder path to check for trust. + /// + /// # Returns + /// + /// Folder trust check result. + /// + ///
+ /// + /// **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 is_trusted( + &self, + params: FolderTrustCheckParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_FOLDERTRUST_ISTRUSTED, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Adds a folder to the user's trusted folders list. + /// + /// Wire method: `session.permissions.folderTrust.addTrusted`. + /// + /// # Parameters + /// + /// * `params` - Folder path to add to trusted folders. + /// + /// # Returns + /// + /// Indicates whether the operation succeeded. + /// + ///
+ /// + /// **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 add_trusted( + &self, + params: FolderTrustAddParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_FOLDERTRUST_ADDTRUSTED, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.permissions.locations.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcPermissionsLocations<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcPermissionsLocations<'a> { + /// Resolves the permission location key and type for a working directory. + /// + /// Wire method: `session.permissions.locations.resolve`. + /// + /// # Parameters + /// + /// * `params` - Working directory to resolve into a location-permissions key. + /// + /// # Returns + /// + /// Resolved location-permissions key and type. + /// + ///
+ /// + /// **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 resolve( + &self, + params: PermissionLocationResolveParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_LOCATIONS_RESOLVE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Applies persisted location-scoped tool approvals and allowed directories for a working directory to this session's permission service. + /// + /// Wire method: `session.permissions.locations.apply`. + /// + /// # Parameters + /// + /// * `params` - Working directory to load persisted location permissions for. + /// + /// # Returns + /// + /// Summary of persisted location permissions applied to the session. + /// + ///
+ /// + /// **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 apply( + &self, + params: PermissionLocationApplyParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_LOCATIONS_APPLY, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Persists a tool approval for a permission location and applies its rules to this session's live permission service. + /// + /// Wire method: `session.permissions.locations.addToolApproval`. + /// + /// # Parameters + /// + /// * `params` - Location-scoped tool approval to persist. + /// + /// # Returns + /// + /// Indicates whether the operation succeeded. + /// + ///
+ /// + /// **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 add_tool_approval( + &self, + params: PermissionLocationAddToolApprovalParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_LOCATIONS_ADDTOOLAPPROVAL, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Updates the session's reasoning effort without changing the selected model. +/// `session.permissions.paths.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcPermissionsPaths<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcPermissionsPaths<'a> { + /// Returns the session's allowed directories and primary working directory. /// - /// Wire method: `session.model.setReasoningEffort`. + /// Wire method: `session.permissions.paths.list`. + /// + /// # Returns + /// + /// Snapshot of the session's allow-listed directories and primary working directory. + /// + ///
+ /// + /// **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 list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_PATHS_LIST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Adds a directory to the session's allow-list and activates conventional skill and agent definitions under it. + /// + /// Wire method: `session.permissions.paths.add`. /// /// # Parameters /// - /// * `params` - Reasoning effort level to apply to the currently selected model. + /// * `params` - Directory path to add to the session's allowed directories. /// /// # Returns /// - /// Update the session's reasoning effort without changing the selected model. Use `switchTo` instead when you also need to change the model. The runtime stores the effort on the session and applies it to subsequent turns. + /// Indicates whether the operation succeeded. /// ///
/// @@ -7802,30 +9183,34 @@ impl<'a> SessionRpcModel<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_reasoning_effort( + pub async fn add( &self, - params: ModelSetReasoningEffortRequest, - ) -> Result { + params: PermissionPathsAddParams, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_MODEL_SETREASONINGEFFORT, + rpc_methods::SESSION_PERMISSIONS_PATHS_ADD, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. + /// Updates the session's primary working directory used by the permission policy. /// - /// Wire method: `session.model.list`. + /// Wire method: `session.permissions.paths.updatePrimary`. + /// + /// # Parameters + /// + /// * `params` - Directory path to set as the session's new primary working directory. /// /// # Returns /// - /// The list of models available to this session. + /// Indicates whether the operation succeeded. /// ///
/// @@ -7834,27 +9219,34 @@ impl<'a> SessionRpcModel<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn update_primary( + &self, + params: PermissionPathsUpdatePrimaryParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MODEL_LIST, Some(wire_params)) + .call( + rpc_methods::SESSION_PERMISSIONS_PATHS_UPDATEPRIMARY, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. + /// Reports whether a path falls within any of the session's allowed directories. /// - /// Wire method: `session.model.list`. + /// Wire method: `session.permissions.paths.isPathWithinAllowedDirectories`. /// /// # Parameters /// - /// * `params` - Optional listing options. + /// * `params` - Path to evaluate against the session's allowed directories. /// /// # Returns /// - /// The list of models available to this session. + /// Indicates whether the supplied path is within the session's allowed directories. /// ///
/// @@ -7863,35 +9255,78 @@ impl<'a> SessionRpcModel<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list_with_params( + pub async fn is_path_within_allowed_directories( &self, - params: ModelListRequest, - ) -> Result { + params: PermissionPathsAllowedCheckParams, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_MODEL_LIST, Some(wire_params)) + .call( + rpc_methods::SESSION_PERMISSIONS_PATHS_ISPATHWITHINALLOWEDDIRECTORIES, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Reports whether a path falls within the session's workspace (primary) directory. + /// + /// Wire method: `session.permissions.paths.isPathWithinWorkspace`. + /// + /// # Parameters + /// + /// * `params` - Path to evaluate against the session's workspace (primary) directory. + /// + /// # Returns + /// + /// Indicates whether the supplied path is within the session's workspace directory. + /// + ///
+ /// + /// **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 is_path_within_workspace( + &self, + params: PermissionPathsWorkspaceCheckParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_PATHS_ISPATHWITHINWORKSPACE, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } } -/// `session.name.*` RPCs. +/// `session.permissions.urls.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcName<'a> { +pub struct SessionRpcPermissionsUrls<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcName<'a> { - /// Gets the session's friendly name. +impl<'a> SessionRpcPermissionsUrls<'a> { + /// Toggles the runtime's URL-permission policy between unrestricted and restricted modes. /// - /// Wire method: `session.name.get`. + /// Wire method: `session.permissions.urls.setUnrestrictedMode`. + /// + /// # Parameters + /// + /// * `params` - Whether the URL-permission policy should run in unrestricted mode. /// /// # Returns /// - /// The session's friendly name, or null when not yet set. + /// Indicates whether the operation succeeded. /// ///
/// @@ -7900,23 +9335,110 @@ impl<'a> SessionRpcName<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get(&self) -> Result { + pub async fn set_unrestricted_mode( + &self, + params: PermissionUrlsSetUnrestrictedModeParams, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PERMISSIONS_URLS_SETUNRESTRICTEDMODE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.plan.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcPlan<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcPlan<'a> { + /// Reads the session plan file from the workspace. + /// + /// Wire method: `session.plan.read`. + /// + /// # Returns + /// + /// Existence, contents, and resolved path of the session plan file. + /// + ///
+ /// + /// **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 read(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_NAME_GET, Some(wire_params)) + .call(rpc_methods::SESSION_PLAN_READ, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Sets the session's friendly name. + /// Writes new content to the session plan file. /// - /// Wire method: `session.name.set`. + /// Wire method: `session.plan.update`. + /// + /// # Parameters + /// + /// * `params` - Replacement contents to write to the session plan file. + /// + ///
+ /// + /// **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 update(&self, params: PlanUpdateRequest) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PLAN_UPDATE, Some(wire_params)) + .await?; + Ok(()) + } + + /// Deletes the session plan file from the workspace. + /// + /// Wire method: `session.plan.delete`. + /// + ///
+ /// + /// **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 delete(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PLAN_DELETE, Some(wire_params)) + .await?; + Ok(()) + } + + /// Reads todo rows from the session SQL database for plan rendering. + /// + /// Wire method: `session.plan.readSqlTodos`. /// - /// # Parameters + /// # Returns /// - /// * `params` - New friendly name to apply to the session. + /// Todo rows read from the session SQL database. Empty when no session database is available. /// ///
/// @@ -7925,28 +9447,23 @@ impl<'a> SessionRpcName<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set(&self, params: NameSetRequest) -> Result<(), Error> { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn read_sql_todos(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_NAME_SET, Some(wire_params)) + .call(rpc_methods::SESSION_PLAN_READSQLTODOS, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Persists an auto-generated session summary as the session's name when no user-set name exists. - /// - /// Wire method: `session.name.setAuto`. - /// - /// # Parameters + /// Reads todo rows AND dependency edges from the session SQL database for structured progress UI. Same defensive behavior as readSqlTodos — returns empty arrays when the database, tables, or columns aren't available. Clients should call this on session start and after every `session.todos_changed` event to refresh structured-UI rendering. /// - /// * `params` - Auto-generated session summary to apply as the session's name when no user-set name exists. + /// Wire method: `session.plan.readSqlTodosWithDependencies`. /// /// # Returns /// - /// Indicates whether the auto-generated summary was applied as the session's name. + /// Todo rows + dependency edges read from the session SQL database. /// ///
/// @@ -7955,36 +9472,43 @@ impl<'a> SessionRpcName<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_auto(&self, params: NameSetAutoRequest) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn read_sql_todos_with_dependencies( + &self, + ) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_NAME_SETAUTO, Some(wire_params)) + .call( + rpc_methods::SESSION_PLAN_READSQLTODOSWITHDEPENDENCIES, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } } -/// `session.options.*` RPCs. +/// `session.plugins.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcOptions<'a> { +pub struct SessionRpcPlugins<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcOptions<'a> { - /// Patches the genuinely-mutable subset of session options. - /// - /// Wire method: `session.options.update`. - /// - /// # Parameters +impl<'a> SessionRpcPlugins<'a> { + /// `session.plugins.marketplaces.*` sub-namespace. + pub fn marketplaces(&self) -> SessionRpcPluginsMarketplaces<'a> { + SessionRpcPluginsMarketplaces { + session: self.session, + } + } + + /// Lists globally installed, live, built-in, and enterprise-managed desired plugins using the live session's authoritative account, working directory, and retained managed policy. /// - /// * `params` - Patch of mutable session options to apply to the running session. + /// Wire method: `session.plugins.list`. /// /// # Returns /// - /// Indicates whether the session options patch was applied successfully. + /// Plugins installed for the session, with their enabled state and version metadata. /// ///
/// @@ -7993,67 +9517,27 @@ impl<'a> SessionRpcOptions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn update( - &self, - params: SessionUpdateOptionsParams, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_OPTIONS_UPDATE, Some(wire_params)) + .call(rpc_methods::SESSION_PLUGINS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.permissions.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcPermissions<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcPermissions<'a> { - /// `session.permissions.folderTrust.*` sub-namespace. - pub fn folder_trust(&self) -> SessionRpcPermissionsFolderTrust<'a> { - SessionRpcPermissionsFolderTrust { - session: self.session, - } - } - - /// `session.permissions.locations.*` sub-namespace. - pub fn locations(&self) -> SessionRpcPermissionsLocations<'a> { - SessionRpcPermissionsLocations { - session: self.session, - } - } - - /// `session.permissions.paths.*` sub-namespace. - pub fn paths(&self) -> SessionRpcPermissionsPaths<'a> { - SessionRpcPermissionsPaths { - session: self.session, - } - } - - /// `session.permissions.urls.*` sub-namespace. - pub fn urls(&self) -> SessionRpcPermissionsUrls<'a> { - SessionRpcPermissionsUrls { - session: self.session, - } - } - /// Replaces selected permission policy fields (rules, paths, URLs, exclusions, allow-all flags) on the session. + /// Installs a plugin using the live session's authoritative account, working directory, and retained managed policy. /// - /// Wire method: `session.permissions.configure`. + /// Wire method: `session.plugins.install`. /// /// # Parameters /// - /// * `params` - Patch of permission policy fields to apply (omit a field to leave it unchanged). + /// * `params` - Plugin source resolved relative to the session's authoritative working directory. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Result of installing a plugin. /// ///
/// @@ -8062,34 +9546,27 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn configure( + pub async fn install( &self, - params: PermissionsConfigureParams, - ) -> Result { + params: SessionPluginsInstallRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_CONFIGURE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PLUGINS_INSTALL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Provides a decision for a pending tool permission request. + /// Uninstalls a plugin when permitted by the live session's retained managed policy. /// - /// Wire method: `session.permissions.handlePendingPermissionRequest`. + /// Wire method: `session.plugins.uninstall`. /// /// # Parameters /// - /// * `params` - Pending permission request ID and the decision to apply (approve/reject and scope). - /// - /// # Returns - /// - /// Indicates whether the permission decision was applied; false when the request was already resolved. + /// * `params` - Name (or spec) of the plugin to uninstall. /// ///
/// @@ -8098,30 +9575,28 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_permission_request( - &self, - params: PermissionDecisionRequest, - ) -> Result { + pub async fn uninstall(&self, params: PluginsUninstallRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PLUGINS_UNINSTALL, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Reconstructs the set of pending tool permission requests from the session's event history. + /// Updates an installed plugin using the live session's authoritative account, working directory, and retained managed policy. /// - /// Wire method: `session.permissions.pendingRequests`. + /// Wire method: `session.plugins.update`. + /// + /// # Parameters + /// + /// * `params` - Name (or spec) of the plugin to update. /// /// # Returns /// - /// List of pending permission requests reconstructed from event history. + /// Result of updating a single plugin. /// ///
/// @@ -8130,30 +9605,24 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn pending_requests(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn update(&self, params: PluginsUpdateRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_PENDINGREQUESTS, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PLUGINS_UPDATE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Enables or disables automatic approval of tool permission requests for the session. + /// Enables installed plugins when permitted by the live session's retained managed policy. /// - /// Wire method: `session.permissions.setApproveAll`. + /// Wire method: `session.plugins.enable`. /// /// # Parameters /// - /// * `params` - Allow-all toggle for tool permission requests, with an optional telemetry source. - /// - /// # Returns - /// - /// Indicates whether the operation succeeded. + /// * `params` - Plugin names (or specs) to enable in the session's authoritative working directory. /// ///
/// @@ -8162,34 +9631,24 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_approve_all( - &self, - params: PermissionsSetApproveAllRequest, - ) -> Result { + pub async fn enable(&self, params: SessionPluginsEnableRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_SETAPPROVEALL, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PLUGINS_ENABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Sets the permission mode for the session. `manual` follows the normal approval flow, `assisted` attaches LLM safety recommendations, and `allow-all` automatically approves permission requests. The result returns the authoritative post-mutation mode so callers can update local state without racing the `session.permissions_changed` notification. + /// Disables installed plugins when permitted by the live session's retained managed policy. /// - /// Wire method: `session.permissions.setMode`. + /// Wire method: `session.plugins.disable`. /// /// # Parameters /// - /// * `params` - Permission mode to apply for the session. - /// - /// # Returns - /// - /// Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. + /// * `params` - Plugin names (or specs) to disable in the session's authoritative working directory. /// ///
/// @@ -8198,27 +9657,20 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_mode( - &self, - params: PermissionsSetModeRequest, - ) -> Result { + pub async fn disable(&self, params: SessionPluginsDisableRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_PERMISSIONS_SETMODE, Some(wire_params)) + .call(rpc_methods::SESSION_PLUGINS_DISABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Returns the current permission mode for the session. - /// - /// Wire method: `session.permissions.getMode`. - /// - /// # Returns + /// Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. /// - /// Current permission mode. + /// Wire method: `session.plugins.reload`. /// ///
/// @@ -8227,27 +9679,57 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_mode(&self) -> Result { + pub async fn reload(&self) -> Result<(), Error> { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_PERMISSIONS_GETMODE, Some(wire_params)) + .call(rpc_methods::SESSION_PLUGINS_RELOAD, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Adds or removes session-scoped or location-scoped permission rules. + /// Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. /// - /// Wire method: `session.permissions.modifyRules`. + /// Wire method: `session.plugins.reload`. /// /// # Parameters /// - /// * `params` - Scope and add/remove instructions for modifying session- or location-scoped permission rules. + /// * `params` - Optional flags controlling which side effects the reload performs. + /// + ///
+ /// + /// **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 reload_with_params(&self, params: PluginsReloadRequest) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_PLUGINS_RELOAD, Some(wire_params)) + .await?; + Ok(()) + } +} + +/// `session.plugins.marketplaces.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcPluginsMarketplaces<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcPluginsMarketplaces<'a> { + /// Lists registered and enterprise-managed desired marketplaces using the live session's retained policy. + /// + /// Wire method: `session.plugins.marketplaces.list`. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// All registered marketplaces, including built-in defaults. /// ///
/// @@ -8256,34 +9738,30 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn modify_rules( - &self, - params: PermissionsModifyRulesParams, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_PERMISSIONS_MODIFYRULES, + rpc_methods::SESSION_PLUGINS_MARKETPLACES_LIST, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Sets whether the client wants permission prompts bridged into session events. + /// Adds a marketplace when permitted by the live session's retained managed policy. /// - /// Wire method: `session.permissions.setRequired`. + /// Wire method: `session.plugins.marketplaces.add`. /// /// # Parameters /// - /// * `params` - Toggles whether permission prompts should be bridged into session events for this client. + /// * `params` - Marketplace source and optional working directory for relative-path resolution. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Result of registering a new marketplace. /// ///
/// @@ -8292,34 +9770,34 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_required( + pub async fn add( &self, - params: PermissionsSetRequiredRequest, - ) -> Result { + params: PluginsMarketplacesAddRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_PERMISSIONS_SETREQUIRED, + rpc_methods::SESSION_PLUGINS_MARKETPLACES_ADD, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Clears session-scoped tool permission approvals. + /// Removes a marketplace when permitted by the live session's retained managed policy. /// - /// Wire method: `session.permissions.resetSessionApprovals`. + /// Wire method: `session.plugins.marketplaces.remove`. /// /// # Parameters /// - /// * `params` - Clears session-scoped tool permission approvals, and optionally the location-scoped ones. + /// * `params` - Name of the marketplace to remove and an optional force flag. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Outcome of the remove attempt, including dependent-plugin info when applicable. /// ///
/// @@ -8328,34 +9806,34 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn reset_session_approvals( + pub async fn remove( &self, - params: PermissionsResetSessionApprovalsRequest, - ) -> Result { + params: PluginsMarketplacesRemoveRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_PERMISSIONS_RESETSESSIONAPPROVALS, + rpc_methods::SESSION_PLUGINS_MARKETPLACES_REMOVE, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Notifies the runtime that a permission prompt UI has been shown to the user. + /// Browses a marketplace resolved through the live session's working directory and retained managed policy. /// - /// Wire method: `session.permissions.notifyPromptShown`. + /// Wire method: `session.plugins.marketplaces.browse`. /// /// # Parameters /// - /// * `params` - Notification payload describing the permission prompt that the client just rendered. + /// * `params` - Name of the marketplace whose plugin catalog to fetch. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Plugins advertised by the marketplace. /// ///
/// @@ -8364,42 +9842,30 @@ impl<'a> SessionRpcPermissions<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn notify_prompt_shown( + pub async fn browse( &self, - params: PermissionPromptShownNotification, - ) -> Result { + params: PluginsMarketplacesBrowseRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_PERMISSIONS_NOTIFYPROMPTSHOWN, + rpc_methods::SESSION_PLUGINS_MARKETPLACES_BROWSE, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.permissions.folderTrust.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcPermissionsFolderTrust<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcPermissionsFolderTrust<'a> { - /// Reports whether a folder is trusted according to the user's folder trust state. - /// - /// Wire method: `session.permissions.folderTrust.isTrusted`. - /// - /// # Parameters + /// Refreshes marketplaces resolved through the live session's working directory and retained managed policy. /// - /// * `params` - Folder path to check for trust. + /// Wire method: `session.plugins.marketplaces.refresh`. /// /// # Returns /// - /// Folder trust check result. + /// Result of refreshing one or more marketplace catalogs. /// ///
/// @@ -8408,34 +9874,30 @@ impl<'a> SessionRpcPermissionsFolderTrust<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn is_trusted( - &self, - params: FolderTrustCheckParams, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn refresh(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_PERMISSIONS_FOLDERTRUST_ISTRUSTED, + rpc_methods::SESSION_PLUGINS_MARKETPLACES_REFRESH, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Adds a folder to the user's trusted folders list. + /// Refreshes marketplaces resolved through the live session's working directory and retained managed policy. /// - /// Wire method: `session.permissions.folderTrust.addTrusted`. + /// Wire method: `session.plugins.marketplaces.refresh`. /// /// # Parameters /// - /// * `params` - Folder path to add to trusted folders. + /// * `params` - Optional marketplace name; omit to refresh all. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Result of refreshing one or more marketplace catalogs. /// ///
/// @@ -8444,17 +9906,17 @@ impl<'a> SessionRpcPermissionsFolderTrust<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn add_trusted( + pub async fn refresh_with_params( &self, - params: FolderTrustAddParams, - ) -> Result { + params: PluginsMarketplacesRefreshRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_PERMISSIONS_FOLDERTRUST_ADDTRUSTED, + rpc_methods::SESSION_PLUGINS_MARKETPLACES_REFRESH, Some(wire_params), ) .await?; @@ -8462,24 +9924,20 @@ impl<'a> SessionRpcPermissionsFolderTrust<'a> { } } -/// `session.permissions.locations.*` RPCs. +/// `session.provider.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcPermissionsLocations<'a> { +pub struct SessionRpcProvider<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcPermissionsLocations<'a> { - /// Resolves the permission location key and type for a working directory. - /// - /// Wire method: `session.permissions.locations.resolve`. - /// - /// # Parameters +impl<'a> SessionRpcProvider<'a> { + /// Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. /// - /// * `params` - Working directory to resolve into a location-permissions key. + /// Wire method: `session.provider.getEndpoint`. /// /// # Returns /// - /// Resolved location-permissions key and type. + /// A snapshot of the provider endpoint the session is currently configured to talk to. /// ///
/// @@ -8488,34 +9946,27 @@ impl<'a> SessionRpcPermissionsLocations<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn resolve( - &self, - params: PermissionLocationResolveParams, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn get_endpoint(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_LOCATIONS_RESOLVE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_GETENDPOINT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Applies persisted location-scoped tool approvals and allowed directories for a working directory to this session's permission service. + /// Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. /// - /// Wire method: `session.permissions.locations.apply`. + /// Wire method: `session.provider.getEndpoint`. /// /// # Parameters /// - /// * `params` - Working directory to load persisted location permissions for. + /// * `params` - Optional model identifier to scope the endpoint snapshot to. /// /// # Returns /// - /// Summary of persisted location permissions applied to the session. + /// A snapshot of the provider endpoint the session is currently configured to talk to. /// ///
/// @@ -8524,34 +9975,31 @@ impl<'a> SessionRpcPermissionsLocations<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn apply( + pub async fn get_endpoint_with_params( &self, - params: PermissionLocationApplyParams, - ) -> Result { + params: ProviderGetEndpointRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_LOCATIONS_APPLY, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_GETENDPOINT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Persists a tool approval for a permission location and applies its rules to this session's live permission service. + /// Adds BYOK providers and/or models to the session's registry at runtime, extending the additive registry built from the session's `providers`/`models` options. Both fields are optional, so a call may add providers only, models only, or both. Within a single call providers are registered before models, so a model may reference a provider added in the same call; across calls a model may reference any provider already registered (from session creation or a prior add). A model whose referenced provider is not registered by the end of the call is rejected. Newly added models become selectable via `model.list` / `model.switchTo` and are inherited by sub-agents spawned afterwards. /// - /// Wire method: `session.permissions.locations.addToolApproval`. + /// Wire method: `session.provider.add`. /// /// # Parameters /// - /// * `params` - Location-scoped tool approval to persist. + /// * `params` - BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// The selectable model entries synthesized for the models added by this call. /// ///
/// @@ -8560,38 +10008,32 @@ impl<'a> SessionRpcPermissionsLocations<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn add_tool_approval( - &self, - params: PermissionLocationAddToolApprovalParams, - ) -> Result { + pub async fn add(&self, params: ProviderAddRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_LOCATIONS_ADDTOOLAPPROVAL, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_ADD, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `session.permissions.paths.*` RPCs. +/// `session.queue.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcPermissionsPaths<'a> { +pub struct SessionRpcQueue<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcPermissionsPaths<'a> { - /// Returns the session's allowed directories and primary working directory. +impl<'a> SessionRpcQueue<'a> { + /// Returns the local session's pending user-facing queued items and steering messages. /// - /// Wire method: `session.permissions.paths.list`. + /// Wire method: `session.queue.pendingItems`. /// /// # Returns /// - /// Snapshot of the session's allow-listed directories and primary working directory. + /// Snapshot of the session's pending queued items and immediate-steering messages. /// ///
/// @@ -8600,30 +10042,52 @@ impl<'a> SessionRpcPermissionsPaths<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list(&self) -> Result { + pub async fn pending_items(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_PATHS_LIST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_QUEUE_PENDINGITEMS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Adds a directory to the session's allow-list and activates conventional skill and agent definitions under it. + /// Returns the internal native queue snapshot for in-process session orchestration. /// - /// Wire method: `session.permissions.paths.add`. + /// Wire method: `session.queue.snapshot`. + /// + /// # Returns + /// + /// Internal snapshot of native queue state for local session orchestration. + /// + ///
+ /// + /// **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(crate) async fn snapshot(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_QUEUE_SNAPSHOT, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Moves an addressable queued item to a public visible position. + /// + /// Wire method: `session.queue.moveItem`. /// /// # Parameters /// - /// * `params` - Directory path to add to the session's allowed directories. + /// * `params` - Parameters for moving a queued item by stable id. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Result of moving a queued item. /// ///
/// @@ -8632,34 +10096,31 @@ impl<'a> SessionRpcPermissionsPaths<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn add( + pub async fn move_item( &self, - params: PermissionPathsAddParams, - ) -> Result { + params: QueueMoveItemRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_PATHS_ADD, - Some(wire_params), - ) + .call(rpc_methods::SESSION_QUEUE_MOVEITEM, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Updates the session's primary working directory used by the permission policy. + /// Inserts a new queued message at a public visible position. /// - /// Wire method: `session.permissions.paths.updatePrimary`. + /// Wire method: `session.queue.insertAt`. /// /// # Parameters /// - /// * `params` - Directory path to set as the session's new primary working directory. + /// * `params` - Parameters for inserting a queued message at a public visible position. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Result of inserting a queued message. /// ///
/// @@ -8668,34 +10129,31 @@ impl<'a> SessionRpcPermissionsPaths<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn update_primary( + pub async fn insert_at( &self, - params: PermissionPathsUpdatePrimaryParams, - ) -> Result { + params: QueueInsertAtRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_PATHS_UPDATEPRIMARY, - Some(wire_params), - ) + .call(rpc_methods::SESSION_QUEUE_INSERTAT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Reports whether a path falls within any of the session's allowed directories. + /// Removes an addressable queued item by its stable id. /// - /// Wire method: `session.permissions.paths.isPathWithinAllowedDirectories`. + /// Wire method: `session.queue.removeAt`. /// /// # Parameters /// - /// * `params` - Path to evaluate against the session's allowed directories. + /// * `params` - Parameters for removing a queued item by stable id. /// /// # Returns /// - /// Indicates whether the supplied path is within the session's allowed directories. + /// Result of removing a queued item. /// ///
/// @@ -8704,34 +10162,31 @@ impl<'a> SessionRpcPermissionsPaths<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn is_path_within_allowed_directories( + pub async fn remove_at( &self, - params: PermissionPathsAllowedCheckParams, - ) -> Result { + params: QueueRemoveAtRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_PATHS_ISPATHWITHINALLOWEDDIRECTORIES, - Some(wire_params), - ) + .call(rpc_methods::SESSION_QUEUE_REMOVEAT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Reports whether a path falls within the session's workspace (primary) directory. + /// Updates the text of an addressable single-message queue item. /// - /// Wire method: `session.permissions.paths.isPathWithinWorkspace`. + /// Wire method: `session.queue.updateText`. /// /// # Parameters /// - /// * `params` - Path to evaluate against the session's workspace (primary) directory. + /// * `params` - Parameters for editing a single queued message. /// /// # Returns /// - /// Indicates whether the supplied path is within the session's workspace directory. + /// Result of editing a queued message. /// ///
/// @@ -8740,42 +10195,31 @@ impl<'a> SessionRpcPermissionsPaths<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn is_path_within_workspace( + pub async fn update_text( &self, - params: PermissionPathsWorkspaceCheckParams, - ) -> Result { + params: QueueUpdateTextRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_PERMISSIONS_PATHS_ISPATHWITHINWORKSPACE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_QUEUE_UPDATETEXT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.permissions.urls.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcPermissionsUrls<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcPermissionsUrls<'a> { - /// Toggles the runtime's URL-permission policy between unrestricted and restricted modes. + /// Atomically withdraws an unchanged, unconsumed user message from the local queued or steering lane. A client retaining the original draft may restore it only when removed is true. Does not interrupt the running turn. /// - /// Wire method: `session.permissions.urls.setUnrestrictedMode`. + /// Wire method: `session.queue.withdrawMessage`. /// /// # Parameters /// - /// * `params` - Whether the URL-permission policy should run in unrestricted mode. + /// * `params` - Conditional withdrawal of a single user message, before the runtime claims it for delivery. /// /// # Returns /// - /// Indicates whether the operation succeeded. + /// Result of removing a queued item. /// ///
/// @@ -8784,38 +10228,34 @@ impl<'a> SessionRpcPermissionsUrls<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_unrestricted_mode( + pub async fn withdraw_message( &self, - params: PermissionUrlsSetUnrestrictedModeParams, - ) -> Result { + params: QueueWithdrawMessageRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_PERMISSIONS_URLS_SETUNRESTRICTEDMODE, + rpc_methods::SESSION_QUEUE_WITHDRAWMESSAGE, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.plan.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcPlan<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcPlan<'a> { - /// Reads the session plan file from the workspace. + /// Atomically appends text and attachments to an unchanged, unconsumed local steering message. Returns updated=false if delivery or withdrawal already claimed the message. /// - /// Wire method: `session.plan.read`. + /// Wire method: `session.queue.appendSteering`. + /// + /// # Parameters + /// + /// * `params` - Append to one pending steering message without changing its identity or delivery position. /// /// # Returns /// - /// Existence, contents, and resolved path of the session plan file. + /// Result of editing a queued message. /// ///
/// @@ -8824,23 +10264,31 @@ impl<'a> SessionRpcPlan<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn read(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn append_steering( + &self, + params: QueueAppendSteeringRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_PLAN_READ, Some(wire_params)) + .call(rpc_methods::SESSION_QUEUE_APPENDSTEERING, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Writes new content to the session plan file. + /// Duplicates an addressable queued item immediately after its source. /// - /// Wire method: `session.plan.update`. + /// Wire method: `session.queue.duplicateAt`. /// /// # Parameters /// - /// * `params` - Replacement contents to write to the session plan file. + /// * `params` - Parameters for duplicating a queued item. + /// + /// # Returns + /// + /// Result of duplicating a queued item. /// ///
/// @@ -8849,20 +10297,27 @@ impl<'a> SessionRpcPlan<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn update(&self, params: PlanUpdateRequest) -> Result<(), Error> { + pub async fn duplicate_at( + &self, + params: QueueDuplicateAtRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_PLAN_UPDATE, Some(wire_params)) + .call(rpc_methods::SESSION_QUEUE_DUPLICATEAT, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Deletes the session plan file from the workspace. + /// Acquires or releases the queued-lane drain pause. /// - /// Wire method: `session.plan.delete`. + /// Wire method: `session.queue.setDrainPaused`. + /// + /// # Parameters + /// + /// * `params` - Parameters for acquiring or releasing the queued-lane drain pause. Acquisition is exclusive and non-idempotent: `paused: true` against an already-paused session fails with `queue_already_paused`. The pause is never released automatically — it is not tied to the caller's lifetime, so a client that exits without sending `paused: false` leaves the lane frozen. Release is unowned: `paused: false` clears the pause for any caller, including one that never acquired it. /// ///
/// @@ -8871,23 +10326,28 @@ impl<'a> SessionRpcPlan<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn delete(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn set_drain_paused(&self, params: QueueSetDrainPausedRequest) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_PLAN_DELETE, Some(wire_params)) + .call(rpc_methods::SESSION_QUEUE_SETDRAINPAUSED, Some(wire_params)) .await?; Ok(()) } - /// Reads todo rows from the session SQL database for plan rendering. + /// Moves an addressable queued message into the live turn's steering lane. /// - /// Wire method: `session.plan.readSqlTodos`. + /// Wire method: `session.queue.sendNow`. + /// + /// # Parameters + /// + /// * `params` - Parameters for steering a queued message into a live turn. /// /// # Returns /// - /// Todo rows read from the session SQL database. Empty when no session database is available. + /// Result of trying to steer a queued message into a live turn. /// ///
/// @@ -8896,23 +10356,24 @@ impl<'a> SessionRpcPlan<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn read_sql_todos(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn send_now(&self, params: QueueSendNowRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_PLAN_READSQLTODOS, Some(wire_params)) + .call(rpc_methods::SESSION_QUEUE_SENDNOW, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Reads todo rows AND dependency edges from the session SQL database for structured progress UI. Same defensive behavior as readSqlTodos — returns empty arrays when the database, tables, or columns aren't available. Clients should call this on session start and after every `session.todos_changed` event to refresh structured-UI rendering. + /// Reports whether the local session has native queued work pending. /// - /// Wire method: `session.plan.readSqlTodosWithDependencies`. + /// Wire method: `session.queue.hasPending`. /// /// # Returns /// - /// Todo rows + dependency edges read from the session SQL database. + /// Whether the native queue has pending work. /// ///
/// @@ -8921,36 +10382,27 @@ impl<'a> SessionRpcPlan<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn read_sql_todos_with_dependencies( - &self, - ) -> Result { + pub(crate) async fn has_pending(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_PLAN_READSQLTODOSWITHDEPENDENCIES, - Some(wire_params), - ) + .call(rpc_methods::SESSION_QUEUE_HASPENDING, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.plugins.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcPlugins<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcPlugins<'a> { - /// Lists plugins installed for the session. + /// Begins a native deferred-idle drain when background work has quiesced. /// - /// Wire method: `session.plugins.list`. + /// Wire method: `session.queue.beginDeferredIdleDrain`. + /// + /// # Parameters + /// + /// * `params` - Inputs for starting a deferred-idle drain. /// /// # Returns /// - /// Plugins installed for the session, with their enabled state and version metadata. + /// Whether a deferred-idle drain should run. /// ///
/// @@ -8959,19 +10411,34 @@ impl<'a> SessionRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub(crate) async fn begin_deferred_idle_drain( + &self, + params: QueueBeginDeferredIdleDrainRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_PLUGINS_LIST, Some(wire_params)) + .call( + rpc_methods::SESSION_QUEUE_BEGINDEFERREDIDLEDRAIN, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. + /// Finishes a native deferred-idle drain and reports whether to drain queue work or emit idle. /// - /// Wire method: `session.plugins.reload`. + /// Wire method: `session.queue.finishDeferredIdleDrain`. + /// + /// # Parameters + /// + /// * `params` - Inputs for completing a deferred-idle drain. + /// + /// # Returns + /// + /// Action selected by the native deferred-idle drain. /// ///
/// @@ -8980,23 +10447,30 @@ impl<'a> SessionRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn reload(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub(crate) async fn finish_deferred_idle_drain( + &self, + params: QueueFinishDeferredIdleDrainRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_PLUGINS_RELOAD, Some(wire_params)) + .call( + rpc_methods::SESSION_QUEUE_FINISHDEFERREDIDLEDRAIN, + Some(wire_params), + ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. + /// Marks session.idle as deferred by native background work state. /// - /// Wire method: `session.plugins.reload`. + /// Wire method: `session.queue.deferSessionIdle`. /// /// # Parameters /// - /// * `params` - Optional flags controlling which side effects the reload performs. + /// * `params` - Inputs for marking session.idle deferred in native state. /// ///
/// @@ -9005,32 +10479,30 @@ impl<'a> SessionRpcPlugins<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn reload_with_params(&self, params: PluginsReloadRequest) -> Result<(), Error> { + pub(crate) async fn defer_session_idle( + &self, + params: QueueDeferSessionIdleRequest, + ) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_PLUGINS_RELOAD, Some(wire_params)) + .call( + rpc_methods::SESSION_QUEUE_DEFERSESSIONIDLE, + Some(wire_params), + ) .await?; Ok(()) } -} -/// `session.provider.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcProvider<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcProvider<'a> { - /// Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. + /// Removes the most recently queued user-facing item (LIFO). /// - /// Wire method: `session.provider.getEndpoint`. + /// Wire method: `session.queue.removeMostRecent`. /// /// # Returns /// - /// A snapshot of the provider endpoint the session is currently configured to talk to. + /// Indicates whether a user-facing pending item was removed. /// ///
/// @@ -9039,27 +10511,22 @@ impl<'a> SessionRpcProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_endpoint(&self) -> Result { + pub async fn remove_most_recent(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_PROVIDER_GETENDPOINT, Some(wire_params)) + .call( + rpc_methods::SESSION_QUEUE_REMOVEMOSTRECENT, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. - /// - /// Wire method: `session.provider.getEndpoint`. - /// - /// # Parameters - /// - /// * `params` - Optional model identifier to scope the endpoint snapshot to. - /// - /// # Returns + /// Clears all pending queued items on the local session. /// - /// A snapshot of the provider endpoint the session is currently configured to talk to. + /// Wire method: `session.queue.clear`. /// ///
/// @@ -9068,65 +10535,27 @@ impl<'a> SessionRpcProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_endpoint_with_params( - &self, - params: ProviderGetEndpointRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn clear(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_PROVIDER_GETENDPOINT, Some(wire_params)) + .call(rpc_methods::SESSION_QUEUE_CLEAR, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Adds BYOK providers and/or models to the session's registry at runtime, extending the additive registry built from the session's `providers`/`models` options. Both fields are optional, so a call may add providers only, models only, or both. Within a single call providers are registered before models, so a model may reference a provider added in the same call; across calls a model may reference any provider already registered (from session creation or a prior add). A model whose referenced provider is not registered by the end of the call is rejected. Newly added models become selectable via `model.list` / `model.switchTo` and are inherited by sub-agents spawned afterwards. + /// Consumes queued native system notifications matching an internal filter. /// - /// Wire method: `session.provider.add`. + /// Wire method: `session.queue.consumeSystemNotifications`. /// /// # Parameters /// - /// * `params` - BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. - /// - /// # Returns - /// - /// The selectable model entries synthesized for the models added by this call. - /// - ///
- /// - /// **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 add(&self, params: ProviderAddRequest) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); - let _value = self - .session - .client() - .call(rpc_methods::SESSION_PROVIDER_ADD, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) - } -} - -/// `session.queue.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcQueue<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcQueue<'a> { - /// Returns the local session's pending user-facing queued items and steering messages. - /// - /// Wire method: `session.queue.pendingItems`. + /// * `params` - Internal filter for consuming queued system notifications. /// /// # Returns /// - /// Snapshot of the session's pending queued items and immediate-steering messages. + /// Indicates whether a user-facing pending item was removed. /// ///
/// @@ -9135,23 +10564,30 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn pending_items(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub(crate) async fn consume_system_notifications( + &self, + params: QueueConsumeSystemNotificationsRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_PENDINGITEMS, Some(wire_params)) + .call( + rpc_methods::SESSION_QUEUE_CONSUMESYSTEMNOTIFICATIONS, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the internal native queue snapshot for in-process session orchestration. + /// Enqueues the internal resume-pending wake item when orphan handling needs a follow-up turn. /// - /// Wire method: `session.queue.snapshot`. + /// Wire method: `session.queue.enqueueResumePending`. /// /// # Returns /// - /// Internal snapshot of native queue state for local session orchestration. + /// Result of enqueueing the resume-pending wake item. /// ///
/// @@ -9160,27 +10596,24 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn snapshot(&self) -> Result { + pub(crate) async fn enqueue_resume_pending( + &self, + ) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_SNAPSHOT, Some(wire_params)) + .call( + rpc_methods::SESSION_QUEUE_ENQUEUERESUMEPENDING, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Moves an addressable queued item to a public visible position. - /// - /// Wire method: `session.queue.moveItem`. - /// - /// # Parameters - /// - /// * `params` - Parameters for moving a queued item by stable id. - /// - /// # Returns + /// Drains the native local-session work queue for in-process session orchestration. /// - /// Result of moving a queued item. + /// Wire method: `session.queue.process`. /// ///
/// @@ -9189,31 +10622,35 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn move_item( - &self, - params: QueueMoveItemRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub(crate) async fn process(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_MOVEITEM, Some(wire_params)) + .call(rpc_methods::SESSION_QUEUE_PROCESS, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } +} - /// Inserts a new queued message at a public visible position. +/// `session.remote.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcRemote<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcRemote<'a> { + /// Enables remote session export or steering. /// - /// Wire method: `session.queue.insertAt`. + /// Wire method: `session.remote.enable`. /// /// # Parameters /// - /// * `params` - Parameters for inserting a queued message at a public visible position. + /// * `params` - Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. /// /// # Returns /// - /// Result of inserting a queued message. + /// GitHub URL for the session and a flag indicating whether remote steering is enabled. /// ///
/// @@ -9222,31 +10659,20 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn insert_at( - &self, - params: QueueInsertAtRequest, - ) -> Result { + pub async fn enable(&self, params: RemoteEnableRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_INSERTAT, Some(wire_params)) + .call(rpc_methods::SESSION_REMOTE_ENABLE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Removes an addressable queued item by its stable id. - /// - /// Wire method: `session.queue.removeAt`. - /// - /// # Parameters - /// - /// * `params` - Parameters for removing a queued item by stable id. - /// - /// # Returns + /// Disables remote session export and steering. /// - /// Result of removing a queued item. + /// Wire method: `session.remote.disable`. /// ///
/// @@ -9255,31 +10681,27 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn remove_at( - &self, - params: QueueRemoveAtRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn disable(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_REMOVEAT, Some(wire_params)) + .call(rpc_methods::SESSION_REMOTE_DISABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Updates the text of an addressable single-message queue item. + /// Persists a remote-steerability change emitted by the host as a session event. /// - /// Wire method: `session.queue.updateText`. + /// Wire method: `session.remote.notifySteerableChanged`. /// /// # Parameters /// - /// * `params` - Parameters for editing a single queued message. + /// * `params` - New remote-steerability state to persist as a `session.remote_steerable_changed` event. /// /// # Returns /// - /// Result of editing a queued message. + /// Persist a steerability change as a `session.remote_steerable_changed` event. Used by the host (CLI / SDK consumer) when it has just finished enabling or disabling steering on a remote exporter that the runtime does not directly own. /// ///
/// @@ -9288,31 +10710,38 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn update_text( + pub async fn notify_steerable_changed( &self, - params: QueueUpdateTextRequest, - ) -> Result { + params: RemoteNotifySteerableChangedRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_UPDATETEXT, Some(wire_params)) + .call( + rpc_methods::SESSION_REMOTE_NOTIFYSTEERABLECHANGED, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Duplicates an addressable queued item immediately after its source. - /// - /// Wire method: `session.queue.duplicateAt`. - /// - /// # Parameters +/// `session.sandbox.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcSandbox<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcSandbox<'a> { + /// Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. /// - /// * `params` - Parameters for duplicating a queued item. + /// Wire method: `session.sandbox.getEnforcementStatus`. /// /// # Returns /// - /// Result of duplicating a queued item. + /// Managed sandbox enforcement state for a session. /// ///
/// @@ -9321,27 +10750,30 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn duplicate_at( - &self, - params: QueueDuplicateAtRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn get_enforcement_status(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_DUPLICATEAT, Some(wire_params)) + .call( + rpc_methods::SESSION_SANDBOX_GETENFORCEMENTSTATUS, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Acquires or releases the queued-lane drain pause. + /// Disables sandboxing for the remainder of the current session and approves the referenced pending sandbox-bypass permission request. The request is rejected unless the exact request is still pending and the effective sandbox policy permits bypass. /// - /// Wire method: `session.queue.setDrainPaused`. + /// Wire method: `session.sandbox.disableForSession`. /// /// # Parameters /// - /// * `params` - Parameters for acquiring or releasing the queued-lane drain pause. Acquisition is exclusive and non-idempotent: `paused: true` against an already-paused session fails with `queue_already_paused`. The pause is never released automatically — it is not tied to the caller's lifetime, so a client that exits without sending `paused: false` leaves the lane frozen. Release is unowned: `paused: false` clears the pause for any caller, including one that never acquired it. + /// * `params` - Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. + /// + /// # Returns + /// + /// Result of attempting to disable sandboxing for the current session. /// ///
/// @@ -9350,28 +10782,38 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_drain_paused(&self, params: QueueSetDrainPausedRequest) -> Result<(), Error> { + pub async fn disable_for_session( + &self, + params: SandboxDisableForSessionRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_SETDRAINPAUSED, Some(wire_params)) + .call( + rpc_methods::SESSION_SANDBOX_DISABLEFORSESSION, + Some(wire_params), + ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } +} - /// Moves an addressable queued message into the live turn's steering lane. - /// - /// Wire method: `session.queue.sendNow`. - /// - /// # Parameters +/// `session.schedule.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcSchedule<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcSchedule<'a> { + /// Lists the session's currently active scheduled prompts. /// - /// * `params` - Parameters for steering a queued message into a live turn. + /// Wire method: `session.schedule.list`. /// /// # Returns /// - /// Result of trying to steer a queued message into a live turn. + /// Snapshot of the currently active recurring prompts for this session. /// ///
/// @@ -9380,24 +10822,19 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn send_now(&self, params: QueueSendNowRequest) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_SENDNOW, Some(wire_params)) + .call(rpc_methods::SESSION_SCHEDULE_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Reports whether the local session has native queued work pending. - /// - /// Wire method: `session.queue.hasPending`. - /// - /// # Returns + /// Hydrates the native schedule registry from persisted session events. /// - /// Whether the native queue has pending work. + /// Wire method: `session.schedule.hydrate`. /// ///
/// @@ -9406,27 +10843,23 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn has_pending(&self) -> Result { + pub(crate) async fn hydrate(&self) -> Result<(), Error> { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_HASPENDING, Some(wire_params)) + .call(rpc_methods::SESSION_SCHEDULE_HYDRATE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Begins a native deferred-idle drain when background work has quiesced. - /// - /// Wire method: `session.queue.beginDeferredIdleDrain`. - /// - /// # Parameters + /// Reports whether the session has an active self-paced scheduled prompt. /// - /// * `params` - Inputs for starting a deferred-idle drain. + /// Wire method: `session.schedule.hasSelfPaced`. /// /// # Returns /// - /// Whether a deferred-idle drain should run. + /// Whether the session currently has an active self-paced schedule. /// ///
/// @@ -9435,34 +10868,30 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn begin_deferred_idle_drain( - &self, - params: QueueBeginDeferredIdleDrainRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub(crate) async fn has_self_paced(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_QUEUE_BEGINDEFERREDIDLEDRAIN, + rpc_methods::SESSION_SCHEDULE_HASSELFPACED, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Finishes a native deferred-idle drain and reports whether to drain queue work or emit idle. + /// Registers a relative-interval scheduled prompt. /// - /// Wire method: `session.queue.finishDeferredIdleDrain`. + /// Wire method: `session.schedule.add`. /// /// # Parameters /// - /// * `params` - Inputs for completing a deferred-idle drain. + /// * `params` - Register a relative-interval scheduled prompt. /// /// # Returns /// - /// Action selected by the native deferred-idle drain. + /// Result of registering or re-arming a scheduled prompt. /// ///
/// @@ -9471,30 +10900,28 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn finish_deferred_idle_drain( - &self, - params: QueueFinishDeferredIdleDrainRequest, - ) -> Result { + pub(crate) async fn add(&self, params: ScheduleAddRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_QUEUE_FINISHDEFERREDIDLEDRAIN, - Some(wire_params), - ) + .call(rpc_methods::SESSION_SCHEDULE_ADD, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Marks session.idle as deferred by native background work state. + /// Registers a recurring cron scheduled prompt. /// - /// Wire method: `session.queue.deferSessionIdle`. + /// Wire method: `session.schedule.addCron`. /// /// # Parameters /// - /// * `params` - Inputs for marking session.idle deferred in native state. + /// * `params` - Register a cron scheduled prompt. + /// + /// # Returns + /// + /// Result of registering or re-arming a scheduled prompt. /// ///
/// @@ -9503,30 +10930,31 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn defer_session_idle( + pub(crate) async fn add_cron( &self, - params: QueueDeferSessionIdleRequest, - ) -> Result<(), Error> { + params: ScheduleAddCronRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_QUEUE_DEFERSESSIONIDLE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_SCHEDULE_ADDCRON, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Removes the most recently queued user-facing item (LIFO). + /// Registers an absolute-time scheduled prompt. /// - /// Wire method: `session.queue.removeMostRecent`. + /// Wire method: `session.schedule.addAt`. + /// + /// # Parameters + /// + /// * `params` - Register an absolute-time scheduled prompt. /// /// # Returns /// - /// Indicates whether a user-facing pending item was removed. + /// Result of registering or re-arming a scheduled prompt. /// ///
/// @@ -9535,22 +10963,31 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn remove_most_recent(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub(crate) async fn add_at( + &self, + params: ScheduleAddAtRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_QUEUE_REMOVEMOSTRECENT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_SCHEDULE_ADDAT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Clears all pending queued items on the local session. + /// Registers a self-paced scheduled prompt. /// - /// Wire method: `session.queue.clear`. + /// Wire method: `session.schedule.addSelfPaced`. + /// + /// # Parameters + /// + /// * `params` - Register a self-paced scheduled prompt. + /// + /// # Returns + /// + /// Result of registering or re-arming a scheduled prompt. /// ///
/// @@ -9559,27 +10996,34 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn clear(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub(crate) async fn add_self_paced( + &self, + params: ScheduleAddSelfPacedRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_CLEAR, Some(wire_params)) + .call( + rpc_methods::SESSION_SCHEDULE_ADDSELFPACED, + Some(wire_params), + ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Consumes queued native system notifications matching an internal filter. + /// Re-arms an active self-paced scheduled prompt. /// - /// Wire method: `session.queue.consumeSystemNotifications`. + /// Wire method: `session.schedule.rearmSelfPaced`. /// /// # Parameters /// - /// * `params` - Internal filter for consuming queued system notifications. + /// * `params` - Re-arm a self-paced scheduled prompt. /// /// # Returns /// - /// Indicates whether a user-facing pending item was removed. + /// Result of registering or re-arming a scheduled prompt. /// ///
/// @@ -9588,30 +11032,34 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn consume_system_notifications( + pub(crate) async fn rearm_self_paced( &self, - params: QueueConsumeSystemNotificationsRequest, - ) -> Result { + params: ScheduleRearmSelfPacedRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_QUEUE_CONSUMESYSTEMNOTIFICATIONS, + rpc_methods::SESSION_SCHEDULE_REARMSELFPACED, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Enqueues the internal resume-pending wake item when orphan handling needs a follow-up turn. + /// Removes a scheduled prompt by id. /// - /// Wire method: `session.queue.enqueueResumePending`. + /// Wire method: `session.schedule.stop`. + /// + /// # Parameters + /// + /// * `params` - Identifier of the scheduled prompt to remove. /// /// # Returns /// - /// Result of enqueueing the resume-pending wake item. + /// Remove a scheduled prompt by id. The result entry is omitted if the id was unknown. /// ///
/// @@ -9620,24 +11068,32 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn enqueue_resume_pending( - &self, - ) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn stop(&self, params: ScheduleStopRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_QUEUE_ENQUEUERESUMEPENDING, - Some(wire_params), - ) + .call(rpc_methods::SESSION_SCHEDULE_STOP, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Drains the native local-session work queue for in-process session orchestration. +/// `session.settings.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcSettings<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcSettings<'a> { + /// Returns a redacted snapshot of session runtime settings, with secrets and raw feature flags excluded. Internal: the runtime settings shape is a runtime-internal surface and is deliberately kept out of the public SDK, because consumers should not depend on the runtime's internal settings layout. It remains callable in-process and is expected to be reworked as the runtime internals are consolidated. /// - /// Wire method: `session.queue.process`. + /// Wire method: `session.settings.snapshot`. + /// + /// # Returns + /// + /// Redacted, serializable view of session runtime settings for SDK boundary consumers. Secrets and raw feature flags are intentionally excluded. /// ///
/// @@ -9646,35 +11102,27 @@ impl<'a> SessionRpcQueue<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn process(&self) -> Result<(), Error> { + pub(crate) async fn snapshot(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_QUEUE_PROCESS, Some(wire_params)) + .call(rpc_methods::SESSION_SETTINGS_SNAPSHOT, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } -} - -/// `session.remote.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcRemote<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcRemote<'a> { - /// Enables remote session export or steering. + /// Evaluates a named Rust-owned settings predicate without exposing raw feature flags. Internal: the raw feature-flag names and composition are runtime-internal, so this predicate-evaluation helper is kept out of the public SDK surface and is callable in-process only. /// - /// Wire method: `session.remote.enable`. + /// Wire method: `session.settings.evaluatePredicate`. /// /// # Parameters /// - /// * `params` - Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. + /// * `params` - Named Rust-owned settings predicate to evaluate for this session. /// /// # Returns /// - /// GitHub URL for the session and a flag indicating whether remote steering is enabled. + /// Result of evaluating a Rust-owned settings predicate. /// ///
/// @@ -9683,20 +11131,42 @@ impl<'a> SessionRpcRemote<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn enable(&self, params: RemoteEnableRequest) -> Result { + pub(crate) async fn evaluate_predicate( + &self, + params: SessionSettingsEvaluatePredicateRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_REMOTE_ENABLE, Some(wire_params)) + .call( + rpc_methods::SESSION_SETTINGS_EVALUATEPREDICATE, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Disables remote session export and steering. +/// `session.shell.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcShell<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcShell<'a> { + /// Starts a shell command and streams output through session notifications. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. /// - /// Wire method: `session.remote.disable`. + /// Wire method: `session.shell.exec`. + /// + /// # Parameters + /// + /// * `params` - Shell command to run, with optional working directory and timeout in milliseconds. + /// + /// # Returns + /// + /// Identifier of the spawned process, used to correlate streamed output and exit notifications. /// ///
/// @@ -9705,27 +11175,28 @@ impl<'a> SessionRpcRemote<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn disable(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn exec(&self, params: ShellExecRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_REMOTE_DISABLE, Some(wire_params)) + .call(rpc_methods::SESSION_SHELL_EXEC, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Persists a remote-steerability change emitted by the host as a session event. + /// Sends a signal to a shell process previously started via "shell.exec". The signal targets the command's whole process group (POSIX) or job object (Windows), so descendants still in that group are signalled too, not just the direct child. On POSIX a descendant that moved itself into a new session or process group (for example via "setsid") is no longer in the signalled group and survives. /// - /// Wire method: `session.remote.notifySteerableChanged`. + /// Wire method: `session.shell.kill`. /// /// # Parameters /// - /// * `params` - New remote-steerability state to persist as a `session.remote_steerable_changed` event. + /// * `params` - Identifier of a process previously returned by "shell.exec" and the signal to send. /// /// # Returns /// - /// Persist a steerability change as a `session.remote_steerable_changed` event. Used by the host (CLI / SDK consumer) when it has just finished enabling or disabling steering on a remote exporter that the runtime does not directly own. + /// Indicates whether the signal was delivered; false if the process was unknown or already exited. /// ///
/// @@ -9734,38 +11205,28 @@ impl<'a> SessionRpcRemote<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn notify_steerable_changed( - &self, - params: RemoteNotifySteerableChangedRequest, - ) -> Result { + pub async fn kill(&self, params: ShellKillRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_REMOTE_NOTIFYSTEERABLECHANGED, - Some(wire_params), - ) + .call(rpc_methods::SESSION_SHELL_KILL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.sandbox.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcSandbox<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcSandbox<'a> { - /// Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. + /// Executes a user-requested shell command through the session runtime. /// - /// Wire method: `session.sandbox.getEnforcementStatus`. + /// Wire method: `session.shell.executeUserRequested`. + /// + /// # Parameters + /// + /// * `params` - User-requested shell command and cancellation handle. /// /// # Returns /// - /// Managed sandbox enforcement state for a session. + /// Result of a user-requested shell command. /// ///
/// @@ -9774,30 +11235,34 @@ impl<'a> SessionRpcSandbox<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_enforcement_status(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn execute_user_requested( + &self, + params: ShellExecuteUserRequestedRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_SANDBOX_GETENFORCEMENTSTATUS, + rpc_methods::SESSION_SHELL_EXECUTEUSERREQUESTED, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Disables sandboxing for the remainder of the current session and approves the referenced pending sandbox-bypass permission request. The request is rejected unless the exact request is still pending and the effective sandbox policy permits bypass. + /// Cancels a user-requested shell command by request ID. /// - /// Wire method: `session.sandbox.disableForSession`. + /// Wire method: `session.shell.cancelUserRequested`. /// /// # Parameters /// - /// * `params` - Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. + /// * `params` - User-requested shell execution cancellation handle. /// /// # Returns /// - /// Result of attempting to disable sandboxing for the current session. + /// Cancellation result for a user-requested shell command. /// ///
/// @@ -9806,17 +11271,17 @@ impl<'a> SessionRpcSandbox<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn disable_for_session( + pub async fn cancel_user_requested( &self, - params: SandboxDisableForSessionRequest, - ) -> Result { + params: ShellCancelUserRequestedRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_SANDBOX_DISABLEFORSESSION, + rpc_methods::SESSION_SHELL_CANCELUSERREQUESTED, Some(wire_params), ) .await?; @@ -9824,20 +11289,20 @@ impl<'a> SessionRpcSandbox<'a> { } } -/// `session.schedule.*` RPCs. +/// `session.skills.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcSchedule<'a> { +pub struct SessionRpcSkills<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcSchedule<'a> { - /// Lists the session's currently active scheduled prompts. +impl<'a> SessionRpcSkills<'a> { + /// Lists skills available to the session. /// - /// Wire method: `session.schedule.list`. + /// Wire method: `session.skills.list`. /// /// # Returns /// - /// Snapshot of the currently active recurring prompts for this session. + /// Skills available to the session, with their enabled state. /// ///
/// @@ -9846,19 +11311,23 @@ impl<'a> SessionRpcSchedule<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list(&self) -> Result { + pub async fn list(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_SCHEDULE_LIST, Some(wire_params)) + .call(rpc_methods::SESSION_SKILLS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Hydrates the native schedule registry from persisted session events. + /// Returns the skills that have been invoked during this session. /// - /// Wire method: `session.schedule.hydrate`. + /// Wire method: `session.skills.getInvoked`. + /// + /// # Returns + /// + /// Skills invoked during this session, ordered by invocation time (most recent last). /// ///
/// @@ -9867,23 +11336,23 @@ impl<'a> SessionRpcSchedule<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn hydrate(&self) -> Result<(), Error> { + pub async fn get_invoked(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_SCHEDULE_HYDRATE, Some(wire_params)) + .call(rpc_methods::SESSION_SKILLS_GETINVOKED, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Reports whether the session has an active self-paced scheduled prompt. + /// Enables a skill for the session. /// - /// Wire method: `session.schedule.hasSelfPaced`. + /// Wire method: `session.skills.enable`. /// - /// # Returns + /// # Parameters /// - /// Whether the session currently has an active self-paced schedule. + /// * `params` - Name of the skill to enable for the session. /// ///
/// @@ -9892,30 +11361,24 @@ impl<'a> SessionRpcSchedule<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn has_self_paced(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn enable(&self, params: SkillsEnableRequest) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_SCHEDULE_HASSELFPACED, - Some(wire_params), - ) + .call(rpc_methods::SESSION_SKILLS_ENABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Registers a relative-interval scheduled prompt. + /// Disables a skill for the session. /// - /// Wire method: `session.schedule.add`. + /// Wire method: `session.skills.disable`. /// /// # Parameters /// - /// * `params` - Register a relative-interval scheduled prompt. - /// - /// # Returns - /// - /// Result of registering or re-arming a scheduled prompt. + /// * `params` - Name of the skill to disable for the session. /// ///
/// @@ -9924,28 +11387,24 @@ impl<'a> SessionRpcSchedule<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn add(&self, params: ScheduleAddRequest) -> Result { + pub async fn disable(&self, params: SkillsDisableRequest) -> Result<(), Error> { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_SCHEDULE_ADD, Some(wire_params)) + .call(rpc_methods::SESSION_SKILLS_DISABLE, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } - /// Registers a recurring cron scheduled prompt. - /// - /// Wire method: `session.schedule.addCron`. - /// - /// # Parameters + /// Reloads skill definitions for the session. /// - /// * `params` - Register a cron scheduled prompt. + /// Wire method: `session.skills.reload`. /// /// # Returns /// - /// Result of registering or re-arming a scheduled prompt. + /// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. /// ///
/// @@ -9954,31 +11413,19 @@ impl<'a> SessionRpcSchedule<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn add_cron( - &self, - params: ScheduleAddCronRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn reload(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_SCHEDULE_ADDCRON, Some(wire_params)) + .call(rpc_methods::SESSION_SKILLS_RELOAD, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Registers an absolute-time scheduled prompt. - /// - /// Wire method: `session.schedule.addAt`. - /// - /// # Parameters - /// - /// * `params` - Register an absolute-time scheduled prompt. - /// - /// # Returns + /// Ensures the session's skill definitions have been loaded from disk. /// - /// Result of registering or re-arming a scheduled prompt. + /// Wire method: `session.skills.ensureLoaded`. /// ///
/// @@ -9987,31 +11434,35 @@ impl<'a> SessionRpcSchedule<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn add_at( - &self, - params: ScheduleAddAtRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn ensure_loaded(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_SCHEDULE_ADDAT, Some(wire_params)) + .call(rpc_methods::SESSION_SKILLS_ENSURELOADED, Some(wire_params)) .await?; - Ok(serde_json::from_value(_value)?) + Ok(()) } +} - /// Registers a self-paced scheduled prompt. +/// `session.tasks.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcTasks<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcTasks<'a> { + /// Starts a background agent task in the session. /// - /// Wire method: `session.schedule.addSelfPaced`. + /// Wire method: `session.tasks.startAgent`. /// /// # Parameters /// - /// * `params` - Register a self-paced scheduled prompt. + /// * `params` - Agent type, prompt, name, and optional description and model override for the new task. /// /// # Returns /// - /// Result of registering or re-arming a scheduled prompt. + /// Identifier assigned to the newly started background agent task. /// ///
/// @@ -10020,34 +11471,27 @@ impl<'a> SessionRpcSchedule<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn add_self_paced( + pub async fn start_agent( &self, - params: ScheduleAddSelfPacedRequest, - ) -> Result { + params: TasksStartAgentRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_SCHEDULE_ADDSELFPACED, - Some(wire_params), - ) + .call(rpc_methods::SESSION_TASKS_STARTAGENT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Re-arms an active self-paced scheduled prompt. - /// - /// Wire method: `session.schedule.rearmSelfPaced`. - /// - /// # Parameters + /// Lists background tasks tracked by the session. /// - /// * `params` - Re-arm a self-paced scheduled prompt. + /// Wire method: `session.tasks.list`. /// /// # Returns /// - /// Result of registering or re-arming a scheduled prompt. + /// Background tasks currently tracked by the session. /// ///
/// @@ -10056,34 +11500,27 @@ impl<'a> SessionRpcSchedule<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn rearm_self_paced( - &self, - params: ScheduleRearmSelfPacedRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_SCHEDULE_REARMSELFPACED, - Some(wire_params), - ) + .call(rpc_methods::SESSION_TASKS_LIST, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Removes a scheduled prompt by id. + /// Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. /// - /// Wire method: `session.schedule.stop`. + /// Wire method: `session.tasks.register`. /// /// # Parameters /// - /// * `params` - Identifier of the scheduled prompt to remove. + /// * `params` - Registers or reclaims a client-owned task. /// /// # Returns /// - /// Remove a scheduled prompt by id. The result entry is omitted if the id was unknown. + /// Result of registering or reclaiming a client-owned task. /// ///
/// @@ -10092,32 +11529,31 @@ impl<'a> SessionRpcSchedule<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn stop(&self, params: ScheduleStopRequest) -> Result { + pub async fn register( + &self, + params: TasksRegisterRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_SCHEDULE_STOP, Some(wire_params)) + .call(rpc_methods::SESSION_TASKS_REGISTER, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `session.settings.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcSettings<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcSettings<'a> { - /// Returns a redacted snapshot of session runtime settings, with secrets and raw feature flags excluded. Internal: the runtime settings shape is a runtime-internal surface and is deliberately kept out of the public SDK, because consumers should not depend on the runtime's internal settings layout. It remains callable in-process and is expected to be reworked as the runtime internals are consolidated. + /// Publishes generic progress or a terminal outcome for a client-owned task. /// - /// Wire method: `session.settings.snapshot`. + /// Wire method: `session.tasks.update`. + /// + /// # Parameters + /// + /// * `params` - Updates a client-owned task. /// /// # Returns /// - /// Redacted, serializable view of session runtime settings for SDK boundary consumers. Secrets and raw feature flags are intentionally excluded. + /// Result of publishing a client-owned task update. /// ///
/// @@ -10126,27 +11562,24 @@ impl<'a> SessionRpcSettings<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn snapshot(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn update(&self, params: TasksUpdateRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_SETTINGS_SNAPSHOT, Some(wire_params)) + .call(rpc_methods::SESSION_TASKS_UPDATE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Evaluates a named Rust-owned settings predicate without exposing raw feature flags. Internal: the raw feature-flag names and composition are runtime-internal, so this predicate-evaluation helper is kept out of the public SDK surface and is callable in-process only. - /// - /// Wire method: `session.settings.evaluatePredicate`. - /// - /// # Parameters + /// Refreshes metadata for any detached background shells the runtime knows about. /// - /// * `params` - Named Rust-owned settings predicate to evaluate for this session. + /// Wire method: `session.tasks.refresh`. /// /// # Returns /// - /// Result of evaluating a Rust-owned settings predicate. + /// Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop. /// ///
/// @@ -10155,42 +11588,23 @@ impl<'a> SessionRpcSettings<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub(crate) async fn evaluate_predicate( - &self, - params: SessionSettingsEvaluatePredicateRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn refresh(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_SETTINGS_EVALUATEPREDICATE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_TASKS_REFRESH, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `session.shell.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcShell<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcShell<'a> { - /// Starts a shell command and streams output through session notifications. The command runs as the leader of its own process group (POSIX) or in a dedicated job object (Windows), so a forced termination — via "shell.kill", the request timeout, or session disposal — signals that whole group/job rather than only the direct child. Two gaps are worth planning for: a command that exits on its own does not trigger that teardown, and on POSIX a descendant that moves itself into a new session or process group (for example via "setsid") leaves the signalled group, so either can leave a background process running. - /// - /// Wire method: `session.shell.exec`. - /// - /// # Parameters + /// Waits for all in-flight background tasks and any follow-up turns to settle. /// - /// * `params` - Shell command to run, with optional working directory and timeout in milliseconds. + /// Wire method: `session.tasks.waitForPending`. /// /// # Returns /// - /// Identifier of the spawned process, used to correlate streamed output and exit notifications. + /// Wait until all in-flight background tasks (agents + shells) and any follow-up turns scheduled by their completions have settled. Returns when the runtime is fully drained or after an internal timeout (default 10 minutes; configurable via COPILOT_TASK_WAIT_TIMEOUT_SECONDS). /// ///
/// @@ -10199,28 +11613,27 @@ impl<'a> SessionRpcShell<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn exec(&self, params: ShellExecRequest) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn wait_for_pending(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_SHELL_EXEC, Some(wire_params)) + .call(rpc_methods::SESSION_TASKS_WAITFORPENDING, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Sends a signal to a shell process previously started via "shell.exec". The signal targets the command's whole process group (POSIX) or job object (Windows), so descendants still in that group are signalled too, not just the direct child. On POSIX a descendant that moved itself into a new session or process group (for example via "setsid") is no longer in the signalled group and survives. + /// Returns progress information for a background task by ID. /// - /// Wire method: `session.shell.kill`. + /// Wire method: `session.tasks.getProgress`. /// /// # Parameters /// - /// * `params` - Identifier of a process previously returned by "shell.exec" and the signal to send. + /// * `params` - Identifier of the background task to fetch progress for. /// /// # Returns /// - /// Indicates whether the signal was delivered; false if the process was unknown or already exited. + /// Progress information for the task, or null when no task with that ID is tracked. /// ///
/// @@ -10229,28 +11642,27 @@ impl<'a> SessionRpcShell<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn kill(&self, params: ShellKillRequest) -> Result { + pub async fn get_progress( + &self, + params: TasksGetProgressRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_SHELL_KILL, Some(wire_params)) - .await?; - Ok(serde_json::from_value(_value)?) - } - - /// Executes a user-requested shell command through the session runtime. - /// - /// Wire method: `session.shell.executeUserRequested`. - /// - /// # Parameters + .call(rpc_methods::SESSION_TASKS_GETPROGRESS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the first sync-waiting task that can currently be promoted to background mode. /// - /// * `params` - User-requested shell command and cancellation handle. + /// Wire method: `session.tasks.getCurrentPromotable`. /// /// # Returns /// - /// Result of a user-requested shell command. + /// The first sync-waiting task that can currently be promoted to background mode. /// ///
/// @@ -10259,34 +11671,30 @@ impl<'a> SessionRpcShell<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn execute_user_requested( - &self, - params: ShellExecuteUserRequestedRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn get_current_promotable(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_SHELL_EXECUTEUSERREQUESTED, + rpc_methods::SESSION_TASKS_GETCURRENTPROMOTABLE, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Cancels a user-requested shell command by request ID. + /// Promotes an eligible synchronously-waited task so it continues running in the background. /// - /// Wire method: `session.shell.cancelUserRequested`. + /// Wire method: `session.tasks.promoteToBackground`. /// /// # Parameters /// - /// * `params` - User-requested shell execution cancellation handle. + /// * `params` - Identifier of the task to promote to background mode. /// /// # Returns /// - /// Cancellation result for a user-requested shell command. + /// Indicates whether the task was successfully promoted to background mode. /// ///
/// @@ -10295,38 +11703,30 @@ impl<'a> SessionRpcShell<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn cancel_user_requested( + pub async fn promote_to_background( &self, - params: ShellCancelUserRequestedRequest, - ) -> Result { + params: TasksPromoteToBackgroundRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_SHELL_CANCELUSERREQUESTED, + rpc_methods::SESSION_TASKS_PROMOTETOBACKGROUND, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.skills.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcSkills<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcSkills<'a> { - /// Lists skills available to the session. + /// Atomically promotes the first promotable sync-waiting task to background mode and returns it. /// - /// Wire method: `session.skills.list`. + /// Wire method: `session.tasks.promoteCurrentToBackground`. /// /// # Returns /// - /// Skills available to the session, with their enabled state. + /// The promoted task as it now exists in background mode, omitted if no promotable task was waiting. /// ///
/// @@ -10335,23 +11735,32 @@ impl<'a> SessionRpcSkills<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list(&self) -> Result { + pub async fn promote_current_to_background( + &self, + ) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_SKILLS_LIST, Some(wire_params)) + .call( + rpc_methods::SESSION_TASKS_PROMOTECURRENTTOBACKGROUND, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the skills that have been invoked during this session. + /// Cancels a background task. /// - /// Wire method: `session.skills.getInvoked`. + /// Wire method: `session.tasks.cancel`. + /// + /// # Parameters + /// + /// * `params` - Identifier of the background task to cancel. /// /// # Returns /// - /// Skills invoked during this session, ordered by invocation time (most recent last). + /// Indicates whether the background task was successfully cancelled. /// ///
/// @@ -10360,23 +11769,28 @@ impl<'a> SessionRpcSkills<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_invoked(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn cancel(&self, params: TasksCancelRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_SKILLS_GETINVOKED, Some(wire_params)) + .call(rpc_methods::SESSION_TASKS_CANCEL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Enables a skill for the session. + /// Removes a completed or cancelled background task from tracking. /// - /// Wire method: `session.skills.enable`. + /// Wire method: `session.tasks.remove`. /// /// # Parameters /// - /// * `params` - Name of the skill to enable for the session. + /// * `params` - Identifier of the completed or cancelled task to remove from tracking. + /// + /// # Returns + /// + /// Indicates whether the task was removed. False when the task does not exist or is still running/idle. /// ///
/// @@ -10385,24 +11799,28 @@ impl<'a> SessionRpcSkills<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn enable(&self, params: SkillsEnableRequest) -> Result<(), Error> { + pub async fn remove(&self, params: TasksRemoveRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_SKILLS_ENABLE, Some(wire_params)) + .call(rpc_methods::SESSION_TASKS_REMOVE, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } - /// Disables a skill for the session. + /// Sends a message to a background agent task. /// - /// Wire method: `session.skills.disable`. + /// Wire method: `session.tasks.sendMessage`. /// /// # Parameters /// - /// * `params` - Name of the skill to disable for the session. + /// * `params` - Identifier of the target agent task, message content, and optional sender agent ID. + /// + /// # Returns + /// + /// Indicates whether the message was delivered, with an error message when delivery failed. /// ///
/// @@ -10411,24 +11829,35 @@ impl<'a> SessionRpcSkills<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn disable(&self, params: SkillsDisableRequest) -> Result<(), Error> { + pub async fn send_message( + &self, + params: TasksSendMessageRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_SKILLS_DISABLE, Some(wire_params)) + .call(rpc_methods::SESSION_TASKS_SENDMESSAGE, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } +} - /// Reloads skill definitions for the session. +/// `session.telemetry.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcTelemetry<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcTelemetry<'a> { + /// Gets the telemetry engagement ID currently associated with the session, when available. /// - /// Wire method: `session.skills.reload`. + /// Wire method: `session.telemetry.getEngagementId`. /// /// # Returns /// - /// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + /// Telemetry engagement ID for the session, when available. /// ///
/// @@ -10437,19 +11866,26 @@ impl<'a> SessionRpcSkills<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn reload(&self) -> Result { + pub async fn get_engagement_id(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_SKILLS_RELOAD, Some(wire_params)) + .call( + rpc_methods::SESSION_TELEMETRY_GETENGAGEMENTID, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Ensures the session's skill definitions have been loaded from disk. + /// Sets feature override key/value pairs to attach to subsequent telemetry events for the session. /// - /// Wire method: `session.skills.ensureLoaded`. + /// Wire method: `session.telemetry.setFeatureOverrides`. + /// + /// # Parameters + /// + /// * `params` - Feature override key/value pairs to attach to subsequent telemetry events from this session. /// ///
/// @@ -10458,35 +11894,42 @@ impl<'a> SessionRpcSkills<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn ensure_loaded(&self) -> Result<(), Error> { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn set_feature_overrides( + &self, + params: TelemetrySetFeatureOverridesRequest, + ) -> Result<(), Error> { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_SKILLS_ENSURELOADED, Some(wire_params)) + .call( + rpc_methods::SESSION_TELEMETRY_SETFEATUREOVERRIDES, + Some(wire_params), + ) .await?; Ok(()) } } -/// `session.tasks.*` RPCs. +/// `session.tools.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcTasks<'a> { +pub struct SessionRpcTools<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcTasks<'a> { - /// Starts a background agent task in the session. +impl<'a> SessionRpcTools<'a> { + /// Executes one tool from the session's currently offered tool set through the native invocation pipeline. /// - /// Wire method: `session.tasks.startAgent`. + /// Wire method: `session.tools.execute`. /// /// # Parameters /// - /// * `params` - Agent type, prompt, name, and optional description and model override for the new task. + /// * `params` - A tool name and arguments to execute through the session's native invocation pipeline. /// /// # Returns /// - /// Identifier assigned to the newly started background agent task. + /// Canonical result returned by a session tool. /// ///
/// @@ -10495,27 +11938,28 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn start_agent( - &self, - params: TasksStartAgentRequest, - ) -> Result { + pub async fn execute(&self, params: ToolsExecuteRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_STARTAGENT, Some(wire_params)) + .call(rpc_methods::SESSION_TOOLS_EXECUTE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Lists background tasks tracked by the session. + /// Returns the Rust-owned built-in tool descriptors used to construct the session's offered tool set. /// - /// Wire method: `session.tasks.list`. + /// Wire method: `session.tools.getBuiltinDescriptors`. + /// + /// # Parameters + /// + /// * `params` - Options controlling how Rust-owned built-in tool descriptors are materialized. /// /// # Returns /// - /// Background tasks currently tracked by the session. + /// Rust-owned built-in tool descriptors for the session. /// ///
/// @@ -10524,27 +11968,34 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn get_builtin_descriptors( + &self, + params: ToolsGetBuiltinDescriptorsRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_LIST, Some(wire_params)) + .call( + rpc_methods::SESSION_TOOLS_GETBUILTINDESCRIPTORS, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. + /// Projects a completed task_complete tool call into its label-safe session event payload. /// - /// Wire method: `session.tasks.register`. + /// Wire method: `session.tools.taskCompleteEventData`. /// /// # Parameters /// - /// * `params` - Registers or reclaims a client-owned task. + /// * `params` - Task-completion tool arguments and final result used to build a label-safe session event payload. /// /// # Returns /// - /// Result of registering or reclaiming a client-owned task. + /// Task completion notification with summary from the agent /// ///
/// @@ -10553,31 +12004,34 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn register( + pub async fn task_complete_event_data( &self, - params: TasksRegisterRequest, - ) -> Result { + params: ToolsTaskCompleteEventDataRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_REGISTER, Some(wire_params)) + .call( + rpc_methods::SESSION_TOOLS_TASKCOMPLETEEVENTDATA, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Publishes generic progress or a terminal outcome for a client-owned task. + /// Provides the result for a pending external tool call. /// - /// Wire method: `session.tasks.update`. + /// Wire method: `session.tools.handlePendingToolCall`. /// /// # Parameters /// - /// * `params` - Updates a client-owned task. + /// * `params` - Pending external tool call request ID, with the tool result or an error describing why it failed. /// /// # Returns /// - /// Result of publishing a client-owned task update. + /// Indicates whether the external tool call result was handled successfully. /// ///
/// @@ -10586,24 +12040,30 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn update(&self, params: TasksUpdateRequest) -> Result { + pub async fn handle_pending_tool_call( + &self, + params: HandlePendingToolCallRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_UPDATE, Some(wire_params)) + .call( + rpc_methods::SESSION_TOOLS_HANDLEPENDINGTOOLCALL, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Refreshes metadata for any detached background shells the runtime knows about. + /// Resolves, builds, and validates the runtime tool list for the session. /// - /// Wire method: `session.tasks.refresh`. + /// Wire method: `session.tools.initializeAndValidate`. /// /// # Returns /// - /// Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop. + /// Resolve, build, and validate the runtime tool list for this session. Subagent sessions and consumer flows that need an initialized tool set before `send` invoke this. Default base-class implementation is a no-op for sessions that don't support tool validation. /// ///
/// @@ -10612,23 +12072,26 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn refresh(&self) -> Result { + pub async fn initialize_and_validate(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_REFRESH, Some(wire_params)) + .call( + rpc_methods::SESSION_TOOLS_INITIALIZEANDVALIDATE, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Waits for all in-flight background tasks and any follow-up turns to settle. + /// Returns lightweight metadata for the session's currently initialized tools. /// - /// Wire method: `session.tasks.waitForPending`. + /// Wire method: `session.tools.getCurrentMetadata`. /// /// # Returns /// - /// Wait until all in-flight background tasks (agents + shells) and any follow-up turns scheduled by their completions have settled. Returns when the runtime is fully drained or after an internal timeout (default 10 minutes; configurable via COPILOT_TASK_WAIT_TIMEOUT_SECONDS). + /// Current lightweight tool metadata snapshot for the session. /// ///
/// @@ -10637,27 +12100,30 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn wait_for_pending(&self) -> Result { + pub async fn get_current_metadata(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_WAITFORPENDING, Some(wire_params)) + .call( + rpc_methods::SESSION_TOOLS_GETCURRENTMETADATA, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns progress information for a background task by ID. + /// Atomically replaces the complete externally implemented tool list supplied by the calling connection. Built-in, MCP/plugin, extension-discovered, subagent, and tools supplied by other connections remain unchanged. /// - /// Wire method: `session.tasks.getProgress`. + /// Wire method: `session.tools.set`. /// /// # Parameters /// - /// * `params` - Identifier of the background task to fetch progress for. + /// * `params` - Complete externally implemented tool list for the calling connection. An empty list removes every tool previously supplied by that connection. /// /// # Returns /// - /// Progress information for the task, or null when no task with that ID is tracked. + /// Empty result after replacing the calling connection's externally implemented tools. /// ///
/// @@ -10666,27 +12132,28 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_progress( - &self, - params: TasksGetProgressRequest, - ) -> Result { + pub async fn set(&self, params: ToolsSetRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_GETPROGRESS, Some(wire_params)) + .call(rpc_methods::SESSION_TOOLS_SET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the first sync-waiting task that can currently be promoted to background mode. + /// Sets the current session's live subagent settings override, which takes precedence over persisted user settings until cleared. Persisted user settings remain the source of truth for future sessions. /// - /// Wire method: `session.tasks.getCurrentPromotable`. + /// Wire method: `session.tools.updateSubagentSettings`. + /// + /// # Parameters + /// + /// * `params` - Subagent settings to apply to the current session /// /// # Returns /// - /// The first sync-waiting task that can currently be promoted to background mode. + /// Empty result after applying subagent settings /// ///
/// @@ -10695,30 +12162,42 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_current_promotable(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn update_subagent_settings( + &self, + params: UpdateSubagentSettingsRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_TASKS_GETCURRENTPROMOTABLE, + rpc_methods::SESSION_TOOLS_UPDATESUBAGENTSETTINGS, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Promotes an eligible synchronously-waited task so it continues running in the background. +/// `session.ui.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcUi<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcUi<'a> { + /// Runs a transient no-tools model query against the current conversation context. /// - /// Wire method: `session.tasks.promoteToBackground`. + /// Wire method: `session.ui.ephemeralQuery`. /// /// # Parameters /// - /// * `params` - Identifier of the task to promote to background mode. + /// * `params` - Transient question to answer without adding it to conversation history. /// /// # Returns /// - /// Indicates whether the task was successfully promoted to background mode. + /// Completed transient query. Ordered chunks and the terminal outcome are also delivered through `ui.ephemeral_query` session events while it runs. /// ///
/// @@ -10727,30 +12206,31 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn promote_to_background( + pub async fn ephemeral_query( &self, - params: TasksPromoteToBackgroundRequest, - ) -> Result { + params: UIEphemeralQueryRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_TASKS_PROMOTETOBACKGROUND, - Some(wire_params), - ) + .call(rpc_methods::SESSION_UI_EPHEMERALQUERY, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Atomically promotes the first promotable sync-waiting task to background mode and returns it. + /// Requests structured input from a UI-capable client. /// - /// Wire method: `session.tasks.promoteCurrentToBackground`. + /// Wire method: `session.ui.elicitation`. + /// + /// # Parameters + /// + /// * `params` - Prompt message and JSON schema describing the form fields to elicit from the user. /// /// # Returns /// - /// The promoted task as it now exists in background mode, omitted if no promotable task was waiting. + /// The elicitation response (accept with form values, decline, or cancel) /// ///
/// @@ -10759,32 +12239,31 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn promote_current_to_background( + pub async fn elicitation( &self, - ) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + params: UIElicitationRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_TASKS_PROMOTECURRENTTOBACKGROUND, - Some(wire_params), - ) + .call(rpc_methods::SESSION_UI_ELICITATION, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Cancels a background task. + /// Provides the user response for a pending elicitation request. /// - /// Wire method: `session.tasks.cancel`. + /// Wire method: `session.ui.handlePendingElicitation`. /// /// # Parameters /// - /// * `params` - Identifier of the background task to cancel. + /// * `params` - Pending elicitation request ID and the user's response (accept/decline/cancel + form values). /// /// # Returns /// - /// Indicates whether the background task was successfully cancelled. + /// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. /// ///
/// @@ -10793,28 +12272,34 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn cancel(&self, params: TasksCancelRequest) -> Result { + pub async fn handle_pending_elicitation( + &self, + params: UIHandlePendingElicitationRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_CANCEL, Some(wire_params)) + .call( + rpc_methods::SESSION_UI_HANDLEPENDINGELICITATION, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Removes a completed or cancelled background task from tracking. + /// Resolves a pending `user_input.requested` event with the user's response. /// - /// Wire method: `session.tasks.remove`. + /// Wire method: `session.ui.handlePendingUserInput`. /// /// # Parameters /// - /// * `params` - Identifier of the completed or cancelled task to remove from tracking. + /// * `params` - Request ID of a pending `user_input.requested` event and the user's response. /// /// # Returns /// - /// Indicates whether the task was removed. False when the task does not exist or is still running/idle. + /// Indicates whether the pending UI request was resolved by this call. /// ///
/// @@ -10823,28 +12308,34 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn remove(&self, params: TasksRemoveRequest) -> Result { + pub async fn handle_pending_user_input( + &self, + params: UIHandlePendingUserInputRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_REMOVE, Some(wire_params)) + .call( + rpc_methods::SESSION_UI_HANDLEPENDINGUSERINPUT, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Sends a message to a background agent task. + /// Resolves a pending `sampling.requested` event with a sampling result, or rejects it. /// - /// Wire method: `session.tasks.sendMessage`. + /// Wire method: `session.ui.handlePendingSampling`. /// /// # Parameters /// - /// * `params` - Identifier of the target agent task, message content, and optional sender agent ID. + /// * `params` - Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). /// /// # Returns /// - /// Indicates whether the message was delivered, with an error message when delivery failed. + /// Indicates whether the pending UI request was resolved by this call. /// ///
/// @@ -10853,35 +12344,34 @@ impl<'a> SessionRpcTasks<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn send_message( + pub async fn handle_pending_sampling( &self, - params: TasksSendMessageRequest, - ) -> Result { + params: UIHandlePendingSamplingRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TASKS_SENDMESSAGE, Some(wire_params)) + .call( + rpc_methods::SESSION_UI_HANDLEPENDINGSAMPLING, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `session.telemetry.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcTelemetry<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcTelemetry<'a> { - /// Gets the telemetry engagement ID currently associated with the session, when available. + /// Resolves a pending `auto_mode_switch.requested` event with the user's accept/decline decision. /// - /// Wire method: `session.telemetry.getEngagementId`. + /// Wire method: `session.ui.handlePendingAutoModeSwitch`. + /// + /// # Parameters + /// + /// * `params` - Request ID of a pending `auto_mode_switch.requested` event and the user's response. /// /// # Returns /// - /// Telemetry engagement ID for the session, when available. + /// Indicates whether the pending UI request was resolved by this call. /// ///
/// @@ -10890,26 +12380,34 @@ impl<'a> SessionRpcTelemetry<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_engagement_id(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn handle_pending_auto_mode_switch( + &self, + params: UIHandlePendingAutoModeSwitchRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_TELEMETRY_GETENGAGEMENTID, + rpc_methods::SESSION_UI_HANDLEPENDINGAUTOMODESWITCH, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Sets feature override key/value pairs to attach to subsequent telemetry events for the session. + /// Resolves a pending `session_limits_exhausted.requested` event with the user's selected limit action. /// - /// Wire method: `session.telemetry.setFeatureOverrides`. + /// Wire method: `session.ui.handlePendingSessionLimitsExhausted`. /// /// # Parameters /// - /// * `params` - Feature override key/value pairs to attach to subsequent telemetry events from this session. + /// * `params` - Request ID of a pending `session_limits_exhausted.requested` event and the user's selected limit action. + /// + /// # Returns + /// + /// Indicates whether the pending UI request was resolved by this call. /// ///
/// @@ -10918,42 +12416,34 @@ impl<'a> SessionRpcTelemetry<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set_feature_overrides( + pub async fn handle_pending_session_limits_exhausted( &self, - params: TelemetrySetFeatureOverridesRequest, - ) -> Result<(), Error> { + params: UIHandlePendingSessionLimitsExhaustedRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_TELEMETRY_SETFEATUREOVERRIDES, + rpc_methods::SESSION_UI_HANDLEPENDINGSESSIONLIMITSEXHAUSTED, Some(wire_params), ) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } -} -/// `session.tools.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcTools<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcTools<'a> { - /// Executes one tool from the session's currently offered tool set through the native invocation pipeline. + /// Resolves a pending `exit_plan_mode.requested` event with the user's response. /// - /// Wire method: `session.tools.execute`. + /// Wire method: `session.ui.handlePendingExitPlanMode`. /// /// # Parameters /// - /// * `params` - A tool name and arguments to execute through the session's native invocation pipeline. + /// * `params` - Request ID of a pending `exit_plan_mode.requested` event and the user's response. /// /// # Returns /// - /// Canonical result returned by a session tool. + /// Indicates whether the pending UI request was resolved by this call. /// ///
/// @@ -10962,28 +12452,30 @@ impl<'a> SessionRpcTools<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn execute(&self, params: ToolsExecuteRequest) -> Result { + pub async fn handle_pending_exit_plan_mode( + &self, + params: UIHandlePendingExitPlanModeRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TOOLS_EXECUTE, Some(wire_params)) + .call( + rpc_methods::SESSION_UI_HANDLEPENDINGEXITPLANMODE, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the Rust-owned built-in tool descriptors used to construct the session's offered tool set. - /// - /// Wire method: `session.tools.getBuiltinDescriptors`. - /// - /// # Parameters + /// Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. /// - /// * `params` - Options controlling how Rust-owned built-in tool descriptors are materialized. + /// Wire method: `session.ui.registerDirectAutoModeSwitchHandler`. /// /// # Returns /// - /// Rust-owned built-in tool descriptors for the session. + /// Register an in-process handler for `auto_mode_switch.requested` events. The caller still attaches the actual listener via the standard event-subscription mechanism; this registration solely tells the server bridge to skip its own dispatch (so a remote client doesn't race the in-process handler for the same requestId). /// ///
/// @@ -10992,34 +12484,32 @@ impl<'a> SessionRpcTools<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_builtin_descriptors( + pub async fn register_direct_auto_mode_switch_handler( &self, - params: ToolsGetBuiltinDescriptorsRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + ) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call( - rpc_methods::SESSION_TOOLS_GETBUILTINDESCRIPTORS, + rpc_methods::SESSION_UI_REGISTERDIRECTAUTOMODESWITCHHANDLER, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Projects a completed task_complete tool call into its label-safe session event payload. + /// Unregisters a previously-registered in-process auto-mode-switch handler by its opaque handle. /// - /// Wire method: `session.tools.taskCompleteEventData`. + /// Wire method: `session.ui.unregisterDirectAutoModeSwitchHandler`. /// /// # Parameters /// - /// * `params` - Task-completion tool arguments and final result used to build a label-safe session event payload. + /// * `params` - Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. /// /// # Returns /// - /// Task completion notification with summary from the agent + /// Indicates whether the handle was active and the registration count was decremented. /// ///
/// @@ -11028,34 +12518,38 @@ impl<'a> SessionRpcTools<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn task_complete_event_data( + pub async fn unregister_direct_auto_mode_switch_handler( &self, - params: ToolsTaskCompleteEventDataRequest, - ) -> Result { + params: UIUnregisterDirectAutoModeSwitchHandlerRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_TOOLS_TASKCOMPLETEEVENTDATA, + rpc_methods::SESSION_UI_UNREGISTERDIRECTAUTOMODESWITCHHANDLER, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } +} + +/// `session.usage.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcUsage<'a> { + pub(crate) session: &'a Session, +} - /// Provides the result for a pending external tool call. - /// - /// Wire method: `session.tools.handlePendingToolCall`. - /// - /// # Parameters +impl<'a> SessionRpcUsage<'a> { + /// Gets accumulated usage metrics for the session. /// - /// * `params` - Pending external tool call request ID, with the tool result or an error describing why it failed. + /// Wire method: `session.usage.getMetrics`. /// /// # Returns /// - /// Indicates whether the external tool call result was handled successfully. + /// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. /// ///
/// @@ -11064,30 +12558,31 @@ impl<'a> SessionRpcTools<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_tool_call( - &self, - params: HandlePendingToolCallRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + pub async fn get_metrics(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_TOOLS_HANDLEPENDINGTOOLCALL, - Some(wire_params), - ) + .call(rpc_methods::SESSION_USAGE_GETMETRICS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Resolves, builds, and validates the runtime tool list for the session. +/// `session.visibility.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcVisibility<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcVisibility<'a> { + /// Returns the session's current Mission Control sharing status and shareable GitHub URL. Reflects whether the synced session is visible to repository readers ("repo") or restricted to its creator and collaborators ("unshared"). /// - /// Wire method: `session.tools.initializeAndValidate`. + /// Wire method: `session.visibility.get`. /// /// # Returns /// - /// Resolve, build, and validate the runtime tool list for this session. Subagent sessions and consumer flows that need an initialized tool set before `send` invoke this. Default base-class implementation is a no-op for sessions that don't support tool validation. + /// Current sharing status and shareable GitHub URL for a session. /// ///
/// @@ -11096,26 +12591,27 @@ impl<'a> SessionRpcTools<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn initialize_and_validate(&self) -> Result { + pub async fn get(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_TOOLS_INITIALIZEANDVALIDATE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_VISIBILITY_GET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns lightweight metadata for the session's currently initialized tools. + /// Sets the session's Mission Control sharing status, controlling whether the synced session is visible to repository readers. Returns the effective status and shareable GitHub URL after the change. /// - /// Wire method: `session.tools.getCurrentMetadata`. + /// Wire method: `session.visibility.set`. + /// + /// # Parameters + /// + /// * `params` - Desired sharing status for the session. /// /// # Returns /// - /// Current lightweight tool metadata snapshot for the session. + /// Effective sharing status and shareable GitHub URL after updating session visibility. /// ///
/// @@ -11124,30 +12620,43 @@ impl<'a> SessionRpcTools<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_current_metadata(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn set(&self, params: VisibilitySetRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_TOOLS_GETCURRENTMETADATA, - Some(wire_params), - ) + .call(rpc_methods::SESSION_VISIBILITY_SET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Atomically replaces the complete externally implemented tool list supplied by the calling connection. Built-in, MCP/plugin, extension-discovered, subagent, and tools supplied by other connections remain unchanged. +/// `session.workflow.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcWorkflow<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcWorkflow<'a> { + /// `session.workflow.journal.*` sub-namespace. + pub fn journal(&self) -> SessionRpcWorkflowJournal<'a> { + SessionRpcWorkflowJournal { + session: self.session, + } + } + + /// Runs a registered dynamic workflow by name at the top level. /// - /// Wire method: `session.tools.set`. + /// Wire method: `session.workflow.run`. /// /// # Parameters /// - /// * `params` - Complete externally implemented tool list for the calling connection. An empty list removes every tool previously supplied by that connection. + /// * `params` - Parameters for invoking a registered workflow. /// /// # Returns /// - /// Empty result after replacing the calling connection's externally implemented tools. + /// Complete current or terminal workflow run envelope. /// ///
/// @@ -11156,28 +12665,28 @@ impl<'a> SessionRpcTools<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set(&self, params: ToolsSetRequest) -> Result { + pub async fn run(&self, params: WorkflowRunRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_TOOLS_SET, Some(wire_params)) + .call(rpc_methods::SESSION_WORKFLOW_RUN, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Sets the current session's live subagent settings override, which takes precedence over persisted user settings until cleared. Persisted user settings remain the source of truth for future sessions. + /// Resumes a dynamic workflow run using its persisted name, arguments, journal, and accounting. /// - /// Wire method: `session.tools.updateSubagentSettings`. + /// Wire method: `session.workflow.resume`. /// /// # Parameters /// - /// * `params` - Subagent settings to apply to the current session + /// * `params` - Parameters for resuming a workflow run from its persisted identity. /// /// # Returns /// - /// Empty result after applying subagent settings + /// Resolved persisted workflow identity and resumed run envelope. /// ///
/// @@ -11186,42 +12695,31 @@ impl<'a> SessionRpcTools<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn update_subagent_settings( + pub async fn resume( &self, - params: UpdateSubagentSettingsRequest, - ) -> Result { + params: WorkflowResumeRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_TOOLS_UPDATESUBAGENTSETTINGS, - Some(wire_params), - ) + .call(rpc_methods::SESSION_WORKFLOW_RESUME, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `session.ui.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcUi<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcUi<'a> { - /// Runs a transient no-tools model query against the current conversation context. + /// Internal tool-originated dynamic workflow invocation. /// - /// Wire method: `session.ui.ephemeralQuery`. + /// Wire method: `session.workflow.runFromTool`. /// /// # Parameters /// - /// * `params` - Transient question to answer without adding it to conversation history. + /// * `params` - Internal parameters for invoking a registered workflow from a tool. /// /// # Returns /// - /// Completed transient query. Ordered chunks and the terminal outcome are also delivered through `ui.ephemeral_query` session events while it runs. + /// Complete current or terminal workflow run envelope. /// ///
/// @@ -11230,31 +12728,31 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn ephemeral_query( + pub(crate) async fn run_from_tool( &self, - params: UIEphemeralQueryRequest, - ) -> Result { + params: WorkflowToolRunRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_UI_EPHEMERALQUERY, Some(wire_params)) + .call(rpc_methods::SESSION_WORKFLOW_RUNFROMTOOL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Requests structured input from a UI-capable client. + /// Internal tool-originated dynamic workflow resume. /// - /// Wire method: `session.ui.elicitation`. + /// Wire method: `session.workflow.resumeFromTool`. /// /// # Parameters /// - /// * `params` - Prompt message and JSON schema describing the form fields to elicit from the user. + /// * `params` - Internal parameters for resuming a workflow run from a tool. /// /// # Returns /// - /// The elicitation response (accept with form values, decline, or cancel) + /// Resolved persisted workflow identity and resumed run envelope. /// ///
/// @@ -11263,31 +12761,34 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn elicitation( + pub(crate) async fn resume_from_tool( &self, - params: UIElicitationRequest, - ) -> Result { + params: WorkflowToolResumeRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_UI_ELICITATION, Some(wire_params)) + .call( + rpc_methods::SESSION_WORKFLOW_RESUMEFROMTOOL, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Provides the user response for a pending elicitation request. + /// Gets the current or settled envelope for a dynamic workflow run. /// - /// Wire method: `session.ui.handlePendingElicitation`. + /// Wire method: `session.workflow.getRun`. /// /// # Parameters /// - /// * `params` - Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + /// * `params` - Parameters for retrieving a workflow run. /// /// # Returns /// - /// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + /// Complete current or terminal workflow run envelope. /// ///
/// @@ -11296,34 +12797,28 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_elicitation( - &self, - params: UIHandlePendingElicitationRequest, - ) -> Result { + pub async fn get_run(&self, params: WorkflowGetRunRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_UI_HANDLEPENDINGELICITATION, - Some(wire_params), - ) + .call(rpc_methods::SESSION_WORKFLOW_GETRUN, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Resolves a pending `user_input.requested` event with the user's response. + /// Lists durable dynamic workflow runs for this session in creation order. /// - /// Wire method: `session.ui.handlePendingUserInput`. + /// Wire method: `session.workflow.listRuns`. /// /// # Parameters /// - /// * `params` - Request ID of a pending `user_input.requested` event and the user's response. + /// * `params` - Parameters for paging workflow runs. /// /// # Returns /// - /// Indicates whether the pending UI request was resolved by this call. + /// A page of workflow runs in durable creation order. /// ///
/// @@ -11332,34 +12827,31 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_user_input( + pub async fn list_runs( &self, - params: UIHandlePendingUserInputRequest, - ) -> Result { + params: WorkflowListRunsRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_UI_HANDLEPENDINGUSERINPUT, - Some(wire_params), - ) + .call(rpc_methods::SESSION_WORKFLOW_LISTRUNS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Resolves a pending `sampling.requested` event with a sampling result, or rejects it. + /// Gets durable and live observability detail for one dynamic workflow run. /// - /// Wire method: `session.ui.handlePendingSampling`. + /// Wire method: `session.workflow.getRunDetail`. /// /// # Parameters /// - /// * `params` - Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). + /// * `params` - Parameters for retrieving a workflow run. /// /// # Returns /// - /// Indicates whether the pending UI request was resolved by this call. + /// Full workflow run observability detail. /// ///
/// @@ -11368,34 +12860,34 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_sampling( + pub async fn get_run_detail( &self, - params: UIHandlePendingSamplingRequest, - ) -> Result { + params: WorkflowGetRunRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_UI_HANDLEPENDINGSAMPLING, + rpc_methods::SESSION_WORKFLOW_GETRUNDETAIL, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Resolves a pending `auto_mode_switch.requested` event with the user's accept/decline decision. + /// Pages durable progress for one dynamic workflow run. /// - /// Wire method: `session.ui.handlePendingAutoModeSwitch`. + /// Wire method: `session.workflow.getRunProgress`. /// /// # Parameters /// - /// * `params` - Request ID of a pending `auto_mode_switch.requested` event and the user's response. + /// * `params` - Parameters for paging workflow progress. /// /// # Returns /// - /// Indicates whether the pending UI request was resolved by this call. + /// A bidirectional page of workflow progress. /// ///
/// @@ -11404,34 +12896,34 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_auto_mode_switch( + pub async fn get_run_progress( &self, - params: UIHandlePendingAutoModeSwitchRequest, - ) -> Result { + params: WorkflowGetRunProgressRequest, + ) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_UI_HANDLEPENDINGAUTOMODESWITCH, + rpc_methods::SESSION_WORKFLOW_GETRUNPROGRESS, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Resolves a pending `session_limits_exhausted.requested` event with the user's selected limit action. + /// Requests cancellation of a dynamic workflow run and returns its run envelope. /// - /// Wire method: `session.ui.handlePendingSessionLimitsExhausted`. + /// Wire method: `session.workflow.cancel`. /// /// # Parameters /// - /// * `params` - Request ID of a pending `session_limits_exhausted.requested` event and the user's selected limit action. + /// * `params` - Parameters for cancelling a workflow run. /// /// # Returns /// - /// Indicates whether the pending UI request was resolved by this call. + /// Complete current or terminal workflow run envelope. /// ///
/// @@ -11440,34 +12932,28 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_session_limits_exhausted( - &self, - params: UIHandlePendingSessionLimitsExhaustedRequest, - ) -> Result { + pub async fn cancel(&self, params: WorkflowCancelRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_UI_HANDLEPENDINGSESSIONLIMITSEXHAUSTED, - Some(wire_params), - ) + .call(rpc_methods::SESSION_WORKFLOW_CANCEL, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Resolves a pending `exit_plan_mode.requested` event with the user's response. + /// Pauses a running dynamic workflow and returns its settled run envelope. /// - /// Wire method: `session.ui.handlePendingExitPlanMode`. + /// Wire method: `session.workflow.pause`. /// /// # Parameters /// - /// * `params` - Request ID of a pending `exit_plan_mode.requested` event and the user's response. + /// * `params` - Parameters for pausing a running workflow. /// /// # Returns /// - /// Indicates whether the pending UI request was resolved by this call. + /// Complete current or terminal workflow run envelope. /// ///
/// @@ -11476,30 +12962,24 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn handle_pending_exit_plan_mode( - &self, - params: UIHandlePendingExitPlanModeRequest, - ) -> Result { + pub async fn pause(&self, params: WorkflowPauseRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_UI_HANDLEPENDINGEXITPLANMODE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_WORKFLOW_PAUSE, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. + /// Atomically pauses an owned dynamic workflow attempt at a durable checkpoint. /// - /// Wire method: `session.ui.registerDirectAutoModeSwitchHandler`. + /// Wire method: `session.workflow.pauseAtCheckpoint`. /// - /// # Returns + /// # Parameters /// - /// Register an in-process handler for `auto_mode_switch.requested` events. The caller still attaches the actual listener via the standard event-subscription mechanism; this registration solely tells the server bridge to skip its own dispatch (so a remote client doesn't race the in-process handler for the same requestId). + /// * `params` - Parameters for an owned durable pause checkpoint. /// ///
/// @@ -11508,32 +12988,34 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn register_direct_auto_mode_switch_handler( + pub(crate) async fn pause_at_checkpoint( &self, - ) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + params: WorkflowPauseCheckpointRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() .call( - rpc_methods::SESSION_UI_REGISTERDIRECTAUTOMODESWITCHHANDLER, + rpc_methods::SESSION_WORKFLOW_PAUSEATCHECKPOINT, Some(wire_params), ) .await?; Ok(serde_json::from_value(_value)?) } - /// Unregisters a previously-registered in-process auto-mode-switch handler by its opaque handle. + /// Records a batch of ordered dynamic workflow progress lines. /// - /// Wire method: `session.ui.unregisterDirectAutoModeSwitchHandler`. + /// Wire method: `session.workflow.log`. /// /// # Parameters /// - /// * `params` - Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. + /// * `params` - Parameters for recording workflow progress. /// /// # Returns /// - /// Indicates whether the handle was active and the registration count was decremented. + /// Acknowledgement that a workflow request was accepted. /// ///
/// @@ -11542,38 +13024,28 @@ impl<'a> SessionRpcUi<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn unregister_direct_auto_mode_switch_handler( - &self, - params: UIUnregisterDirectAutoModeSwitchHandlerRequest, - ) -> Result { + pub async fn log(&self, params: WorkflowLogRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call( - rpc_methods::SESSION_UI_UNREGISTERDIRECTAUTOMODESWITCHHANDLER, - Some(wire_params), - ) + .call(rpc_methods::SESSION_WORKFLOW_LOG, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} -/// `session.usage.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcUsage<'a> { - pub(crate) session: &'a Session, -} - -impl<'a> SessionRpcUsage<'a> { - /// Gets accumulated usage metrics for the session. + /// Runs one dynamic-workflow-scoped subagent and returns its result. /// - /// Wire method: `session.usage.getMetrics`. + /// Wire method: `session.workflow.agent`. + /// + /// # Parameters + /// + /// * `params` - Parameters for one workflow-scoped subagent call. /// /// # Returns /// - /// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. + /// Result of one workflow-scoped subagent call. /// ///
/// @@ -11582,31 +13054,36 @@ impl<'a> SessionRpcUsage<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_metrics(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn agent(&self, params: WorkflowAgentRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_USAGE_GETMETRICS, Some(wire_params)) + .call(rpc_methods::SESSION_WORKFLOW_AGENT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } } -/// `session.visibility.*` RPCs. +/// `session.workflow.journal.*` RPCs. #[derive(Clone, Copy)] -pub struct SessionRpcVisibility<'a> { +pub struct SessionRpcWorkflowJournal<'a> { pub(crate) session: &'a Session, } -impl<'a> SessionRpcVisibility<'a> { - /// Returns the session's current Mission Control sharing status and shareable GitHub URL. Reflects whether the synced session is visible to repository readers ("repo") or restricted to its creator and collaborators ("unshared"). +impl<'a> SessionRpcWorkflowJournal<'a> { + /// Reads a memoized dynamic workflow journal entry. /// - /// Wire method: `session.visibility.get`. + /// Wire method: `session.workflow.journal.get`. + /// + /// # Parameters + /// + /// * `params` - Parameters for reading a workflow journal entry. /// /// # Returns /// - /// Current sharing status and shareable GitHub URL for a session. + /// Result of reading a workflow journal entry. /// ///
/// @@ -11615,27 +13092,31 @@ impl<'a> SessionRpcVisibility<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn get( + &self, + params: WorkflowJournalGetRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_VISIBILITY_GET, Some(wire_params)) + .call(rpc_methods::SESSION_WORKFLOW_JOURNAL_GET, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Sets the session's Mission Control sharing status, controlling whether the synced session is visible to repository readers. Returns the effective status and shareable GitHub URL after the change. + /// Stores a memoized dynamic workflow journal entry. /// - /// Wire method: `session.visibility.set`. + /// Wire method: `session.workflow.journal.put`. /// /// # Parameters /// - /// * `params` - Desired sharing status for the session. + /// * `params` - Parameters for storing a workflow journal entry. /// /// # Returns /// - /// Effective sharing status and shareable GitHub URL after updating session visibility. + /// Acknowledgement that a workflow request was accepted. /// ///
/// @@ -11644,13 +13125,13 @@ impl<'a> SessionRpcVisibility<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn set(&self, params: VisibilitySetRequest) -> Result { + pub async fn put(&self, params: WorkflowJournalPutRequest) -> Result { let mut wire_params = serde_json::to_value(params)?; wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); let _value = self .session .client() - .call(rpc_methods::SESSION_VISIBILITY_SET, Some(wire_params)) + .call(rpc_methods::SESSION_WORKFLOW_JOURNAL_PUT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index f34ee19e7b..553088a7c0 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -33,6 +33,8 @@ pub enum SessionEventType { SessionAutopilotObjectiveChanged, #[serde(rename = "session.info")] SessionInfo, + #[serde(rename = "session.indexed_search")] + SessionIndexedSearch, #[serde(rename = "session.warning")] SessionWarning, #[serde(rename = "session.model_change")] @@ -136,6 +138,8 @@ pub enum SessionEventType { ///
#[serde(rename = "session.fusion_completed")] SessionFusionCompleted, + #[serde(rename = "session.permission_recovery")] + SessionPermissionRecovery, #[serde(rename = "user.message")] UserMessage, #[serde(rename = "pending_messages.modified")] @@ -575,6 +579,8 @@ pub enum SessionEventData { SessionAutopilotObjectiveChanged(SessionAutopilotObjectiveChangedData), #[serde(rename = "session.info")] SessionInfo(SessionInfoData), + #[serde(rename = "session.indexed_search")] + SessionIndexedSearch(SessionIndexedSearchData), #[serde(rename = "session.warning")] SessionWarning(SessionWarningData), #[serde(rename = "session.model_change")] @@ -678,6 +684,8 @@ pub enum SessionEventData { ///
#[serde(rename = "session.fusion_completed")] SessionFusionCompleted(SessionFusionCompletedData), + #[serde(rename = "session.permission_recovery")] + SessionPermissionRecovery(SessionPermissionRecoveryData), #[serde(rename = "user.message")] UserMessage(UserMessageData), #[serde(rename = "pending_messages.modified")] @@ -1388,6 +1396,11 @@ pub struct SessionInfoData { pub url: Option, } +/// Session event "session.indexed_search". Transient indexed-search status and diagnostics from the live runtime service. Never persisted or used to infer activation from session history. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionIndexedSearchData {} + /// Session event "session.warning". Warning message for timeline display with categorization #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1999,6 +2012,18 @@ pub struct CompactionCompleteCompactionTokensUsed { pub output_tokens: Option, } +/// Original request-level and effective conversation reasoning effort for a Responses history boundary +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResponsesReasoning { + /// Effective effort selected before this message, independent of the response-level reasoning field + pub effort: String, + /// Original request-level effort, retained while replaying this conversation prefix + pub initial_effort: String, + /// Provider model whose reasoning settings this boundary records + pub model: String, +} + /// Session event "session.compaction_complete". Conversation compaction results including success status, metrics, and optional error details #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -2043,6 +2068,9 @@ pub struct SessionCompactionCompleteData { /// GitHub request tracing ID (x-github-request-id header) for the compaction LLM call #[serde(skip_serializing_if = "Option::is_none")] pub request_id: Option, + /// Reasoning baseline on the replacement summary, preserved when replay skips the compacted history + #[serde(skip_serializing_if = "Option::is_none")] + pub responses_reasoning: Option, /// Copilot service request ID (x-copilot-service-request-id header) for the compaction LLM call #[serde(skip_serializing_if = "Option::is_none")] pub service_request_id: Option, @@ -2071,10 +2099,74 @@ pub struct SessionCompactionCompleteData { pub trigger: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionRecoveryAttempt { + /// Unique identifier for this attempt record + pub attempt_id: String, + /// How the runtime handled this attempt + pub disposition: PermissionRecoveryAttemptDisposition, + /// One-based position of this attempt in the episode + pub ordinal: i64, + /// Controlled permission request kind, such as shell, path, URL, or tool + pub permission_kind: String, + /// Controlled reason for the attempt disposition + pub reason: PermissionRecoveryAttemptReason, + /// Relationship between this attempt and earlier attempts in the episode + pub relation: PermissionRecoveryAttemptRelation, + /// SHA-256 fingerprint of normalized request data; raw permission arguments are not included + pub request_fingerprint: String, + /// Tool-call identifier associated with this attempt, when available + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, +} + +/// Authoritative snapshot of an Autopilot permission-recovery episode +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionRecoveryData { + /// Ordered privacy-safe record of permission attempts and the successful alternative, when any + pub attempts: Vec, + /// Stable identifier shared by every transition in this recovery episode + pub episode_id: String, + /// Maximum number of distinct autonomous permission attempts allowed before escalation + pub max_attempts: i64, + /// Policy selected from the current client's response capability; mode or client changes may update it during recovery + pub on_blocked: PermissionRecoveryOnBlocked, + /// Controlled reason for the latest episode transition + pub reason: PermissionRecoveryReason, + /// Current lifecycle state of the recovery episode + pub status: PermissionRecoveryStatus, +} + +/// Structured reason that the task cannot continue without intervention +/// +///
+/// +/// **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 TaskBlocker { + /// Category of intervention that blocked the task + pub kind: TaskBlockerKind, + /// Permission-recovery episode that produced this blocker + pub permission_recovery: PermissionRecoveryData, + /// Controlled reason for the current blocked state + pub reason: PermissionRecoveryReason, + /// Whether a later user response or steering message can resume the task + pub resumable: bool, +} + /// Session event "session.task_complete". Task completion notification with summary from the agent #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionTaskCompleteData { + /// Structured blocker details when outcome is blocked + #[serde(skip_serializing_if = "Option::is_none")] + pub blocker: Option, /// Active autopilot objective ID evaluated by the completion reviewer #[serde(skip_serializing_if = "Option::is_none")] pub objective_id: Option, @@ -2382,6 +2474,24 @@ pub struct SessionFusionCompletedData { pub turn_id: String, } +/// Session event "session.permission_recovery". Authoritative snapshot of an Autopilot permission-recovery episode +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionPermissionRecoveryData { + /// Ordered privacy-safe record of permission attempts and the successful alternative, when any + pub attempts: Vec, + /// Stable identifier shared by every transition in this recovery episode + pub episode_id: String, + /// Maximum number of distinct autonomous permission attempts allowed before escalation + pub max_attempts: i64, + /// Policy selected from the current client's response capability; mode or client changes may update it during recovery + pub on_blocked: PermissionRecoveryOnBlocked, + /// Controlled reason for the latest episode transition + pub reason: PermissionRecoveryReason, + /// Current lifecycle state of the recovery episode + pub status: PermissionRecoveryStatus, +} + /// 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")] @@ -2412,6 +2522,9 @@ pub struct UserMessageData { /// Parent agent task ID for background telemetry correlated to this user turn #[serde(skip_serializing_if = "Option::is_none")] pub parent_agent_task_id: Option, + /// Responses reasoning settings anchored before this model-facing message, for cache-stable history replay + #[serde(skip_serializing_if = "Option::is_none")] + pub responses_reasoning: Option, /// Origin of this message, used for timeline filtering and attribution (e.g., `skill-pdf` for hidden skill injection or `agent-` for an inter-agent prompt) #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, @@ -3324,6 +3437,14 @@ pub struct AssistantUsageData { /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation #[serde(skip_serializing_if = "Option::is_none")] pub service_request_id: Option, + /// Number of prior thinking blocks the provider dropped while transforming the request + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) thinking_dropped_blocks: Option, + /// Recognized provider-reported reasons for dropped thinking blocks, in response order + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) thinking_dropped_reasons: Option>, /// Time to first token in milliseconds. Only available for streaming requests #[serde(skip_serializing_if = "Option::is_none")] pub time_to_first_token_ms: Option, @@ -4307,6 +4428,9 @@ pub struct SkillInvokedData { /// Whether model invocation is disabled for this skill #[serde(skip_serializing_if = "Option::is_none")] pub disable_model_invocation: Option, + /// Projected chat-message count when the skill was invoked. New writers persist this so replay does not need to reconstruct superseded history; readers derive it for legacy events when absent. + #[serde(skip_serializing_if = "Option::is_none")] + pub invoked_at_turn: Option, /// Model identifier active when the skill was invoked, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, @@ -4345,6 +4469,9 @@ pub struct SkillInvokedRefData { /// Whether model invocation is disabled for this skill #[serde(skip_serializing_if = "Option::is_none")] pub disable_model_invocation: Option, + /// Projected chat-message count when the skill was invoked. Preserved from the inline event data when the authored body is deduplicated. + #[serde(skip_serializing_if = "Option::is_none")] + pub invoked_at_turn: Option, /// Model identifier active when the skill was invoked, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, @@ -4692,6 +4819,9 @@ pub struct SystemNotificationData { pub content: String, /// Structured metadata identifying what triggered this notification pub kind: serde_json::Value, + /// Responses reasoning settings anchored before this model-facing message, for cache-stable history replay + #[serde(skip_serializing_if = "Option::is_none")] + pub responses_reasoning: Option, } /// A parsed command identifier in a shell permission request, including whether it is read-only. @@ -4922,6 +5052,21 @@ pub struct PermissionRequestUrl { pub url: String, } +/// Bounded runtime attribution, independent of free-text rationale. Telemetry revalidates this vocabulary before standard collection. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionApprovalEvaluation { + /// Stage that produced this attribution. + pub evaluation_stage: PermissionApprovalEvaluationEvaluationStage, + /// Whether the request invoked the judge interface. A cached recommendation retains the original attempt fact. Omitted means unknown, including inherited outcomes. + #[serde(skip_serializing_if = "Option::is_none")] + pub judge_attempted: Option, + /// Status of the local judge interface, not proof of a model network call. + pub judge_status: PermissionApprovalEvaluationJudgeStatus, + /// Machine-readable runtime gate reason, never a command, path or human rationale. + pub reason_code: PermissionApprovalEvaluationReasonCode, +} + /// Assisted-approval judge information attached to a permission request. Present only in assisted mode; its absence means the judge did not evaluate the request. The `recommendation` conveys the judge's disposition for this request. /// ///
@@ -4933,6 +5078,9 @@ pub struct PermissionRequestUrl { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PermissionAssistedApproval { + /// Runtime reason and judge-call metadata. Absent on older events; missing metadata means unknown, not that the judge was skipped. + #[serde(skip_serializing_if = "Option::is_none")] + pub evaluation: Option, /// Classified cause of an `error` recommendation. Absent for every other recommendation. #[serde(skip_serializing_if = "Option::is_none")] pub failure_reason: Option, @@ -5625,11 +5773,17 @@ pub struct PermissionRequestedData { /// Agent mode captured from the owning turn when permission evaluation began. #[serde(skip_serializing_if = "Option::is_none")] pub agent_mode: Option, + /// Permission mode captured when evaluation began. Absent on historical events. + #[serde(skip_serializing_if = "Option::is_none")] + pub permission_mode: Option, /// Details of the permission being requested pub permission_request: PermissionRequest, /// Derived user-facing permission prompt details for UI consumers #[serde(skip_serializing_if = "Option::is_none")] pub prompt_request: Option, + /// Permission-recovery episode that authorized this request to surface for interactive attention + #[serde(skip_serializing_if = "Option::is_none")] + pub recovery_episode_id: Option, /// Unique identifier for this permission request; used to respond via session.respondToPermission() pub request_id: RequestId, /// When true, this permission was already resolved by a permissionRequest hook and requires no client action @@ -5862,6 +6016,9 @@ pub struct PermissionDeniedByPermissionRequestHook { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PermissionCompletedData { + /// Atomic structured blocked outcome when this permission response ended an Autopilot recovery episode unsuccessfully + #[serde(skip_serializing_if = "Option::is_none")] + pub blocker: Option, /// Who decided this permission request. Absent on completions recorded before this field existed, which consumers must treat as "not a human decision" rather than assuming one. Authorization records are minted only for `human_response`; an assisted-approval verdict, a host policy, an unattended fallback, and a hook resolution all produce the same `result` a person does, so this is the only field that distinguishes them. /// ///
@@ -5872,6 +6029,9 @@ pub struct PermissionCompletedData { ///
#[serde(skip_serializing_if = "Option::is_none")] pub decision_source: Option, + /// Permission-recovery episode settled by this response, when the request was escalated by Autopilot + #[serde(skip_serializing_if = "Option::is_none")] + pub recovery_episode_id: Option, /// Request ID of the resolved permission request; clients should dismiss any UI for this request pub request_id: RequestId, /// The result of the permission request @@ -7516,6 +7676,9 @@ pub enum ModelChangeSource { /// The runtime selected the model automatically, such as rate-limit recovery or refusal fallback. #[serde(rename = "automatic")] Automatic, + /// The user selected the promoted model from the changeboarding card or its keyboard shortcut. + #[serde(rename = "changeboarding_shortcut")] + ChangeboardingShortcut, /// An SDK or RPC caller selected the model. #[serde(rename = "sdk")] Sdk, @@ -7676,6 +7839,159 @@ pub enum CompactionTrigger { Unknown, } +/// Category of structured task blocker +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskBlockerKind { + /// Autopilot permission recovery requires intervention or has no safe autonomous path. + #[serde(rename = "permission_recovery")] + PermissionRecovery, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Runtime handling applied to a recovery attempt +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionRecoveryAttemptDisposition { + /// The request was denied without prompting so the agent could try an alternative. + #[serde(rename = "deferred")] + Deferred, + /// The request was surfaced to an interactive responder. + #[serde(rename = "prompted")] + Prompted, + /// The interactive responder approved the request. + #[serde(rename = "approved")] + Approved, + /// The interactive responder denied the request or became unavailable. + #[serde(rename = "denied")] + Denied, + /// The request exhausted unattended recovery and produced a blocked outcome. + #[serde(rename = "blocked")] + Blocked, + /// A tool call succeeded as an equivalent alternative. + #[serde(rename = "succeeded")] + Succeeded, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Controlled reason for an individual attempt disposition +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionRecoveryAttemptReason { + /// The attempt required permission that Assisted Permissions could not grant. + #[serde(rename = "permission_required")] + PermissionRequired, + /// The request repeated an earlier attempt. + #[serde(rename = "repeated_attempt")] + RepeatedAttempt, + /// The request exceeded the bounded number of distinct attempts. + #[serde(rename = "attempts_exhausted")] + AttemptsExhausted, + /// The interactive responder approved the request. + #[serde(rename = "permission_approved")] + PermissionApproved, + /// The interactive responder denied the request. + #[serde(rename = "permission_denied")] + PermissionDenied, + /// The interactive responder became unavailable. + #[serde(rename = "responder_unavailable")] + ResponderUnavailable, + /// The tool call succeeded without the blocked permission. + #[serde(rename = "equivalent_alternative_succeeded")] + EquivalentAlternativeSucceeded, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Relationship of an attempt to earlier permission requests +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionRecoveryAttemptRelation { + /// The first denied permission request in the episode. + #[serde(rename = "initial")] + Initial, + /// A request equivalent to an earlier attempt. + #[serde(rename = "retry")] + Retry, + /// A distinct request or a successful alternative tool call. + #[serde(rename = "alternative")] + Alternative, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Action selected when autonomous recovery cannot continue +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionRecoveryOnBlocked { + /// Surface the existing permission prompt to a response-capable client. + #[serde(rename = "ask")] + Ask, + /// Return a structured unsuccessful blocked outcome because no responder is available. + #[serde(rename = "fail")] + Fail, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Controlled reason for a permission-recovery episode transition +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionRecoveryReason { + /// An action required permission that Assisted Permissions could not grant. + #[serde(rename = "permission_required")] + PermissionRequired, + /// The agent repeated an equivalent permission request instead of making progress. + #[serde(rename = "repeated_attempt")] + RepeatedAttempt, + /// The bounded number of distinct permission attempts was exhausted. + #[serde(rename = "attempts_exhausted")] + AttemptsExhausted, + /// A responder approved the escalated permission request. + #[serde(rename = "permission_approved")] + PermissionApproved, + /// A responder denied the escalated permission request. + #[serde(rename = "permission_denied")] + PermissionDenied, + /// The response-capable client became unavailable while escalation was pending. + #[serde(rename = "responder_unavailable")] + ResponderUnavailable, + /// A later tool call succeeded without requiring the blocked permission. + #[serde(rename = "equivalent_alternative_succeeded")] + EquivalentAlternativeSucceeded, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Lifecycle state of a permission-recovery episode +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionRecoveryStatus { + /// Autopilot may try a bounded equivalent alternative. + #[serde(rename = "recovering")] + Recovering, + /// An interactive permission response is required. + #[serde(rename = "awaiting_approval")] + AwaitingApproval, + /// The episode ended through approval or a successful equivalent alternative. + #[serde(rename = "resolved")] + Resolved, + /// No autonomous path remains and the task requires intervention. + #[serde(rename = "blocked")] + Blocked, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Semantic result of evaluating a task completion request #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum TaskCompletionOutcome { @@ -8622,6 +8938,144 @@ pub enum PermissionRequestMemoryAction { Unknown, } +/// Stage that produced this attribution. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionApprovalEvaluationEvaluationStage { + /// The attribution stage is unknown. + #[serde(rename = "unknown")] + UnknownValue, + /// The request resolved before assisted-approval evaluation. + #[serde(rename = "not_reached")] + NotReached, + /// A runtime gate skipped the judge. + #[serde(rename = "pre_judge")] + PreJudge, + /// The judge interface produced the evaluation. + #[serde(rename = "judge")] + Judge, + /// A cached recommendation or another request's outcome was reused. + #[serde(rename = "reuse")] + Reuse, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Status of the local judge interface, not proof of a model network call. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionApprovalEvaluationJudgeStatus { + /// No authoritative attribution is available. + #[serde(rename = "unknown")] + UnknownValue, + /// This evaluation did not invoke the judge interface. + #[serde(rename = "not_called")] + NotCalled, + /// The judge interface returned a usable verdict. + #[serde(rename = "completed")] + Completed, + /// The judge interface returned an error. + #[serde(rename = "failed")] + Failed, + /// This evaluation reused a cached recommendation. + #[serde(rename = "cached")] + Cached, + /// This request inherited another decision without local judge attribution. + #[serde(rename = "inherited")] + Inherited, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Machine-readable runtime gate reason, never a command, path or human rationale. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionApprovalEvaluationReasonCode { + /// Attribution is missing or outside the supported vocabulary. + #[serde(rename = "unknown")] + UnknownValue, + /// The request resolved before assisted-approval evaluation. + #[serde(rename = "not-reached")] + NotReached, + /// Assisted approval was inactive for this request. + #[serde(rename = "inactive")] + Inactive, + /// The judge was skipped because authorization extraction could not safely establish a complete recent history. + #[serde(rename = "authorization-history-incomplete")] + AuthorizationHistoryIncomplete, + /// Managed policy required a human decision. + #[serde(rename = "managed-approval-required")] + ManagedApprovalRequired, + /// The request asked to bypass sandbox restrictions. + #[serde(rename = "sandbox-bypass")] + SandboxBypass, + /// An action field exceeded the judge input limit. + #[serde(rename = "action-too-long")] + ActionTooLong, + /// The script path was not authorized for inspection. + #[serde(rename = "path-not-authorized")] + PathNotAuthorized, + /// The script working directory was invalid. + #[serde(rename = "invalid-working-directory")] + InvalidWorkingDirectory, + /// The script snapshot could not be read. + #[serde(rename = "unreadable")] + Unreadable, + /// The script path was not a regular file. + #[serde(rename = "not-regular-file")] + NotRegularFile, + /// The script snapshot exceeded the size limit. + #[serde(rename = "too-large")] + TooLarge, + /// The script snapshot was not UTF-8. + #[serde(rename = "non-utf8")] + NonUtf8, + /// The script interpreter could not be inspected. + #[serde(rename = "interpreter-unavailable")] + InterpreterUnavailable, + /// The interpreter snapshot exceeded the size limit. + #[serde(rename = "interpreter-too-large")] + InterpreterTooLarge, + /// The shell environment could not be reviewed. + #[serde(rename = "shell-environment-unreviewable")] + ShellEnvironmentUnreviewable, + /// A script path could not be represented for review. + #[serde(rename = "unrepresentable-path")] + UnrepresentablePath, + /// An interpreter wrapped a script that could not be reviewed. + #[serde(rename = "interpreter-wrapped-script")] + InterpreterWrappedScript, + /// The script invocation could not be reviewed. + #[serde(rename = "unreviewable-script-invocation")] + UnreviewableScriptInvocation, + /// The script argument binding could not be reviewed. + #[serde(rename = "argument-binding-unreviewable")] + ArgumentBindingUnreviewable, + /// The script review metadata was malformed. + #[serde(rename = "malformed-script-action-review")] + MalformedScriptActionReview, + /// The script snapshot manifest was malformed. + #[serde(rename = "malformed-script-action-manifest")] + MalformedScriptActionManifest, + /// Script review was unavailable. + #[serde(rename = "unavailable")] + Unavailable, + /// The judge interface returned a usable verdict. + #[serde(rename = "judge-verdict")] + JudgeVerdict, + /// The judge interface returned an error. + #[serde(rename = "judge-error")] + JudgeError, + /// The request inherited an outcome from another decision. + #[serde(rename = "inherited")] + Inherited, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Why the assisted-approval judge produced no usable recommendation. Present only alongside an `error` recommendation, where the human-readable reason is a fixed string and therefore cannot distinguish these cases. Intended to make a judge failure reportable by a consumer that has no access to the host's logs. /// ///
diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index 48e6090aed..e9dbb997bb 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -268,10 +268,13 @@ pub struct JsonRpcClient { notification_tx: broadcast::Sender, request_tx: mpsc::UnboundedSender, connection_closed: CancellationToken, + server_message_handler: Arc>>, read_task: Mutex>>, write_task: Mutex>>, } +type ServerMessageHandler = Box bool + Send + Sync>; + impl JsonRpcClient { /// Create a new client from async read/write streams. /// @@ -297,6 +300,7 @@ impl JsonRpcClient { notification_tx, request_tx, connection_closed: CancellationToken::new(), + server_message_handler: Arc::new(RwLock::new(None)), read_task: Mutex::new(None), write_task: Mutex::new(Some(write_task)), }; @@ -305,6 +309,7 @@ impl JsonRpcClient { let notification_tx_clone = client.notification_tx.clone(); let request_tx_clone = client.request_tx.clone(); let connection_closed = client.connection_closed.clone(); + let server_message_handler = client.server_message_handler.clone(); let reader_span = tracing::error_span!("jsonrpc_read_loop"); let read_task = tokio::spawn( @@ -314,6 +319,7 @@ impl JsonRpcClient { pending_requests, notification_tx_clone, request_tx_clone, + server_message_handler, ) .await; connection_closed.cancel(); @@ -340,6 +346,12 @@ impl JsonRpcClient { self.connection_closed.child_token() } + /// Install nonblocking bookkeeping that must preserve request/notification + /// wire order. Return true to consume a message instead of routing it. + pub(crate) fn set_server_message_handler(&self, handler: ServerMessageHandler) { + *self.server_message_handler.write() = Some(handler); + } + /// Writer-actor task. Owns the `AsyncWrite`, drains the command queue, /// and writes each frame atomically (header + body + flush) before /// signaling the ack. @@ -376,75 +388,85 @@ impl JsonRpcClient { pending_requests: Arc>>, notification_tx: broadcast::Sender, request_tx: mpsc::UnboundedSender, + server_message_handler: Arc>>, ) { let mut reader = BufReader::new(reader); loop { match Self::read_message(&mut reader).await { - Ok(Some(message)) => match message { - JsonRpcMessage::Response(mut response) => { - let id = response.id; - let pending = pending_requests.write().remove(&id); - if let Some(PendingRequest { - sender, - inline_callback, - }) = pending - { - // Run the inline callback synchronously on the - // read loop so any state it mutates (e.g. - // registering a server-assigned session id with - // the router) is visible before the loop reads - // and dispatches the next message. - if let Some(cb) = inline_callback - && response.error.is_none() + Ok(Some(message)) => { + if !matches!(message, JsonRpcMessage::Response(_)) + && server_message_handler + .read() + .as_ref() + .is_some_and(|handler| handler(&message)) + { + continue; + } + match message { + JsonRpcMessage::Response(mut response) => { + let id = response.id; + let pending = pending_requests.write().remove(&id); + if let Some(PendingRequest { + sender, + inline_callback, + }) = pending { - let cb_outcome = - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - cb(&response) - })); - match cb_outcome { - Ok(Ok(())) => {} - Ok(Err(error)) => { - response.result = None; - response.error = Some(JsonRpcError { - code: -32603, - message: error.to_string(), - data: None, - }); - } - Err(panic) => { - let message = panic - .downcast_ref::<&'static str>() - .map(|s| (*s).to_string()) - .or_else(|| panic.downcast_ref::().cloned()) - .unwrap_or_else(|| { - "inline response callback panicked".to_string() + // Run the inline callback synchronously on the + // read loop so any state it mutates (e.g. + // registering a server-assigned session id with + // the router) is visible before the loop reads + // and dispatches the next message. + if let Some(cb) = inline_callback + && response.error.is_none() + { + let cb_outcome = std::panic::catch_unwind( + std::panic::AssertUnwindSafe(|| cb(&response)), + ); + match cb_outcome { + Ok(Ok(())) => {} + Ok(Err(error)) => { + response.result = None; + response.error = Some(JsonRpcError { + code: -32603, + message: error.to_string(), + data: None, + }); + } + Err(panic) => { + let message = panic + .downcast_ref::<&'static str>() + .map(|s| (*s).to_string()) + .or_else(|| panic.downcast_ref::().cloned()) + .unwrap_or_else(|| { + "inline response callback panicked".to_string() + }); + response.result = None; + response.error = Some(JsonRpcError { + code: -32603, + message, + data: None, }); - response.result = None; - response.error = Some(JsonRpcError { - code: -32603, - message, - data: None, - }); + } } } + if sender.send(response).is_err() { + warn!(request_id = %id, "failed to send response for request"); + } + } else { + warn!(request_id = %id, "received response for unknown request id"); } - if sender.send(response).is_err() { - warn!(request_id = %id, "failed to send response for request"); - } - } else { - warn!(request_id = %id, "received response for unknown request id"); } - } - JsonRpcMessage::Notification(notification) => { - let _ = notification_tx.send(notification); - } - JsonRpcMessage::Request(request) => { - if request_tx.send(request).is_err() { - warn!("failed to forward JSON-RPC request, channel closed"); + JsonRpcMessage::Notification(notification) => { + let _ = notification_tx.send(notification); + } + JsonRpcMessage::Request(request) => { + if request_tx.send(request).is_err() { + warn!("failed to forward JSON-RPC request, channel closed"); + } } } - }, + } Ok(None) => { break; } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 7e0be532d5..6786834e01 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,6 +3,8 @@ #![deny(rustdoc::broken_intra_doc_links)] #![cfg_attr(test, allow(clippy::unwrap_used))] +/// Transport-neutral native Agent Host Protocol endpoints. +pub mod ahp; #[cfg(not(feature = "bundled-cli"))] mod cache_paths; /// Canvas declarations, provider callbacks, and host-side canvas RPC types. @@ -2829,6 +2831,7 @@ impl Client { info!(pid = ?pid, "stopping CLI process"); let mut errors: Vec = Vec::new(); self.inner.extension_launch_provider.clear(); + self.inner.router.ahp.clear(); // Snapshot the registered session IDs without holding the router // lock across the detach RPCs. @@ -3030,6 +3033,7 @@ impl Client { impl Drop for ClientInner { fn drop(&mut self) { + self.router.ahp.clear(); let pid = self.child.lock().as_ref().and_then(Child::id); if let Some(process_tree) = self.process_tree.lock().take() && let Err(error) = process_tree.terminate() diff --git a/rust/src/router.rs b/rust/src/router.rs index ce83d5f3ed..67fe3524a0 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -45,6 +45,7 @@ struct SessionSenders { /// /// Internal to the SDK — consumers interact via `Client::register_session()`. pub(crate) struct SessionRouter { + pub(crate) ahp: Arc, sessions: Arc>>, next_token: AtomicU64, started: Mutex, @@ -53,6 +54,7 @@ pub(crate) struct SessionRouter { impl SessionRouter { pub(crate) fn new() -> Self { Self { + ahp: Arc::new(crate::ahp::Registry::default()), sessions: Arc::new(Mutex::new(HashMap::new())), next_token: AtomicU64::new(0), started: Mutex::new(false), @@ -137,6 +139,7 @@ impl SessionRouter { /// Used by [`Client::force_stop`](crate::Client::force_stop) to release /// per-session state without waiting for graceful unregistration. pub(crate) fn clear(&self) { + self.ahp.clear(); self.sessions.lock().clear(); } diff --git a/rust/tests/ahp_test.rs b/rust/tests/ahp_test.rs new file mode 100644 index 0000000000..c3066eeca4 --- /dev/null +++ b/rust/tests/ahp_test.rs @@ -0,0 +1,683 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +#![cfg(feature = "test-support")] +#![allow(clippy::unwrap_used)] + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +use github_copilot_sdk::ahp::*; +use github_copilot_sdk::test_support::{ + JsonRpcClient, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, +}; +use github_copilot_sdk::{Client, Error, ErrorKind}; +use serde_json::{Value, json}; +use tokio::sync::{Mutex, Notify, broadcast, mpsc}; +use tokio::time::timeout; + +const WAIT: Duration = Duration::from_secs(3); + +struct Peer { + client: Client, + rpc: Arc, + calls: Arc>>, + hold_method: Arc>>, + send_started: Arc, + release_send: Arc, +} + +impl Peer { + fn new(unsupported: bool) -> Self { + let (sdk, runtime) = tokio::io::duplex(1024 * 1024); + let (read, write) = tokio::io::split(sdk); + let client = Client::from_streams(read, write, std::env::temp_dir()).unwrap(); + let (read, write) = tokio::io::split(runtime); + let (requests, mut rx) = mpsc::unbounded_channel(); + let rpc = Arc::new(JsonRpcClient::new( + write, + read, + broadcast::channel(64).0, + requests, + )); + let calls = Arc::new(Mutex::new(Vec::new())); + let hold_method = Arc::new(Mutex::new(None)); + let send_started = Arc::new(Notify::new()); + let release_send = Arc::new(Notify::new()); + tokio::spawn({ + let rpc = rpc.clone(); + let calls = calls.clone(); + let hold_method = hold_method.clone(); + let send_started = send_started.clone(); + let release_send = release_send.clone(); + async move { + let mut next_id = 0; + while let Some(request) = rx.recv().await { + calls.lock().await.push(( + request.method.clone(), + request.params.clone().unwrap_or(Value::Null), + )); + let held = hold_method.lock().await.clone(); + if held.as_deref() == Some(request.method.as_str()) { + send_started.notify_one(); + release_send.notified().await; + } + let result = match request.method.as_str() { + "ahp.registerEndpoint" => json!({"endpointId": "endpoint"}), + "ahp.openConnection" => { + next_id += 1; + json!({"connectionId": format!("connection-{next_id}")}) + } + "ping" => { + json!({"message": "pong", "timestamp": "2026-01-01T00:00:00Z", "protocolVersion": 3}) + } + _ => Value::Null, + }; + let error = if unsupported { + Some(github_copilot_sdk::test_support::JsonRpcResponse { + jsonrpc: "2.0".into(), + id: request.id, + result: None, + error: serde_json::from_value( + json!({"code": -32601, "message": "not found"}), + ) + .unwrap(), + }) + } else { + None + }; + rpc.write(&error.unwrap_or(JsonRpcResponse { + jsonrpc: "2.0".into(), + id: request.id, + result: Some(result), + error: None, + })) + .await + .unwrap(); + } + } + }); + Self { + client, + rpc, + calls, + hold_method, + send_started, + release_send, + } + } + + async fn callback(&self, method: &str, params: Value) -> JsonRpcResponse { + timeout(WAIT, self.rpc.send_request(method, Some(params))) + .await + .unwrap() + .unwrap() + } + + async fn endpoint(&self, options: AhpEndpointOptions) -> AhpEndpoint { + timeout(WAIT, self.client.create_ahp_endpoint(options)) + .await + .unwrap() + .unwrap() + } +} + +impl Drop for Peer { + fn drop(&mut self) { + self.client.force_stop(); + } +} + +fn messages() -> (AhpConnectionOptions, mpsc::Receiver) { + let (tx, rx) = mpsc::channel(128); + ( + AhpConnectionOptions { + on_message: Arc::new(move |message| { + let tx = tx.clone(); + Box::pin(async move { + tx.send(message) + .await + .map_err(|e| Error::with_message(ErrorKind::Io, e.to_string())) + }) + }), + on_close: None, + }, + rx, + ) +} + +#[tokio::test] +async fn opaque_transport_and_generated_wire_contracts() { + let peer = Peer::new(false); + let endpoint = peer.endpoint(AhpEndpointOptions::default()).await; + let (options, mut received) = messages(); + let connection = endpoint.open_connection(options).await.unwrap(); + let opaque = "{\"notDecoded\":\"🚀\"}"; + connection.send(opaque).await.unwrap(); + let response = peer + .callback( + "ahp.message", + json!({ + "endpointId": endpoint.id(), "connectionId": connection.id(), "message": opaque, + }), + ) + .await; + assert!(response.error.is_none()); + assert_eq!(received.recv().await.unwrap(), opaque); + endpoint + .set_capabilities(json!({"custom": true})) + .await + .unwrap(); + endpoint.refresh_exposure().await.unwrap(); + connection.close().await.unwrap(); + connection.close().await.unwrap(); + endpoint.dispose().await.unwrap(); + endpoint.dispose().await.unwrap(); + assert!(connection.send("late").await.is_err()); + let calls = peer.calls.lock().await; + assert_eq!( + calls[0].1, + json!({"callbacks": { + "createSession": false, "resumeSession": false, "listSessions": false, "sessionControl": false, + }}) + ); + assert_eq!( + calls.iter().find(|c| c.0 == "ahp.send").unwrap().1["message"], + opaque + ); + assert_eq!( + calls + .iter() + .filter(|c| c.0 == "ahp.closeConnection") + .count(), + 1 + ); + assert_eq!( + calls + .iter() + .filter(|c| c.0 == "ahp.disposeEndpoint") + .count(), + 1 + ); +} + +#[tokio::test] +async fn callbacks_are_reentrant_and_keep_policy_local() { + let peer = Peer::new(false); + let client = peer.client.clone(); + let endpoint = peer + .endpoint(AhpEndpointOptions { + on_create_session: Some(Arc::new(move |request, _| { + let client = client.clone(); + Box::pin(async move { + client.ping(None).await?; + assert!( + client + .create_ahp_endpoint(AhpEndpointOptions::default()) + .await + .is_err() + ); + Ok(AhpSessionIdentity { + session_id: request.requested_session_id, + }) + }) + })), + on_resume_session: Some(Arc::new(|request, _| { + Box::pin(async move { + Ok(AhpSessionIdentity { + session_id: request.session_id, + }) + }) + })), + on_list_sessions: Some(Arc::new(|(), _| { + Box::pin(async { + Ok(vec![AhpSessionIdentity { + session_id: "visible".into(), + }]) + }) + })), + on_session_control: Some(Arc::new(|request, _| { + Box::pin(async move { + assert_eq!(request.kind, "setRemoteControl"); + Ok(AhpSessionControlResult { + applied: false, + reason: Some("owner refused".into()), + result: Some(json!({"steerable": false})), + }) + }) + })), + ..Default::default() + }) + .await; + let connection = endpoint.open_connection(messages().0).await.unwrap(); + for (method, extra) in [ + ( + "ahp.createSession", + json!({"requestedSessionId": "visible"}), + ), + ("ahp.resumeSession", json!({"sessionId": "visible"})), + ] { + let mut params = extra; + params["endpointId"] = json!(endpoint.id()); + params["connectionId"] = json!(connection.id()); + let response = peer.callback(method, params).await; + assert_eq!(response.result.unwrap()["sessionId"], "visible"); + } + assert_eq!( + peer.callback("ahp.listSessions", json!({"endpointId": endpoint.id()})) + .await + .result + .unwrap(), + json!({"sessionIds": ["visible"]}) + ); + assert_eq!(peer.callback("ahp.sessionControl", json!({ + "endpointId": endpoint.id(), "sessionId": "visible", "kind": "setRemoteControl", "payload": {}, + })).await.result.unwrap()["result"]["steerable"], false); + endpoint.dispose().await.unwrap(); + assert!( + peer.callback("ahp.listSessions", json!({"endpointId": endpoint.id()})) + .await + .error + .is_some() + ); +} + +#[tokio::test] +async fn output_ack_waits_for_delivery_and_connections_are_independent() { + let peer = Peer::new(false); + let endpoint = peer.endpoint(AhpEndpointOptions::default()).await; + let entered = Arc::new(Notify::new()); + let release = Arc::new(Notify::new()); + let first = endpoint + .open_connection(AhpConnectionOptions { + on_message: Arc::new({ + let entered = entered.clone(); + let release = release.clone(); + move |_| { + let entered = entered.clone(); + let release = release.clone(); + Box::pin(async move { + entered.notify_one(); + release.notified().await; + Ok(()) + }) + } + }), + on_close: None, + }) + .await + .unwrap(); + let (options, mut messages) = messages(); + let second = endpoint.open_connection(options).await.unwrap(); + let delivery = tokio::spawn({ + let rpc = peer.rpc.clone(); + let params = + json!({"endpointId": endpoint.id(), "connectionId": first.id(), "message": "held"}); + async move { rpc.send_request("ahp.message", Some(params)).await.unwrap() } + }); + timeout(WAIT, entered.notified()).await.unwrap(); + assert!(!delivery.is_finished()); + assert!( + peer.callback( + "ahp.message", + json!({ + "endpointId": endpoint.id(), "connectionId": second.id(), "message": "independent", + }) + ) + .await + .error + .is_none() + ); + assert_eq!(messages.recv().await.unwrap(), "independent"); + release.notify_one(); + assert!( + timeout(WAIT, delivery) + .await + .unwrap() + .unwrap() + .error + .is_none() + ); + endpoint.dispose().await.unwrap(); +} + +#[tokio::test] +async fn input_bounds_include_inflight_utf8_bytes_and_close_only_that_connection() { + let peer = Peer::new(false); + let endpoint = peer + .endpoint(AhpEndpointOptions { + limits: AhpLimits { + max_message_bytes: 4, + max_queued_messages: 1, + max_buffered_bytes: 4, + }, + ..Default::default() + }) + .await; + let first = endpoint.open_connection(messages().0).await.unwrap(); + let second = endpoint.open_connection(messages().0).await.unwrap(); + *peer.hold_method.lock().await = Some("ahp.send".into()); + let send = tokio::spawn({ + let first = first.clone(); + async move { first.send("🚀").await } + }); + timeout(WAIT, peer.send_started.notified()).await.unwrap(); + assert!( + first + .send("a") + .await + .unwrap_err() + .to_string() + .contains("buffer limit") + ); + assert!(timeout(WAIT, send).await.unwrap().unwrap().is_err()); + *peer.hold_method.lock().await = None; + peer.release_send.notify_one(); + second.send("ok").await.unwrap(); + assert!( + second + .send("🚀x") + .await + .unwrap_err() + .to_string() + .contains("size limit") + ); + endpoint.dispose().await.unwrap(); +} + +#[tokio::test] +async fn cancellation_and_endpoint_shutdown_abort_policy_without_blocking_router() { + let peer = Peer::new(false); + let entered = Arc::new(Notify::new()); + let cancellation = Arc::new(Mutex::new(None)); + let endpoint = peer + .endpoint(AhpEndpointOptions { + on_list_sessions: Some(Arc::new({ + let entered = entered.clone(); + let cancellation = cancellation.clone(); + move |(), context| { + let entered = entered.clone(); + let cancellation = cancellation.clone(); + Box::pin(async move { + *cancellation.lock().await = Some(context.cancellation); + entered.notify_one(); + std::future::pending().await + }) + } + })), + ..Default::default() + }) + .await; + let pending = tokio::spawn({ + let rpc = peer.rpc.clone(); + async move { + rpc.send_request("ahp.listSessions", Some(json!({"endpointId": "endpoint"}))) + .await + .unwrap() + } + }); + timeout(WAIT, entered.notified()).await.unwrap(); + peer.rpc + .write(&JsonRpcNotification { + jsonrpc: "2.0".into(), + method: "$/cancelRequest".into(), + params: Some(json!({"id": 1})), + }) + .await + .unwrap(); + let response = timeout(WAIT, pending).await.unwrap().unwrap(); + assert_eq!(response.error.unwrap().code, -32800); + assert!(cancellation.lock().await.as_ref().unwrap().is_cancelled()); + endpoint.dispose().await.unwrap(); +} + +#[tokio::test] +async fn shutdown_notifies_once_and_releases_pending_delivery() { + let peer = Peer::new(false); + let endpoint = peer.endpoint(AhpEndpointOptions::default()).await; + let closed = Arc::new(AtomicUsize::new(0)); + let entered = Arc::new(Notify::new()); + let connection = endpoint + .open_connection(AhpConnectionOptions { + on_message: Arc::new({ + let entered = entered.clone(); + move |_| { + let entered = entered.clone(); + Box::pin(async move { + entered.notify_one(); + std::future::pending().await + }) + } + }), + on_close: Some(Arc::new({ + let closed = closed.clone(); + move |_| { + closed.fetch_add(1, Ordering::SeqCst); + } + })), + }) + .await + .unwrap(); + let pending = tokio::spawn({ + let rpc = peer.rpc.clone(); + let params = json!({"endpointId": endpoint.id(), "connectionId": connection.id(), "message": "pending"}); + async move { rpc.send_request("ahp.message", Some(params)).await.unwrap() } + }); + timeout(WAIT, entered.notified()).await.unwrap(); + endpoint.dispose().await.unwrap(); + assert!( + timeout(WAIT, pending) + .await + .unwrap() + .unwrap() + .error + .is_some() + ); + connection.close().await.unwrap(); + assert_eq!(closed.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn rejects_invalid_limits_and_reports_unsupported_runtime() { + let peer = Peer::new(true); + let result = peer + .client + .create_ahp_endpoint(AhpEndpointOptions { + limits: AhpLimits { + max_queued_messages: 129, + ..Default::default() + }, + ..Default::default() + }) + .await; + assert!(result.is_err()); + assert!(peer.calls.lock().await.is_empty()); + let result = peer + .client + .create_ahp_endpoint(AhpEndpointOptions::default()) + .await; + assert!( + result + .err() + .unwrap() + .to_string() + .contains("does not support native AHP") + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn output_delivery_preserves_wire_order() { + let peer = Peer::new(false); + let endpoint = peer.endpoint(AhpEndpointOptions::default()).await; + let (options, mut messages) = messages(); + let connection = endpoint.open_connection(options).await.unwrap(); + for id in 100..200 { + peer.rpc.write(&JsonRpcRequest::new(id, "ahp.message", Some(json!({ + "endpointId": endpoint.id(), "connectionId": connection.id(), "message": id.to_string(), + })))).await.unwrap(); + } + for id in 100..200 { + assert_eq!( + timeout(WAIT, messages.recv()).await.unwrap().unwrap(), + id.to_string() + ); + } + endpoint.dispose().await.unwrap(); +} + +#[tokio::test] +async fn output_overflow_cancels_inflight_delivery_and_notifies_once() { + let peer = Peer::new(false); + let endpoint = peer + .endpoint(AhpEndpointOptions { + limits: AhpLimits { + max_queued_messages: 1, + ..Default::default() + }, + ..Default::default() + }) + .await; + let entered = Arc::new(Notify::new()); + let closes = Arc::new(AtomicUsize::new(0)); + let connection = endpoint + .open_connection(AhpConnectionOptions { + on_message: Arc::new({ + let entered = entered.clone(); + move |_| { + let entered = entered.clone(); + Box::pin(async move { + entered.notify_one(); + std::future::pending().await + }) + } + }), + on_close: Some(Arc::new({ + let closes = closes.clone(); + move |error| { + assert!(error.unwrap().contains("buffer limit")); + closes.fetch_add(1, Ordering::SeqCst); + } + })), + }) + .await + .unwrap(); + let pending = tokio::spawn({ + let rpc = peer.rpc.clone(); + let params = json!({"endpointId": endpoint.id(), "connectionId": connection.id(), "message": "held"}); + async move { rpc.send_request("ahp.message", Some(params)).await.unwrap() } + }); + timeout(WAIT, entered.notified()).await.unwrap(); + assert!( + peer.callback( + "ahp.message", + json!({ + "endpointId": endpoint.id(), "connectionId": connection.id(), "message": "overflow", + }) + ) + .await + .error + .unwrap() + .message + .contains("buffer limit") + ); + assert!( + timeout(WAIT, pending) + .await + .unwrap() + .unwrap() + .error + .is_some() + ); + endpoint.dispose().await.unwrap(); + assert_eq!(closes.load(Ordering::SeqCst), 1); +} + +#[tokio::test] +async fn abandoned_registration_and_open_are_cleaned_up() { + let peer = Peer::new(false); + for method in ["ahp.registerEndpoint", "ahp.openConnection"] { + let endpoint = if method == "ahp.openConnection" { + Some(peer.endpoint(AhpEndpointOptions::default()).await) + } else { + None + }; + *peer.hold_method.lock().await = Some(method.into()); + let abandoned = tokio::spawn({ + let client = peer.client.clone(); + let endpoint = endpoint.clone(); + async move { + if let Some(endpoint) = endpoint { + endpoint.open_connection(messages().0).await.unwrap(); + } else { + client + .create_ahp_endpoint(AhpEndpointOptions::default()) + .await + .unwrap(); + } + } + }); + timeout(WAIT, peer.send_started.notified()).await.unwrap(); + abandoned.abort(); + assert!(abandoned.await.unwrap_err().is_cancelled()); + *peer.hold_method.lock().await = None; + peer.release_send.notify_one(); + let cleanup = if endpoint.is_some() { + "ahp.closeConnection" + } else { + "ahp.disposeEndpoint" + }; + timeout(WAIT, async { + loop { + if peer.calls.lock().await.iter().any(|c| c.0 == cleanup) { + break; + } + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + if let Some(endpoint) = endpoint { + endpoint.dispose().await.unwrap(); + } + } +} + +#[tokio::test] +async fn back_to_back_cancellation_preserves_request_order() { + let peer = Peer::new(false); + let endpoint = peer + .endpoint(AhpEndpointOptions { + on_list_sessions: Some(Arc::new(|(), _| Box::pin(std::future::pending()))), + ..Default::default() + }) + .await; + let mut pending = Box::pin(peer.rpc.send_request( + "ahp.listSessions", + Some(json!({"endpointId": endpoint.id()})), + )); + // Queue the request before the cancellation without waiting for the handler. + assert!(futures_util::poll!(pending.as_mut()).is_pending()); + peer.rpc + .write(&JsonRpcNotification { + jsonrpc: "2.0".into(), + method: "$/cancelRequest".into(), + params: Some(json!({"id": 1})), + }) + .await + .unwrap(); + let response = timeout(WAIT, pending).await.unwrap().unwrap(); + assert_eq!(response.error.unwrap().code, -32800); + // A following policy request still works and cannot be blocked by the cancelled one. + let result = peer.callback("ahp.sessionControl", json!({ + "endpointId": endpoint.id(), "sessionId": "session", "kind": "dispose", "payload": {}, + })).await; + assert!( + result + .error + .unwrap() + .message + .contains("No AHP control callback") + ); + endpoint.dispose().await.unwrap(); +} diff --git a/rust/tests/e2e.rs b/rust/tests/e2e.rs index 9d1c868fe9..6eb239f4ae 100644 --- a/rust/tests/e2e.rs +++ b/rust/tests/e2e.rs @@ -3,6 +3,8 @@ #[path = "e2e/abort.rs"] mod abort; +#[path = "e2e/ahp.rs"] +mod ahp; #[path = "e2e/ask_user.rs"] mod ask_user; #[path = "e2e/auto_tier.rs"] diff --git a/rust/tests/e2e/ahp.rs b/rust/tests/e2e/ahp.rs new file mode 100644 index 0000000000..11783a4be2 --- /dev/null +++ b/rust/tests/e2e/ahp.rs @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use futures_util::{SinkExt, StreamExt}; +use github_copilot_sdk::ahp::{ + AhpConnectionOptions, AhpEndpoint, AhpEndpointOptions, AhpSessionIdentity, +}; +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::tool::ToolHandler; +use github_copilot_sdk::{Error, ErrorKind, SessionConfig, Tool, ToolInvocation, ToolResult}; +use serde_json::json; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; + +use super::support::{DEFAULT_TEST_TOKEN, with_dedicated_e2e_context}; + +struct Encrypt(Arc); + +#[async_trait::async_trait] +impl ToolHandler for Encrypt { + async fn call(&self, invocation: ToolInvocation) -> Result { + assert_eq!(invocation.arguments["input"], "Hello"); + self.0.fetch_add(1, Ordering::SeqCst); + Ok(ToolResult::Text("HELLO".into())) + } +} + +fn config(calls: Arc) -> SessionConfig { + SessionConfig::default() + .with_github_token(DEFAULT_TEST_TOKEN) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![ + Tool::new("encrypt_string") + .with_description("Encrypts a string") + .with_parameters(json!({ + "type": "object", + "properties": {"input": {"type": "string", "description": "String to encrypt"}}, + "required": ["input"], + })) + .with_handler(Arc::new(Encrypt(calls))), + ]) +} + +async fn serve(stream: TcpStream, endpoint: AhpEndpoint, stopped: CancellationToken) { + let socket = tokio_tungstenite::accept_async(stream).await.unwrap(); + let (sink, mut source) = socket.split(); + let sink = Arc::new(Mutex::new(sink)); + let close_token = stopped.child_token(); + let on_close_token = close_token.clone(); + let connection = endpoint + .open_connection(AhpConnectionOptions { + on_message: Arc::new(move |message| { + let sink = sink.clone(); + Box::pin(async move { + sink.lock() + .await + .send(message.into()) + .await + .map_err(|e| Error::with_message(ErrorKind::Io, e.to_string())) + }) + }), + on_close: Some(Arc::new(move |_| on_close_token.cancel())), + }) + .await + .unwrap(); + loop { + tokio::select! { + _ = close_token.cancelled() => break, + message = source.next() => match message { + Some(Ok(tokio_tungstenite::tungstenite::Message::Text(message))) => + connection.send(message).await.unwrap(), + Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) | None => break, + Some(Err(error)) => panic!("AHP WebSocket failed: {error}"), + _ => {} + }, + } + } + connection.close().await.unwrap(); +} + +async fn drive(create: bool) { + // Reuse the same recorded model exchanges as the ordinary SDK tool test. + // Only the client transport changes; model calls still go through CapiProxy. + with_dedicated_e2e_context("tools", "invokes_custom_tool", |ctx| { + Box::pin(async move { + let client = ctx.start_client().await; + let calls = Arc::new(AtomicUsize::new(0)); + let creations = Arc::new(AtomicUsize::new(0)); + let sessions = Arc::new(Mutex::new(Vec::new())); + let private = client + .create_session(ctx.approve_all_session_config()) + .await + .unwrap(); + let existing_id = if create { + String::new() + } else { + let session = client.create_session(config(calls.clone())).await.unwrap(); + let id = session.id().to_string(); + sessions.lock().await.push(session); + id + }; + let endpoint = client + .create_ahp_endpoint(AhpEndpointOptions { + allow_session_creation: Some(create), + on_create_session: Some(Arc::new({ + let client = client.clone(); + let sessions = sessions.clone(); + let calls = calls.clone(); + let creations = creations.clone(); + move |request, _context| { + let client = client.clone(); + let sessions = sessions.clone(); + let calls = calls.clone(); + let creations = creations.clone(); + Box::pin(async move { + let session = client + .create_session( + config(calls).with_session_id(request.requested_session_id), + ) + .await?; + let session_id = session.id().to_string(); + sessions.lock().await.push(session); + creations.fetch_add(1, Ordering::SeqCst); + Ok(AhpSessionIdentity { session_id }) + }) + } + })), + on_list_sessions: Some(Arc::new({ + let sessions = sessions.clone(); + move |(), _context| { + let sessions = sessions.clone(); + Box::pin(async move { + Ok(sessions + .lock() + .await + .iter() + .map(|session| AhpSessionIdentity { + session_id: session.id().to_string(), + }) + .collect()) + }) + } + })), + ..Default::default() + }) + .await + .unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = format!("ws://{}", listener.local_addr().unwrap()); + let stopped = CancellationToken::new(); + let server = tokio::spawn({ + let endpoint = endpoint.clone(); + let stopped = stopped.clone(); + async move { + let mut connections = tokio::task::JoinSet::new(); + loop { + tokio::select! { + _ = stopped.cancelled() => break, + accepted = listener.accept() => { + let (stream, _) = accepted.unwrap(); + connections.spawn(serve(stream, endpoint.clone(), stopped.clone())); + } + } + } + while let Some(result) = connections.join_next().await { + result.unwrap(); + } + } + }); + let output = tokio::process::Command::new("node") + .arg( + ctx.repo_root() + .join("nodejs/test/fixtures/ahp-drive-agent.mjs"), + ) + .args([url, existing_id, private.id().to_string()]) + .kill_on_drop(true) + .output() + .await + .unwrap(); + stopped.cancel(); + server.await.unwrap(); + endpoint.dispose().await.unwrap(); + assert!( + output.status.success(), + "AHP client failed:\n{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + String::from_utf8_lossy(&output.stdout).contains("AHP_TOOL_TURN_AND_RECONNECT_OK") + ); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(creations.load(Ordering::SeqCst), usize::from(create)); + // Endpoint disposal must not destroy the application's owners. + for session in sessions.lock().await.iter() { + assert!(!session.get_events().await.unwrap().is_empty()); + session.disconnect().await.unwrap(); + } + private.disconnect().await.unwrap(); + client.stop().await.unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn standard_ahp_client_drives_existing_rust_session() { + drive(false).await; +} + +#[tokio::test] +async fn standard_ahp_client_creates_and_drives_rust_session() { + drive(true).await; +} diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 714988cb05..ee6922658e 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -1060,7 +1060,8 @@ function emitRustStringEnum( usedVariantNames, "Value", reservedVariantNames, - STRING_ENUM_VARIANT_OVERRIDES[enumName]?.[value], + STRING_ENUM_VARIANT_OVERRIDES[enumName]?.[value] ?? + (value === "unknown" ? "UnknownValue" : undefined), ); pushRustDoc(lines, enumValueDescriptions?.[value], " "); if (variantName !== value) {