diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f55ce7f..ee5756f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ changes accumulate. Track in-flight protocol changes via PRs touching ### Added +- `root/downloadProgress` (`DownloadProgressParams`, `DownloadPhase`) host→client + notification reporting progress while the host downloads a resource on the + client's behalf (e.g. an agent's native SDK fetched lazily on first use), so + clients can show a progress indicator instead of a silent multi-second hang. - `SessionModelInfo.maxOutputTokens` and `SessionModelInfo.maxPromptTokens` optional fields for communicating model token limits. - `SessionSummary._meta` optional provider metadata field for lightweight diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index e8ff02d6..fce4e904 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -16,6 +16,9 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. ### Added +- `DownloadProgressParams` struct and `DownloadPhase` (wire + `root/downloadProgress`) for reporting host-side resource download progress + (e.g. an agent's native SDK fetched lazily on first use). - `SessionModelInfo.MaxOutputTokens` and `SessionModelInfo.MaxPromptTokens` optional fields for communicating model token limits. - `SessionSummary.Meta` (wire `_meta`) optional provider metadata field for diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index aaa8a0b9..2a0da1a9 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -25,6 +25,20 @@ const ( AuthRequiredReasonExpired AuthRequiredReason = "expired" ) +// Lifecycle phase of a single download. +type DownloadPhase string + +const ( + // The download has begun; no bytes received yet. + DownloadPhaseStarted DownloadPhase = "started" + // A throttled progress sample with bytes received so far. + DownloadPhaseProgress DownloadPhase = "progress" + // Terminal success frame; the resource is fully downloaded. + DownloadPhaseCompleted DownloadPhase = "completed" + // Terminal failure frame; see {@link DownloadProgressParams.error}. + DownloadPhaseFailed DownloadPhase = "failed" +) + // ─── Notification Payloads ──────────────────────────────────────────── // Broadcast to all clients subscribed to the root channel when a new session @@ -85,6 +99,70 @@ type SessionSummaryChangedParams struct { Changes PartialSessionSummary `json:"changes"` } +// Broadcast on the root channel while the host downloads a resource on the +// client's behalf — typically a multi-MB artifact fetched lazily the first time +// it is needed (today: an agent's native SDK/runtime, `kind: 'agent-sdk'`). Lets +// clients show a progress indicator instead of a silent multi-second hang. +// +// The notification is intentionally **resource-agnostic** so the same channel +// can report future downloads (additional agent runtimes, plugins, models, …) +// without a new method. The `kind` discriminant categorizes the resource and +// `resourceId` identifies it within that kind; clients that don't care can show +// a single generic indicator driven by `displayName` + the byte counts. +// +// This is **host-level**, not session state: the artifact is shared across every +// consumer and the host deduplicates concurrent fetches into one download (one +// `downloadId`). The optional `session` field names the session whose action +// triggered the fetch, purely as context — a client MAY attribute the progress +// to that session's row, or show a single global indicator and ignore it. +// +// Semantics: +// +// - Frames for one download share a stable `downloadId`. The first frame a +// client observes for a `downloadId` begins the indicator even if it is not +// `phase: 'started'` (a client that connects mid-download may miss the +// `started` frame). +// - `receivedBytes` is monotonically non-decreasing within a `downloadId`. +// `totalBytes` is present only when the host knows the size up front +// (e.g. a `Content-Length`); when absent the client SHOULD show an +// indeterminate indicator. +// - Exactly one terminal frame (`phase: 'completed'` or `'failed'`) ends a +// download. `error` carries a short, non-localized reason on failure. +// - Like all notifications this is ephemeral and is **not** replayed on +// reconnect. A client that never receives a terminal frame (the download +// finished while it was disconnected) SHOULD expire the indicator after an +// idle timeout. +// - The brand noun is carried in `displayName`; clients own the surrounding +// (localized) template, e.g. `"Downloading {displayName}… {pct}%"`. +type DownloadProgressParams struct { + // Channel URI this notification belongs to (the root channel) + Channel URI `json:"channel"` + // Stable id for one download. Coalesces the frames of a single fetch and + // distinguishes concurrent downloads (e.g. two resources at once). + DownloadId string `json:"downloadId"` + // Category of resource being downloaded. An open string (not a closed enum) + // so new resource types can be reported without a protocol bump. Known + // values today: `'agent-sdk'` (an agent's native SDK/runtime). + Kind string `json:"kind"` + // Id of the resource within its {@link kind}, e.g. the provider id `'claude'` + // or `'codex'` for an `'agent-sdk'` download. + ResourceId string `json:"resourceId"` + // Human-readable brand name for display, e.g. `'Claude'`. The host supplies + // the noun; the client owns the surrounding localized template. + DisplayName string `json:"displayName"` + // Lifecycle phase of this frame. + Phase DownloadPhase `json:"phase"` + // Bytes written so far. Monotonically non-decreasing within a `downloadId`. + ReceivedBytes int64 `json:"receivedBytes"` + // Total bytes when known (e.g. from `Content-Length`); omitted ⇒ indeterminate. + TotalBytes *int64 `json:"totalBytes,omitempty"` + // Session whose action triggered the fetch, if any. Informational only — + // the download is host-level and shared across sessions. + Session *URI `json:"session,omitempty"` + // Short, non-localized failure reason; present only when `phase: 'failed'`. + Error *string `json:"error,omitempty"` +} + // Sent by the server when a protected resource requires (re-)authentication. // // This notification MAY be associated with any channel — for example, an diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index 9a564c32..b2b2b7d0 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -17,6 +17,9 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump ### Added +- `DownloadProgressParams` data class and `DownloadPhase` enum (wire + `root/downloadProgress`) for reporting host-side resource download progress + (e.g. an agent's native SDK fetched lazily on first use). - `SessionModelInfo.maxOutputTokens` and `SessionModelInfo.maxPromptTokens` optional fields for communicating model token limits. - `SessionSummary.meta` (`_meta` on the wire) optional provider metadata field diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index 7335c7d6..31c52a15 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -38,6 +38,33 @@ enum class AuthRequiredReason { EXPIRED } +/** + * Lifecycle phase of a single download. + */ +@Serializable +enum class DownloadPhase { + /** + * The download has begun; no bytes received yet. + */ + @SerialName("started") + STARTED, + /** + * A throttled progress sample with bytes received so far. + */ + @SerialName("progress") + PROGRESS, + /** + * Terminal success frame; the resource is fully downloaded. + */ + @SerialName("completed") + COMPLETED, + /** + * Terminal failure frame; see {@link DownloadProgressParams.error}. + */ + @SerialName("failed") + FAILED +} + // ─── Notification Types ───────────────────────────────────────────────────── @Serializable @@ -83,6 +110,56 @@ data class SessionSummaryChangedParams( val changes: PartialSessionSummary ) +@Serializable +data class DownloadProgressParams( + /** + * Channel URI this notification belongs to (the root channel) + */ + val channel: String, + /** + * Stable id for one download. Coalesces the frames of a single fetch and + * distinguishes concurrent downloads (e.g. two resources at once). + */ + val downloadId: String, + /** + * Category of resource being downloaded. An open string (not a closed enum) + * so new resource types can be reported without a protocol bump. Known + * values today: `'agent-sdk'` (an agent's native SDK/runtime). + */ + val kind: String, + /** + * Id of the resource within its {@link kind}, e.g. the provider id `'claude'` + * or `'codex'` for an `'agent-sdk'` download. + */ + val resourceId: String, + /** + * Human-readable brand name for display, e.g. `'Claude'`. The host supplies + * the noun; the client owns the surrounding localized template. + */ + val displayName: String, + /** + * Lifecycle phase of this frame. + */ + val phase: DownloadPhase, + /** + * Bytes written so far. Monotonically non-decreasing within a `downloadId`. + */ + val receivedBytes: Long, + /** + * Total bytes when known (e.g. from `Content-Length`); omitted ⇒ indeterminate. + */ + val totalBytes: Long? = null, + /** + * Session whose action triggered the fetch, if any. Informational only — + * the download is host-level and shared across sessions. + */ + val session: String? = null, + /** + * Short, non-localized failure reason; present only when `phase: 'failed'`. + */ + val error: String? = null +) + @Serializable data class AuthRequiredParams( /** diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index ed90eda0..3ab44114 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -17,6 +17,9 @@ matching `## [X.Y.Z]` heading is missing from this file. ### Added +- `DownloadProgressParams` struct and `DownloadPhase` enum (wire + `root/downloadProgress`) for reporting host-side resource download progress + (e.g. an agent's native SDK fetched lazily on first use). - `SessionModelInfo.maxOutputTokens` and `SessionModelInfo.maxPromptTokens` optional fields for communicating model token limits. - `SessionSummary.meta` (`_meta` on the wire) optional provider metadata field diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index e6e3b0f8..385b7e06 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -30,6 +30,23 @@ pub enum AuthRequiredReason { Expired, } +/// Lifecycle phase of a single download. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum DownloadPhase { + /// The download has begun; no bytes received yet. + #[serde(rename = "started")] + Started, + /// A throttled progress sample with bytes received so far. + #[serde(rename = "progress")] + Progress, + /// Terminal success frame; the resource is fully downloaded. + #[serde(rename = "completed")] + Completed, + /// Terminal failure frame; see {@link DownloadProgressParams.error}. + #[serde(rename = "failed")] + Failed, +} + // ─── Notification Payloads ──────────────────────────────────────────── /// Broadcast to all clients subscribed to the root channel when a new session @@ -96,6 +113,75 @@ pub struct SessionSummaryChangedParams { pub changes: PartialSessionSummary, } +/// Broadcast on the root channel while the host downloads a resource on the +/// client's behalf — typically a multi-MB artifact fetched lazily the first time +/// it is needed (today: an agent's native SDK/runtime, `kind: 'agent-sdk'`). Lets +/// clients show a progress indicator instead of a silent multi-second hang. +/// +/// The notification is intentionally **resource-agnostic** so the same channel +/// can report future downloads (additional agent runtimes, plugins, models, …) +/// without a new method. The `kind` discriminant categorizes the resource and +/// `resourceId` identifies it within that kind; clients that don't care can show +/// a single generic indicator driven by `displayName` + the byte counts. +/// +/// This is **host-level**, not session state: the artifact is shared across every +/// consumer and the host deduplicates concurrent fetches into one download (one +/// `downloadId`). The optional `session` field names the session whose action +/// triggered the fetch, purely as context — a client MAY attribute the progress +/// to that session's row, or show a single global indicator and ignore it. +/// +/// Semantics: +/// +/// - Frames for one download share a stable `downloadId`. The first frame a +/// client observes for a `downloadId` begins the indicator even if it is not +/// `phase: 'started'` (a client that connects mid-download may miss the +/// `started` frame). +/// - `receivedBytes` is monotonically non-decreasing within a `downloadId`. +/// `totalBytes` is present only when the host knows the size up front +/// (e.g. a `Content-Length`); when absent the client SHOULD show an +/// indeterminate indicator. +/// - Exactly one terminal frame (`phase: 'completed'` or `'failed'`) ends a +/// download. `error` carries a short, non-localized reason on failure. +/// - Like all notifications this is ephemeral and is **not** replayed on +/// reconnect. A client that never receives a terminal frame (the download +/// finished while it was disconnected) SHOULD expire the indicator after an +/// idle timeout. +/// - The brand noun is carried in `displayName`; clients own the surrounding +/// (localized) template, e.g. `"Downloading {displayName}… {pct}%"`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DownloadProgressParams { + /// Channel URI this notification belongs to (the root channel) + pub channel: Uri, + /// Stable id for one download. Coalesces the frames of a single fetch and + /// distinguishes concurrent downloads (e.g. two resources at once). + pub download_id: String, + /// Category of resource being downloaded. An open string (not a closed enum) + /// so new resource types can be reported without a protocol bump. Known + /// values today: `'agent-sdk'` (an agent's native SDK/runtime). + pub kind: String, + /// Id of the resource within its {@link kind}, e.g. the provider id `'claude'` + /// or `'codex'` for an `'agent-sdk'` download. + pub resource_id: String, + /// Human-readable brand name for display, e.g. `'Claude'`. The host supplies + /// the noun; the client owns the surrounding localized template. + pub display_name: String, + /// Lifecycle phase of this frame. + pub phase: DownloadPhase, + /// Bytes written so far. Monotonically non-decreasing within a `downloadId`. + pub received_bytes: i64, + /// Total bytes when known (e.g. from `Content-Length`); omitted ⇒ indeterminate. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_bytes: Option, + /// Session whose action triggered the fetch, if any. Informational only — + /// the download is host-level and shared across sessions. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub session: Option, + /// Short, non-localized failure reason; present only when `phase: 'failed'`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, +} + /// Sent by the server when a protected resource requires (re-)authentication. /// /// This notification MAY be associated with any channel — for example, an diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index 811705a8..28e32b8f 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -12,6 +12,18 @@ public enum AuthRequiredReason: String, Codable, Sendable { case expired = "expired" } +/// Lifecycle phase of a single download. +public enum DownloadPhase: String, Codable, Sendable { + /// The download has begun; no bytes received yet. + case started = "started" + /// A throttled progress sample with bytes received so far. + case progress = "progress" + /// Terminal success frame; the resource is fully downloaded. + case completed = "completed" + /// Terminal failure frame; see {@link DownloadProgressParams.error}. + case failed = "failed" +} + // MARK: - Notification Types public struct SessionAddedParams: Codable, Sendable { @@ -66,6 +78,59 @@ public struct SessionSummaryChangedParams: Codable, Sendable { } } +public struct DownloadProgressParams: Codable, Sendable { + /// Channel URI this notification belongs to (the root channel) + public var channel: String + /// Stable id for one download. Coalesces the frames of a single fetch and + /// distinguishes concurrent downloads (e.g. two resources at once). + public var downloadId: String + /// Category of resource being downloaded. An open string (not a closed enum) + /// so new resource types can be reported without a protocol bump. Known + /// values today: `'agent-sdk'` (an agent's native SDK/runtime). + public var kind: String + /// Id of the resource within its {@link kind}, e.g. the provider id `'claude'` + /// or `'codex'` for an `'agent-sdk'` download. + public var resourceId: String + /// Human-readable brand name for display, e.g. `'Claude'`. The host supplies + /// the noun; the client owns the surrounding localized template. + public var displayName: String + /// Lifecycle phase of this frame. + public var phase: DownloadPhase + /// Bytes written so far. Monotonically non-decreasing within a `downloadId`. + public var receivedBytes: Int + /// Total bytes when known (e.g. from `Content-Length`); omitted ⇒ indeterminate. + public var totalBytes: Int? + /// Session whose action triggered the fetch, if any. Informational only — + /// the download is host-level and shared across sessions. + public var session: String? + /// Short, non-localized failure reason; present only when `phase: 'failed'`. + public var error: String? + + public init( + channel: String, + downloadId: String, + kind: String, + resourceId: String, + displayName: String, + phase: DownloadPhase, + receivedBytes: Int, + totalBytes: Int? = nil, + session: String? = nil, + error: String? = nil + ) { + self.channel = channel + self.downloadId = downloadId + self.kind = kind + self.resourceId = resourceId + self.displayName = displayName + self.phase = phase + self.receivedBytes = receivedBytes + self.totalBytes = totalBytes + self.session = session + self.error = error + } +} + public struct AuthRequiredParams: Codable, Sendable { /// Channel URI this notification belongs to public var channel: String diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index c6d857ac..09e57db1 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -19,6 +19,9 @@ the tag matches the version pinned in [`VERSION`](VERSION). ### Added +- `DownloadProgressParams` struct and `DownloadPhase` enum (wire + `root/downloadProgress`) for reporting host-side resource download progress + (e.g. an agent's native SDK fetched lazily on first use). - `SessionModelInfo.maxOutputTokens` and `SessionModelInfo.maxPromptTokens` optional fields for communicating model token limits. - `SessionSummary.meta` (`_meta` on the wire) optional provider metadata field diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index ab7e9ef7..9b019041 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -22,6 +22,10 @@ hotfix escape hatch. ### Added +- `root/downloadProgress` notification types (`DownloadProgressParams`, + `DownloadPhase`) for reporting host-side resource download progress (e.g. an + agent's native SDK fetched lazily on first use), so clients can show a progress + indicator instead of a silent multi-second hang. - `SessionModelInfo.maxOutputTokens` and `SessionModelInfo.maxPromptTokens` optional fields for communicating model token limits. - `SessionSummary._meta` optional provider metadata field for lightweight diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 7f72b1c6..20d2d065 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -145,6 +145,61 @@ "changes" ] }, + "DownloadProgressParams": { + "type": "object", + "description": "Broadcast on the root channel while the host downloads a resource on the\nclient's behalf — typically a multi-MB artifact fetched lazily the first time\nit is needed (today: an agent's native SDK/runtime, `kind: 'agent-sdk'`). Lets\nclients show a progress indicator instead of a silent multi-second hang.\n\nThe notification is intentionally **resource-agnostic** so the same channel\ncan report future downloads (additional agent runtimes, plugins, models, …)\nwithout a new method. The `kind` discriminant categorizes the resource and\n`resourceId` identifies it within that kind; clients that don't care can show\na single generic indicator driven by `displayName` + the byte counts.\n\nThis is **host-level**, not session state: the artifact is shared across every\nconsumer and the host deduplicates concurrent fetches into one download (one\n`downloadId`). The optional `session` field names the session whose action\ntriggered the fetch, purely as context — a client MAY attribute the progress\nto that session's row, or show a single global indicator and ignore it.\n\nSemantics:\n\n- Frames for one download share a stable `downloadId`. The first frame a\n client observes for a `downloadId` begins the indicator even if it is not\n `phase: 'started'` (a client that connects mid-download may miss the\n `started` frame).\n- `receivedBytes` is monotonically non-decreasing within a `downloadId`.\n `totalBytes` is present only when the host knows the size up front\n (e.g. a `Content-Length`); when absent the client SHOULD show an\n indeterminate indicator.\n- Exactly one terminal frame (`phase: 'completed'` or `'failed'`) ends a\n download. `error` carries a short, non-localized reason on failure.\n- Like all notifications this is ephemeral and is **not** replayed on\n reconnect. A client that never receives a terminal frame (the download\n finished while it was disconnected) SHOULD expire the indicator after an\n idle timeout.\n- The brand noun is carried in `displayName`; clients own the surrounding\n (localized) template, e.g. `\"Downloading {displayName}… {pct}%\"`.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this notification belongs to (the root channel)" + }, + "downloadId": { + "type": "string", + "description": "Stable id for one download. Coalesces the frames of a single fetch and\ndistinguishes concurrent downloads (e.g. two resources at once)." + }, + "kind": { + "type": "string", + "description": "Category of resource being downloaded. An open string (not a closed enum)\nso new resource types can be reported without a protocol bump. Known\nvalues today: `'agent-sdk'` (an agent's native SDK/runtime)." + }, + "resourceId": { + "type": "string", + "description": "Id of the resource within its {@link kind}, e.g. the provider id `'claude'`\nor `'codex'` for an `'agent-sdk'` download." + }, + "displayName": { + "type": "string", + "description": "Human-readable brand name for display, e.g. `'Claude'`. The host supplies\nthe noun; the client owns the surrounding localized template." + }, + "phase": { + "$ref": "#/$defs/DownloadPhase", + "description": "Lifecycle phase of this frame." + }, + "receivedBytes": { + "type": "number", + "description": "Bytes written so far. Monotonically non-decreasing within a `downloadId`." + }, + "totalBytes": { + "type": "number", + "description": "Total bytes when known (e.g. from `Content-Length`); omitted ⇒ indeterminate." + }, + "session": { + "$ref": "#/$defs/URI", + "description": "Session whose action triggered the fetch, if any. Informational only —\nthe download is host-level and shared across sessions." + }, + "error": { + "type": "string", + "description": "Short, non-localized failure reason; present only when `phase: 'failed'`." + } + }, + "required": [ + "channel", + "downloadId", + "kind", + "resourceId", + "displayName", + "phase", + "receivedBytes" + ] + }, "OtlpExportLogsParams": { "type": "object", "description": "Delivers a batch of OTLP log records to a client subscribed to the host's\nlogs channel (advertised on `TelemetryCapabilities.logs`).\n\nThe `payload` field is an OTLP/JSON `ExportLogsServiceRequest` value\nverbatim — i.e. an object of shape `{ resourceLogs: ResourceLogs[] }` as\ndefined by [opentelemetry-proto](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/collector/logs/v1/logs_service.proto).\nAHP does not redeclare the OTLP type system; clients SHOULD use an\nOpenTelemetry SDK or schema to parse it.\n\nLike all stateless-channel notifications, this is ephemeral: it is not\nreplayed on reconnect. Subscribers receive only batches emitted after\ntheir `subscribe` succeeds.", @@ -217,6 +272,9 @@ { "$ref": "#/$defs/SessionSummaryChangedParams" }, + { + "$ref": "#/$defs/DownloadProgressParams" + }, { "$ref": "#/$defs/OtlpExportLogsParams" }, diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 014312ab..c2b5c3c6 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1533,12 +1533,13 @@ function generateCommandsFile(project: Project): string { // ─── Notifications File Generator ──────────────────────────────────────────── -const NOTIFICATION_ENUMS = ['AuthRequiredReason']; +const NOTIFICATION_ENUMS = ['AuthRequiredReason', 'DownloadPhase']; const NOTIFICATION_STRUCTS = [ 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'DownloadProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', 'OtlpExportTracesParams', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 8201a3ed..1ecc8835 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1516,12 +1516,13 @@ function generateCommandsFile(project: Project): string { // ─── Notifications File Generator ──────────────────────────────────────────── -const NOTIFICATION_ENUMS = ['AuthRequiredReason']; +const NOTIFICATION_ENUMS = ['AuthRequiredReason', 'DownloadPhase']; const NOTIFICATION_STRUCTS = [ 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'DownloadProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', 'OtlpExportTracesParams', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 55747102..7e00ef97 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1439,12 +1439,13 @@ pub enum ChangesetOperationTarget { // ─── Notifications File Generator ──────────────────────────────────────────── -const NOTIFICATION_ENUMS = ['AuthRequiredReason']; +const NOTIFICATION_ENUMS = ['AuthRequiredReason', 'DownloadPhase']; const NOTIFICATION_STRUCTS = [ 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'DownloadProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', 'OtlpExportTracesParams', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 90aac11a..2425fb63 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1438,10 +1438,10 @@ public struct ChangesetOperationRangeTarget: Codable, Sendable { // ─── Notifications File Generator ──────────────────────────────────────────── -const NOTIFICATION_ENUMS = ['AuthRequiredReason']; +const NOTIFICATION_ENUMS = ['AuthRequiredReason', 'DownloadPhase']; const NOTIFICATION_STRUCTS = [ - 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', 'AuthRequiredParams', + 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', 'DownloadProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', 'OtlpExportTracesParams', 'OtlpExportMetricsParams', ]; diff --git a/types/channels-root/notifications.ts b/types/channels-root/notifications.ts index 32fea359..f9522611 100644 --- a/types/channels-root/notifications.ts +++ b/types/channels-root/notifications.ts @@ -142,3 +142,121 @@ export interface SessionSummaryChangedParams { */ changes: Partial; } + +// ─── root/downloadProgress ─────────────────────────────────────────────────── + +/** + * Lifecycle phase of a single download. + * + * @category Protocol Notifications + */ +export const enum DownloadPhase { + /** The download has begun; no bytes received yet. */ + Started = 'started', + /** A throttled progress sample with bytes received so far. */ + Progress = 'progress', + /** Terminal success frame; the resource is fully downloaded. */ + Completed = 'completed', + /** Terminal failure frame; see {@link DownloadProgressParams.error}. */ + Failed = 'failed', +} + +/** + * Broadcast on the root channel while the host downloads a resource on the + * client's behalf — typically a multi-MB artifact fetched lazily the first time + * it is needed (today: an agent's native SDK/runtime, `kind: 'agent-sdk'`). Lets + * clients show a progress indicator instead of a silent multi-second hang. + * + * The notification is intentionally **resource-agnostic** so the same channel + * can report future downloads (additional agent runtimes, plugins, models, …) + * without a new method. The `kind` discriminant categorizes the resource and + * `resourceId` identifies it within that kind; clients that don't care can show + * a single generic indicator driven by `displayName` + the byte counts. + * + * This is **host-level**, not session state: the artifact is shared across every + * consumer and the host deduplicates concurrent fetches into one download (one + * `downloadId`). The optional `session` field names the session whose action + * triggered the fetch, purely as context — a client MAY attribute the progress + * to that session's row, or show a single global indicator and ignore it. + * + * Semantics: + * + * - Frames for one download share a stable `downloadId`. The first frame a + * client observes for a `downloadId` begins the indicator even if it is not + * `phase: 'started'` (a client that connects mid-download may miss the + * `started` frame). + * - `receivedBytes` is monotonically non-decreasing within a `downloadId`. + * `totalBytes` is present only when the host knows the size up front + * (e.g. a `Content-Length`); when absent the client SHOULD show an + * indeterminate indicator. + * - Exactly one terminal frame (`phase: 'completed'` or `'failed'`) ends a + * download. `error` carries a short, non-localized reason on failure. + * - Like all notifications this is ephemeral and is **not** replayed on + * reconnect. A client that never receives a terminal frame (the download + * finished while it was disconnected) SHOULD expire the indicator after an + * idle timeout. + * - The brand noun is carried in `displayName`; clients own the surrounding + * (localized) template, e.g. `"Downloading {displayName}… {pct}%"`. + * + * @category Protocol Notifications + * @method root/downloadProgress + * @direction Server → Client + * @messageType Notification + * @version 1 + * @example + * ```json + * { + * "jsonrpc": "2.0", + * "method": "root/downloadProgress", + * "params": { + * "channel": "ahp-root://", + * "downloadId": "d3f1c2", + * "kind": "agent-sdk", + * "resourceId": "claude", + * "displayName": "Claude", + * "phase": "progress", + * "receivedBytes": 18874368, + * "totalBytes": 41957498, + * "session": "ahp-session:/" + * } + * } + * ``` + */ +export interface DownloadProgressParams { + /** Channel URI this notification belongs to (the root channel) */ + channel: URI; + /** + * Stable id for one download. Coalesces the frames of a single fetch and + * distinguishes concurrent downloads (e.g. two resources at once). + */ + downloadId: string; + /** + * Category of resource being downloaded. An open string (not a closed enum) + * so new resource types can be reported without a protocol bump. Known + * values today: `'agent-sdk'` (an agent's native SDK/runtime). + */ + kind: string; + /** + * Id of the resource within its {@link kind}, e.g. the provider id `'claude'` + * or `'codex'` for an `'agent-sdk'` download. + */ + resourceId: string; + /** + * Human-readable brand name for display, e.g. `'Claude'`. The host supplies + * the noun; the client owns the surrounding localized template. + */ + displayName: string; + /** Lifecycle phase of this frame. */ + phase: DownloadPhase; + /** Bytes written so far. Monotonically non-decreasing within a `downloadId`. */ + receivedBytes: number; + /** Total bytes when known (e.g. from `Content-Length`); omitted ⇒ indeterminate. */ + totalBytes?: number; + /** + * Session whose action triggered the fetch, if any. Informational only — + * the download is host-level and shared across sessions. + */ + session?: URI; + /** Short, non-localized failure reason; present only when `phase: 'failed'`. */ + error?: string; +} diff --git a/types/common/messages.ts b/types/common/messages.ts index fe422f37..1d8e9f13 100644 --- a/types/common/messages.ts +++ b/types/common/messages.ts @@ -76,6 +76,7 @@ import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, + DownloadProgressParams, } from '../channels-root/notifications.js'; import type { AuthRequiredParams } from './notifications.js'; import type { @@ -231,6 +232,7 @@ export interface ServerNotificationMap { 'root/sessionAdded': { params: SessionAddedParams }; 'root/sessionRemoved': { params: SessionRemovedParams }; 'root/sessionSummaryChanged': { params: SessionSummaryChangedParams }; + 'root/downloadProgress': { params: DownloadProgressParams }; 'auth/required': { params: AuthRequiredParams }; 'otlp/exportLogs': { params: OtlpExportLogsParams }; 'otlp/exportTraces': { params: OtlpExportTracesParams }; diff --git a/types/index.ts b/types/index.ts index 88b2db44..e611eb02 100644 --- a/types/index.ts +++ b/types/index.ts @@ -320,13 +320,14 @@ export type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, + DownloadProgressParams, AuthRequiredParams, OtlpExportLogsParams, OtlpExportTracesParams, OtlpExportMetricsParams, } from './notifications.js'; -export { AuthRequiredReason } from './notifications.js'; +export { AuthRequiredReason, DownloadPhase } from './notifications.js'; // Message types (JSON-RPC wire format) export type { diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts index 414602d6..eb9be00e 100644 --- a/types/version/message-checks.ts +++ b/types/version/message-checks.ts @@ -89,6 +89,7 @@ type _ExpectedServerNotifications = | 'root/sessionAdded' | 'root/sessionRemoved' | 'root/sessionSummaryChanged' + | 'root/downloadProgress' | 'auth/required' | 'otlp/exportLogs' | 'otlp/exportTraces' diff --git a/types/version/registry.ts b/types/version/registry.ts index b6ebd689..a104bbc3 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -180,6 +180,7 @@ export const NOTIFICATION_INTRODUCED_IN: { readonly [K in ProtocolNotificationMe 'root/sessionAdded': '0.1.0', 'root/sessionRemoved': '0.1.0', 'root/sessionSummaryChanged': '0.1.0', + 'root/downloadProgress': '0.5.0', 'auth/required': '0.1.0', 'otlp/exportLogs': '0.2.0', 'otlp/exportTraces': '0.2.0',