From 414495032046d70ae5f76d52b817518d5b97aa24 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 7 Oct 2026 03:28:41 +1100 Subject: [PATCH 1/4] feat: support initial chat titles Add an optional createChat title and initialTitle capability, regenerate client models, and cover cross-language round trips and constructor compatibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Commands.generated.cs | 13 ++++ .../Generated/State.generated.cs | 7 +++ .../TypesRoundTripFixtures.cs | 2 + clients/go/ahptypes/commands.generated.go | 11 ++++ clients/go/ahptypes/roundtrip_fixture_test.go | 4 ++ clients/go/ahptypes/state.generated.go | 5 ++ .../generated/Commands.generated.kt | 15 ++++- .../generated/State.generated.kt | 9 ++- .../ConstructorCompatibilityTest.kt | 35 +++++++++++ .../agenthostprotocol/RoundTripCorpusTest.kt | 2 + clients/rust/crates/ahp-types/src/commands.rs | 12 ++++ clients/rust/crates/ahp-types/src/state.rs | 6 ++ .../ahp-types/tests/roundtrip_corpus.rs | 5 +- .../Generated/Commands.generated.swift | 16 ++++- .../Generated/State.generated.swift | 9 ++- .../TypesRoundTripFixtureTests.swift | 2 + .../.changes/20261005-initial-chat-title.json | 4 ++ ...otlin-trailing-constructor-properties.json | 5 ++ ...261006-go-initial-title-struct-fields.json | 5 ++ ...1006-rust-initial-title-struct-fields.json | 5 ++ docs/specification/chat-channel.md | 17 +++++- docs/specification/root-channel.md | 15 +++++ schema/actions.schema.json | 4 ++ schema/commands.schema.json | 8 +++ schema/errors.schema.json | 8 +++ schema/notifications.schema.json | 4 ++ schema/state.schema.json | 4 ++ scripts/kotlin-constructor-order.test.ts | 32 ++++++++++ types/channels-chat/commands.ts | 13 ++++ types/channels-root/state.ts | 7 +++ .../055-create-chat-initial-title.json | 18 ++++++ .../056-agent-initial-title-capability.json | 60 +++++++++++++++++++ 32 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt create mode 100644 docs/.changes/20261005-initial-chat-title.json create mode 100644 docs/.changes/20261005-kotlin-trailing-constructor-properties.json create mode 100644 docs/.changes/20261006-go-initial-title-struct-fields.json create mode 100644 docs/.changes/20261006-rust-initial-title-struct-fields.json create mode 100644 scripts/kotlin-constructor-order.test.ts create mode 100644 types/test-cases/round-trips/055-create-chat-initial-title.json create mode 100644 types/test-cases/round-trips/056-agent-initial-title-capability.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 25945d5a3..8767313bc 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -819,6 +819,19 @@ public sealed record CreateChatParams /// {@link AgentCapabilities.multipleWorkingDirectories}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; init; } + + /// Client-selected initial display title for the new chat. + /// + /// Clients MUST NOT supply this field unless the selected agent advertises + /// {@link MultipleChatsCapability.initialTitle}. + /// + /// A server honoring this field MUST trim leading and trailing whitespace. + /// If the result is empty, it MUST treat the field as absent and choose its + /// normal initial title. Otherwise, it MUST preserve the remaining text + /// exactly: it MUST NOT collapse internal whitespace, strip quotes or + /// punctuation, or truncate it. The protocol defines no maximum title length. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Title { get; init; } } /// Disposes a chat and cleans up server-side resources. diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index ccff10f6c..09febe732 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2002,6 +2002,13 @@ public sealed record MultipleChatsCapability /// time. Side-chat support always implies multi-chat support. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? SideChat { get; init; } + + /// The agent honors {@link CreateChatParams.title} as the chat's first + /// published title and does not replace it through automatic title generation. + /// A later explicit rename is still allowed. When absent or `false`, clients + /// MUST omit `title` when calling `createChat`. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? InitialTitle { get; init; } } /// Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. diff --git a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs index b609ca48a..e6022e674 100644 --- a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs +++ b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs @@ -164,6 +164,8 @@ private static (object decoded, string reencoded) DecodeAndReencode(string type, return Wrap(Ser.Deserialize(inputJson)); case "InitializeResult": return Wrap(Ser.Deserialize(inputJson)); + case "CreateChatParams": + return Wrap(Ser.Deserialize(inputJson)); case "Snapshot": return Wrap(Ser.Deserialize(inputJson)); default: diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 096f97b77..e2694ee35 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -525,6 +525,17 @@ type CreateChatParams struct { // A client MUST NOT supply this field unless the agent advertises // {@link AgentCapabilities.multipleWorkingDirectories}. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // Client-selected initial display title for the new chat. + // + // Clients MUST NOT supply this field unless the selected agent advertises + // {@link MultipleChatsCapability.initialTitle}. + // + // A server honoring this field MUST trim leading and trailing whitespace. + // If the result is empty, it MUST treat the field as absent and choose its + // normal initial title. Otherwise, it MUST preserve the remaining text + // exactly: it MUST NOT collapse internal whitespace, strip quotes or + // punctuation, or truncate it. The protocol defines no maximum title length. + Title *string `json:"title,omitempty"` } // Disposes a chat and cleans up server-side resources. diff --git a/clients/go/ahptypes/roundtrip_fixture_test.go b/clients/go/ahptypes/roundtrip_fixture_test.go index d62481424..7806c26f5 100644 --- a/clients/go/ahptypes/roundtrip_fixture_test.go +++ b/clients/go/ahptypes/roundtrip_fixture_test.go @@ -242,6 +242,10 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string { var v InitializeResult dec(&v) return enc(&v) + case "CreateChatParams": + var v CreateChatParams + dec(&v) + return enc(&v) case "ChatSource": var v ChatSource dec(&v) diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 6f75e003d..4a607e364 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -739,6 +739,11 @@ type MultipleChatsCapability struct { // the host snapshots the available partial assistant response at creation // time. Side-chat support always implies multi-chat support. SideChat *bool `json:"sideChat,omitempty"` + // The agent honors {@link CreateChatParams.title} as the chat's first + // published title and does not replace it through automatic title generation. + // A later explicit rename is still allowed. When absent or `false`, clients + // MUST omit `title` when calling `createChat`. + InitialTitle *bool `json:"initialTitle,omitempty"` } // Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 5c19a8055..26c86c022 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -747,7 +747,20 @@ data class CreateChatParams( * A client MUST NOT supply this field unless the agent advertises * {@link AgentCapabilities.multipleWorkingDirectories}. */ - val workingDirectories: List? = null + val workingDirectories: List? = null, + /** + * Client-selected initial display title for the new chat. + * + * Clients MUST NOT supply this field unless the selected agent advertises + * {@link MultipleChatsCapability.initialTitle}. + * + * A server honoring this field MUST trim leading and trailing whitespace. + * If the result is empty, it MUST treat the field as absent and choose its + * normal initial title. Otherwise, it MUST preserve the remaining text + * exactly: it MUST NOT collapse internal whitespace, strip quotes or + * punctuation, or truncate it. The protocol defines no maximum title length. + */ + val title: String? = null ) @Serializable diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index c9bb7e8a2..c802790ac 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -1432,7 +1432,14 @@ data class MultipleChatsCapability( * the host snapshots the available partial assistant response at creation * time. Side-chat support always implies multi-chat support. */ - val sideChat: Boolean? = null + val sideChat: Boolean? = null, + /** + * The agent honors {@link CreateChatParams.title} as the chat's first + * published title and does not replace it through automatic title generation. + * A later explicit rename is still allowed. When absent or `false`, clients + * MUST omit `title` when calling `createChat`. + */ + val initialTitle: Boolean? = null ) @Serializable diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt new file mode 100644 index 000000000..3948cb1e4 --- /dev/null +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt @@ -0,0 +1,35 @@ +package com.microsoft.agenthostprotocol + +import com.microsoft.agenthostprotocol.generated.CreateChatParams +import com.microsoft.agenthostprotocol.generated.Message +import com.microsoft.agenthostprotocol.generated.MessageKind +import com.microsoft.agenthostprotocol.generated.MessageOrigin +import com.microsoft.agenthostprotocol.generated.MultipleChatsCapability +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ConstructorCompatibilityTest { + @Test + fun `old full positional constructors retain their source semantics`() { + val message = Message("Hello", MessageOrigin(MessageKind.USER)) + val createChat = + CreateChatParams( + "ahp-session:/session", + null, + "ahp-chat:/chat", + message, + null, + listOf("file:///workspace"), + ) + val multipleChats = MultipleChatsCapability(true, true) + + assertEquals(message, createChat.initialMessage) + assertEquals(listOf("file:///workspace"), createChat.workingDirectories) + assertNull(createChat.title) + assertTrue(multipleChats.fork == true) + assertTrue(multipleChats.sideChat == true) + assertNull(multipleChats.initialTitle) + } +} diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt index 7af1319f9..bee56cffa 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt @@ -31,6 +31,7 @@ package com.microsoft.agenthostprotocol import com.microsoft.agenthostprotocol.generated.ActionEnvelope import com.microsoft.agenthostprotocol.generated.ChangesetOperationTarget import com.microsoft.agenthostprotocol.generated.ChatSource +import com.microsoft.agenthostprotocol.generated.CreateChatParams import com.microsoft.agenthostprotocol.generated.Customization import com.microsoft.agenthostprotocol.generated.Implementation import com.microsoft.agenthostprotocol.generated.InitializeResult @@ -253,6 +254,7 @@ class RoundTripCorpusTest { "PartialSessionSummary" -> rt(PartialSessionSummary.serializer()) "Implementation" -> rt(Implementation.serializer()) "InitializeResult" -> rt(InitializeResult.serializer()) + "CreateChatParams" -> rt(CreateChatParams.serializer()) "ChatSource" -> rt(ChatSource.serializer()) "Snapshot" -> rt(Snapshot.serializer()) else -> fail( diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 8c876185d..2478e7b4c 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -756,6 +756,18 @@ pub struct CreateChatParams { /// {@link AgentCapabilities.multipleWorkingDirectories}. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// Client-selected initial display title for the new chat. + /// + /// Clients MUST NOT supply this field unless the selected agent advertises + /// {@link MultipleChatsCapability.initialTitle}. + /// + /// A server honoring this field MUST trim leading and trailing whitespace. + /// If the result is empty, it MUST treat the field as absent and choose its + /// normal initial title. Otherwise, it MUST preserve the remaining text + /// exactly: it MUST NOT collapse internal whitespace, strip quotes or + /// punctuation, or truncate it. The protocol defines no maximum title length. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, } /// Disposes a chat and cleans up server-side resources. diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index b7d5cd6be..d9d0cca02 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1723,6 +1723,12 @@ pub struct MultipleChatsCapability { /// time. Side-chat support always implies multi-chat support. #[serde(default, skip_serializing_if = "Option::is_none")] pub side_chat: Option, + /// The agent honors {@link CreateChatParams.title} as the chat's first + /// published title and does not replace it through automatic title generation. + /// A later explicit rename is still allowed. When absent or `false`, clients + /// MUST omit `title` when calling `createChat`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub initial_title: Option, } /// Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. diff --git a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs index 53fb11e5d..b7c2b657d 100644 --- a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs +++ b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs @@ -28,7 +28,9 @@ use ahp_types::{ actions::{ActionEnvelope, StateAction}, - commands::{ChangesetOperationTarget, ChatSource, Implementation, InitializeResult}, + commands::{ + ChangesetOperationTarget, ChatSource, CreateChatParams, Implementation, InitializeResult, + }, common::StringOrMarkdown, messages::JsonRpcMessage, notifications::{PartialSessionSummary, SessionAddedParams}, @@ -223,6 +225,7 @@ fn decode_and_reencode(file: &str, type_name: &str, input_json: &str) -> Result< "PartialSessionSummary" => round_trip!(PartialSessionSummary), "Implementation" => round_trip!(Implementation), "InitializeResult" => round_trip!(InitializeResult), + "CreateChatParams" => round_trip!(CreateChatParams), "ChatSource" => round_trip!(ChatSource), "Snapshot" => round_trip!(Snapshot), other => Err(format!( diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 838526497..163135b2b 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -802,6 +802,17 @@ public struct CreateChatParams: Codable, Sendable { /// A client MUST NOT supply this field unless the agent advertises /// {@link AgentCapabilities.multipleWorkingDirectories}. public var workingDirectories: [String]? + /// Client-selected initial display title for the new chat. + /// + /// Clients MUST NOT supply this field unless the selected agent advertises + /// {@link MultipleChatsCapability.initialTitle}. + /// + /// A server honoring this field MUST trim leading and trailing whitespace. + /// If the result is empty, it MUST treat the field as absent and choose its + /// normal initial title. Otherwise, it MUST preserve the remaining text + /// exactly: it MUST NOT collapse internal whitespace, strip quotes or + /// punctuation, or truncate it. The protocol defines no maximum title length. + public var title: String? enum CodingKeys: String, CodingKey { case channel @@ -810,6 +821,7 @@ public struct CreateChatParams: Codable, Sendable { case initialMessage case source case workingDirectories + case title } public init( @@ -818,7 +830,8 @@ public struct CreateChatParams: Codable, Sendable { chat: String, initialMessage: Message? = nil, source: ChatSource? = nil, - workingDirectories: [String]? = nil + workingDirectories: [String]? = nil, + title: String? = nil ) { self.channel = channel self.meta = meta @@ -826,6 +839,7 @@ public struct CreateChatParams: Codable, Sendable { self.initialMessage = initialMessage self.source = source self.workingDirectories = workingDirectories + self.title = title } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index ba255f605..9f4a05361 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1410,13 +1410,20 @@ public struct MultipleChatsCapability: Codable, Sendable { /// the host snapshots the available partial assistant response at creation /// time. Side-chat support always implies multi-chat support. public var sideChat: Bool? + /// The agent honors {@link CreateChatParams.title} as the chat's first + /// published title and does not replace it through automatic title generation. + /// A later explicit rename is still allowed. When absent or `false`, clients + /// MUST omit `title` when calling `createChat`. + public var initialTitle: Bool? public init( fork: Bool? = nil, - sideChat: Bool? = nil + sideChat: Bool? = nil, + initialTitle: Bool? = nil ) { self.fork = fork self.sideChat = sideChat + self.initialTitle = initialTitle } } diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift index 7d2f1350f..5bffb5972 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift @@ -188,6 +188,8 @@ final class TypesRoundTripFixtureTests: XCTestCase { return try reencode(dec.decode(Implementation.self, from: inputData)) case "InitializeResult": return try reencode(dec.decode(InitializeResult.self, from: inputData)) + case "CreateChatParams": + return try reencode(dec.decode(CreateChatParams.self, from: inputData)) case "ChatSource": return try reencode(dec.decode(ChatSource.self, from: inputData)) case "Snapshot": diff --git a/docs/.changes/20261005-initial-chat-title.json b/docs/.changes/20261005-initial-chat-title.json new file mode 100644 index 000000000..34d4443a6 --- /dev/null +++ b/docs/.changes/20261005-initial-chat-title.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Optional initial titles for newly created chats, advertised through the multiple-chat capability." +} diff --git a/docs/.changes/20261005-kotlin-trailing-constructor-properties.json b/docs/.changes/20261005-kotlin-trailing-constructor-properties.json new file mode 100644 index 000000000..57c48b92d --- /dev/null +++ b/docs/.changes/20261005-kotlin-trailing-constructor-properties.json @@ -0,0 +1,5 @@ +{ + "type": "changed", + "message": "Generated models gain trailing optional constructor properties; Kotlin binaries must be recompiled against the new client version.", + "targets": ["kotlin"] +} diff --git a/docs/.changes/20261006-go-initial-title-struct-fields.json b/docs/.changes/20261006-go-initial-title-struct-fields.json new file mode 100644 index 000000000..c9f0e6742 --- /dev/null +++ b/docs/.changes/20261006-go-initial-title-struct-fields.json @@ -0,0 +1,5 @@ +{ + "type": "changed", + "message": "Generated `CreateChatParams.Title` and `MultipleChatsCapability.InitialTitle` fields require unkeyed Go composite literals to add trailing `nil` values or migrate to keyed literals.", + "targets": ["go"] +} diff --git a/docs/.changes/20261006-rust-initial-title-struct-fields.json b/docs/.changes/20261006-rust-initial-title-struct-fields.json new file mode 100644 index 000000000..8a4fd655f --- /dev/null +++ b/docs/.changes/20261006-rust-initial-title-struct-fields.json @@ -0,0 +1,5 @@ +{ + "type": "changed", + "message": "Generated `CreateChatParams.title` and `MultipleChatsCapability.initialTitle` fields require existing Rust struct literals to add `None` or use defaults/update syntax.", + "targets": ["rust"] +} diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index 469908f52..f1f7a26e7 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -96,6 +96,7 @@ fully replaces the catalogue or clears it when `changesets` is absent. [`createChat`](/reference/chat#createchat) is a JSON-RPC request. Callers identify the owning session via the request's `channel` parameter (`ahp-session:/`) and MAY supply: +- a `title` containing the client-selected initial display title for the target chat, - an `initialMessage` to start the first turn immediately — carrying its own [`model`](/reference/chat#message) / [`agent`](/reference/chat#message) selection — and - a `source` of type [`ChatSource`](/reference/chat#chatsource), either `{ kind: "fork", chat, turnId }` or `{ kind: "sideChat", chat, turnId }`, @@ -105,7 +106,21 @@ fully replaces the catalogue or clears it when `changesets` is absent. The server allocates the chat URI and adds the chat to the session's catalog (`session/chatAdded` on the session channel) before returning. -Clients MUST gate source-based creation using the selected +Clients MUST send `title` only when the selected agent advertises +[`multipleChats.initialTitle`](/reference/root#multiplechatscapability). A +supporting server MUST trim leading and trailing whitespace. If the result is +empty, it MUST treat `title` as absent and choose its normal initial title. +Otherwise, it MUST use the remaining text in the first published chat summary +and [`ChatState`](/reference/chat#chatstate) and preserve it exactly: it MUST +NOT collapse internal whitespace, strip quotes or punctuation, or truncate it. +The protocol defines no maximum title length. The server MUST NOT replace a +non-empty supplied title through automatic title generation. A later explicit +rename is still allowed. When the capability is absent or `false`, clients MUST +omit `title` but MAY continue creating or forking the chat normally, allowing +the server to choose its initial title. The top-level `title` describes the +target chat and is independent of source provenance. + +Clients MUST also gate source-based creation using the selected [`AgentInfo.capabilities.multipleChats`](/reference/root#multiplechatscapability): - `fork: true` permits `source.kind: "fork"`. diff --git a/docs/specification/root-channel.md b/docs/specification/root-channel.md index 2ccdad606..b9c8b8006 100644 --- a/docs/specification/root-channel.md +++ b/docs/specification/root-channel.md @@ -32,6 +32,21 @@ RootState { The session list is **not** part of root state. Clients fetch it imperatively via [`listSessions`](/reference/root#listsessions) and patch it from `root/*` notifications described below. +### Agent capabilities + +Each [`AgentInfo`](/reference/root#agentinfo) may advertise optional +capabilities. Under +[`multipleChats`](/reference/root#multiplechatscapability), +`initialTitle: true` guarantees that the host honors +[`CreateChatParams.title`](/reference/chat#createchatparams) as the target +chat's first published title in its summary and state, without replacing it +through automatic title generation. A later explicit rename remains allowed. + +When `initialTitle` is absent or `false`, clients MUST omit `title` from +`createChat`. They MAY still create or fork a chat when the corresponding +multi-chat capabilities permit it; the host then chooses the initial title +normally. + ### Paginating the catalogue A large catalogue can be fetched incrementally. [`listSessions`](/reference/root#listsessions) accepts an optional `limit` (the maximum number of entries the client wants in the page — the server SHOULD respect it, but MAY return fewer and MAY impose its own upper cap) and an optional opaque `cursor`. The result carries the page in `items` plus an optional `nextCursor`: diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 5f115582d..7425eb5ca 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -3223,6 +3223,10 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." + }, + "initialTitle": { + "type": "boolean", + "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." } } }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index b091b38f4..6ad2bcd94 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1331,6 +1331,10 @@ "$ref": "#/$defs/URI" }, "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (those whose `source.kind` is `\"fork\"`) inherit\nthe source chat's `workingDirectories`; this field is ignored for forks.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}." + }, + "title": { + "type": "string", + "description": "Client-selected initial display title for the new chat.\n\nClients MUST NOT supply this field unless the selected agent advertises\n{@link MultipleChatsCapability.initialTitle}.\n\nA server honoring this field MUST trim leading and trailing whitespace.\nIf the result is empty, it MUST treat the field as absent and choose its\nnormal initial title. Otherwise, it MUST preserve the remaining text\nexactly: it MUST NOT collapse internal whitespace, strip quotes or\npunctuation, or truncate it. The protocol defines no maximum title length." } }, "required": [ @@ -2399,6 +2403,10 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." + }, + "initialTitle": { + "type": "boolean", + "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." } } }, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 238f4a7cf..4aeaf4e8b 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -662,6 +662,10 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." + }, + "initialTitle": { + "type": "boolean", + "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." } } }, @@ -7311,6 +7315,10 @@ "$ref": "#/$defs/URI" }, "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (those whose `source.kind` is `\"fork\"`) inherit\nthe source chat's `workingDirectories`; this field is ignored for forks.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}." + }, + "title": { + "type": "string", + "description": "Client-selected initial display title for the new chat.\n\nClients MUST NOT supply this field unless the selected agent advertises\n{@link MultipleChatsCapability.initialTitle}.\n\nA server honoring this field MUST trim leading and trailing whitespace.\nIf the result is empty, it MUST treat the field as absent and choose its\nnormal initial title. Otherwise, it MUST preserve the remaining text\nexactly: it MUST NOT collapse internal whitespace, strip quotes or\npunctuation, or truncate it. The protocol defines no maximum title length." } }, "required": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 723ffa747..d3cd41a46 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -840,6 +840,10 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." + }, + "initialTitle": { + "type": "boolean", + "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." } } }, diff --git a/schema/state.schema.json b/schema/state.schema.json index 043ef7fde..bc389e97b 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -573,6 +573,10 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." + }, + "initialTitle": { + "type": "boolean", + "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." } } }, diff --git a/scripts/kotlin-constructor-order.test.ts b/scripts/kotlin-constructor-order.test.ts new file mode 100644 index 000000000..49f4251d2 --- /dev/null +++ b/scripts/kotlin-constructor-order.test.ts @@ -0,0 +1,32 @@ +import { strict as assert } from 'node:assert'; +import { readFileSync } from 'node:fs'; +import { describe, it } from 'node:test'; + +function getConstructorPropertyOrder(file: URL, className: string): string[] { + const source = readFileSync(file, 'utf8'); + const declaration = source.match(new RegExp(`data class ${className}\\(([\\s\\S]*?)\\n\\)`)); + assert.ok(declaration, `missing generated Kotlin data class ${className}`); + return [...declaration[1].matchAll(/val (\w+):/g)].map(match => match[1]); +} + +describe('generated Kotlin constructor order', () => { + it('appends CreateChatParams.title after the existing constructor properties', () => { + assert.deepEqual( + getConstructorPropertyOrder( + new URL('../clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt', import.meta.url), + 'CreateChatParams', + ), + ['channel', 'meta', 'chat', 'initialMessage', 'source', 'workingDirectories', 'title'], + ); + }); + + it('appends MultipleChatsCapability.initialTitle after the existing constructor properties', () => { + assert.deepEqual( + getConstructorPropertyOrder( + new URL('../clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt', import.meta.url), + 'MultipleChatsCapability', + ), + ['fork', 'sideChat', 'initialTitle'], + ); + }); +}); diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 744b773ec..4807f0b7d 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -118,6 +118,19 @@ export interface CreateChatParams extends BaseParams { * {@link AgentCapabilities.multipleWorkingDirectories}. */ workingDirectories?: URI[]; + /** + * Client-selected initial display title for the new chat. + * + * Clients MUST NOT supply this field unless the selected agent advertises + * {@link MultipleChatsCapability.initialTitle}. + * + * A server honoring this field MUST trim leading and trailing whitespace. + * If the result is empty, it MUST treat the field as absent and choose its + * normal initial title. Otherwise, it MUST preserve the remaining text + * exactly: it MUST NOT collapse internal whitespace, strip quotes or + * punctuation, or truncate it. The protocol defines no maximum title length. + */ + title?: string; } // ─── moveChat ──────────────────────────────────────────────────────────────── diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index e9a3bc91b..048b12d32 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -154,6 +154,13 @@ export interface MultipleChatsCapability { * time. Side-chat support always implies multi-chat support. */ sideChat?: boolean; + /** + * The agent honors {@link CreateChatParams.title} as the chat's first + * published title and does not replace it through automatic title generation. + * A later explicit rename is still allowed. When absent or `false`, clients + * MUST omit `title` when calling `createChat`. + */ + initialTitle?: boolean; } /** diff --git a/types/test-cases/round-trips/055-create-chat-initial-title.json b/types/test-cases/round-trips/055-create-chat-initial-title.json new file mode 100644 index 000000000..d138ef183 --- /dev/null +++ b/types/test-cases/round-trips/055-create-chat-initial-title.json @@ -0,0 +1,18 @@ +{ + "name": "create-chat-initial-title", + "group": "A", + "description": "CreateChatParams preserves a client-selected initial display title through every generated client.", + "type": "CreateChatParams", + "input": { + "channel": "ahp-session:/s1", + "chat": "ahp-chat:/c2", + "title": "Investigate startup performance" + }, + "acceptableOutputs": [ + { + "channel": "ahp-session:/s1", + "chat": "ahp-chat:/c2", + "title": "Investigate startup performance" + } + ] +} diff --git a/types/test-cases/round-trips/056-agent-initial-title-capability.json b/types/test-cases/round-trips/056-agent-initial-title-capability.json new file mode 100644 index 000000000..b930486a4 --- /dev/null +++ b/types/test-cases/round-trips/056-agent-initial-title-capability.json @@ -0,0 +1,60 @@ +{ + "name": "agent-initial-title-capability", + "group": "A", + "description": "AgentInfo advertises support for client-selected initial chat titles through capabilities.multipleChats.initialTitle.", + "type": "InitializeResult", + "input": { + "protocolVersion": "0.9.0", + "serverSeq": 3, + "snapshots": [ + { + "resource": "ahp-root://", + "state": { + "agents": [ + { + "provider": "example", + "displayName": "Example", + "description": "Example agent", + "models": [], + "capabilities": { + "multipleChats": { + "fork": true, + "initialTitle": true + } + } + } + ] + }, + "fromSeq": 3 + } + ] + }, + "acceptableOutputs": [ + { + "protocolVersion": "0.9.0", + "serverSeq": 3, + "snapshots": [ + { + "resource": "ahp-root://", + "state": { + "agents": [ + { + "provider": "example", + "displayName": "Example", + "description": "Example agent", + "models": [], + "capabilities": { + "multipleChats": { + "fork": true, + "initialTitle": true + } + } + } + ] + }, + "fromSeq": 3 + } + ] + } + ] +} From a3da2d22b433f7a1cb1680e37ab7e1016f4a9fb6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 13 Oct 2026 18:38:39 +1100 Subject: [PATCH 2/4] feat: support capability-backed fork initial messages Define composed fork acceptance and rejection semantics while preserving legacy initialMessage behavior. Update generated client models, schemas, documentation, and round-trip coverage. Refs microsoft/vscode#341229 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Commands.generated.cs | 12 +++- .../Generated/Errors.generated.cs | 18 ++++++ .../JsonSerializerContext.generated.cs | 2 + .../Generated/State.generated.cs | 13 ++++ clients/go/ahptypes/commands.generated.go | 10 +++ clients/go/ahptypes/errors.generated.go | 26 ++++++-- clients/go/ahptypes/state.generated.go | 11 ++++ .../generated/Commands.generated.kt | 10 +++ .../generated/Errors.generated.kt | 32 ++++++++++ .../generated/State.generated.kt | 15 ++++- .../ConstructorCompatibilityTest.kt | 1 + clients/rust/crates/ahp-types/src/commands.rs | 10 +++ clients/rust/crates/ahp-types/src/errors.rs | 21 +++++++ clients/rust/crates/ahp-types/src/state.rs | 12 ++++ .../Generated/Commands.generated.swift | 10 +++ .../Generated/Errors.generated.swift | 38 ++++++++++++ .../Generated/State.generated.swift | 15 ++++- .../20261008-fork-initial-message.json | 4 ++ docs/specification/chat-channel.md | 42 +++++++++++-- docs/specification/root-channel.md | 9 +++ schema/actions.schema.json | 4 ++ schema/commands.schema.json | 6 +- schema/errors.schema.json | 51 ++++++++++++++- schema/notifications.schema.json | 4 ++ schema/state.schema.json | 4 ++ scripts/generate-csharp.ts | 17 +++++ scripts/generate-go.ts | 20 ++++++ scripts/generate-kotlin.ts | 8 ++- scripts/generate-markdown.ts | 2 +- scripts/generate-rust.ts | 23 +++++++ scripts/generate-swift.ts | 8 ++- scripts/kotlin-constructor-order.test.ts | 4 +- types/channels-chat/commands.ts | 14 ++++- types/channels-root/state.ts | 13 ++++ types/common/errors.ts | 40 ++++++++++++ types/messages.test.ts | 57 +++++++++++++++++ .../057-create-chat-fork-initial-message.json | 50 +++++++++++++++ .../058-initial-message-rejected-error.json | 58 +++++++++++++++++ .../059-fork-initial-message-capability.json | 62 +++++++++++++++++++ 39 files changed, 735 insertions(+), 21 deletions(-) create mode 100644 docs/.changes/20261008-fork-initial-message.json create mode 100644 types/test-cases/round-trips/057-create-chat-fork-initial-message.json create mode 100644 types/test-cases/round-trips/058-initial-message-rejected-error.json create mode 100644 types/test-cases/round-trips/059-fork-initial-message-capability.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 8767313bc..92adb3825 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -788,7 +788,17 @@ public sealed record CreateChatParams /// Chat URI (client-chosen, e.g. `ahp-chat:/<uuid>`). public required string Chat { get; init; } - /// Optional initial message for the new chat. + /// Optional initial message for the new chat. + /// + /// This existing field remains ungated, including for legacy fork requests. + /// A client that needs the stronger composed fork-and-message guarantees MUST + /// require both {@link MultipleChatsCapability.fork} and + /// {@link MultipleChatsCapability.forkWithInitialMessage}. For that + /// capability-backed operation, a host that has created the fork but can + /// definitely not accept this message MUST return `InitialMessageRejected` + /// (-32012) with the created chat URI and underlying rejection cause. A + /// successful `null` result confirms message acceptance, not completion of + /// the message's model turn. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Message? InitialMessage { get; init; } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Errors.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Errors.generated.cs index 02a52462b..e1ff60b15 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Errors.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Errors.generated.cs @@ -45,6 +45,8 @@ public static class AhpErrorCodes public const int AlreadyExists = -32010; /// An optimistic-concurrency precondition failed. Returned when a request carries a precondition token that no longer matches the receiver's current state — for example, `resourceWrite` with an `ifMatch` etag that has been superseded by a concurrent write. Callers SHOULD re-read the resource (e.g. via `resourceResolve`) and decide whether to retry the operation with the fresh token or surface the conflict to the user. public const int Conflict = -32011; + /// A fork chat was created, but its requested initial message was definitely rejected before it could be accepted for processing. + public const int InitialMessageRejected = -32012; } /// Detail payload of an AuthRequired (-32007) error. @@ -65,3 +67,19 @@ public sealed record UnsupportedProtocolVersionErrorData { public required List SupportedVersions { get; init; } } + +/// Detail payload of an InitialMessageRejected (-32012) error. +public sealed record InitialMessageRejectedErrorData +{ + public required string Chat { get; init; } + public required InitialMessageRejectedCause Cause { get; init; } +} + +/// The preserved underlying error for an InitialMessageRejected error. +public sealed record InitialMessageRejectedCause +{ + public required int Code { get; init; } + public required string Message { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonElement? Data { get; init; } +} diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index 5ee1af5c7..bf10c4a11 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -232,6 +232,8 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(Implementation))] [JsonSerializable(typeof(InitializeParams))] [JsonSerializable(typeof(InitializeResult))] +[JsonSerializable(typeof(InitialMessageRejectedCause))] +[JsonSerializable(typeof(InitialMessageRejectedErrorData))] [JsonSerializable(typeof(InputRequestResponsePart))] [JsonSerializable(typeof(InvokeChangesetOperationParams))] [JsonSerializable(typeof(InvokeChangesetOperationResult))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 09febe732..19d51a850 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2009,6 +2009,19 @@ public sealed record MultipleChatsCapability /// MUST omit `title` when calling `createChat`. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? InitialTitle { get; init; } + + /// The agent supports the stronger composed fork-and-initial-message + /// operation. Clients relying on its preserved client context, definite + /// acceptance or rejection result, and prompt-aware naming context MUST + /// require this and {@link MultipleChatsCapability.fork} to be `true`. + /// The naming context is available to the host's normal strategy at its + /// normal time; it does not guarantee title generation. + /// + /// This attestation applies only to the stronger combined operation. It does + /// not reject or otherwise change existing ungated `initialMessage` requests, + /// including legacy fork requests on third-party hosts. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? ForkWithInitialMessage { get; init; } } /// Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index e2694ee35..efeacf317 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -500,6 +500,16 @@ type CreateChatParams struct { // Chat URI (client-chosen, e.g. `ahp-chat:/`). Chat URI `json:"chat"` // Optional initial message for the new chat. + // + // This existing field remains ungated, including for legacy fork requests. + // A client that needs the stronger composed fork-and-message guarantees MUST + // require both {@link MultipleChatsCapability.fork} and + // {@link MultipleChatsCapability.forkWithInitialMessage}. For that + // capability-backed operation, a host that has created the fork but can + // definitely not accept this message MUST return `InitialMessageRejected` + // (-32012) with the created chat URI and underlying rejection cause. A + // successful `null` result confirms message acceptance, not completion of + // the message's model turn. InitialMessage *Message `json:"initialMessage,omitempty"` // Optional source chat and source turn. // diff --git a/clients/go/ahptypes/errors.generated.go b/clients/go/ahptypes/errors.generated.go index e6a4fd395..fdebb2505 100644 --- a/clients/go/ahptypes/errors.generated.go +++ b/clients/go/ahptypes/errors.generated.go @@ -4,6 +4,8 @@ package ahptypes +import "encoding/json" + // ─── Standard JSON-RPC Error Codes ───────────────────────────────────────── // Standard JSON-RPC 2.0 error codes. @@ -32,10 +34,11 @@ const ( ErrorCodeTurnInProgress int32 = -32004 ErrorCodeUnsupportedProtocolVersion int32 = -32005 // -32006 is intentionally reserved and unassigned. - ErrorCodeAuthRequired int32 = -32007 - ErrorCodeNotFound int32 = -32008 - ErrorCodePermissionDenied int32 = -32009 - ErrorCodeAlreadyExists int32 = -32010 + ErrorCodeAuthRequired int32 = -32007 + ErrorCodeNotFound int32 = -32008 + ErrorCodePermissionDenied int32 = -32009 + ErrorCodeAlreadyExists int32 = -32010 + ErrorCodeInitialMessageRejected int32 = -32012 ) // AhpErrorCode is the type alias used by AHP application error codes. @@ -63,3 +66,18 @@ type PermissionDeniedErrorData struct { type UnsupportedProtocolVersionErrorData struct { SupportedVersions []string `json:"supportedVersions"` } + +// InitialMessageRejectedErrorData is the detail payload of an +// InitialMessageRejected (-32012) error. +type InitialMessageRejectedErrorData struct { + Chat URI `json:"chat"` + Cause InitialMessageRejectedCause `json:"cause"` +} + +// InitialMessageRejectedCause is the preserved underlying error for an +// InitialMessageRejected error. +type InitialMessageRejectedCause struct { + Code int32 `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data,omitempty"` +} diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 4a607e364..d73de576f 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -744,6 +744,17 @@ type MultipleChatsCapability struct { // A later explicit rename is still allowed. When absent or `false`, clients // MUST omit `title` when calling `createChat`. InitialTitle *bool `json:"initialTitle,omitempty"` + // The agent supports the stronger composed fork-and-initial-message + // operation. Clients relying on its preserved client context, definite + // acceptance or rejection result, and prompt-aware naming context MUST + // require this and {@link MultipleChatsCapability.fork} to be `true`. + // The naming context is available to the host's normal strategy at its + // normal time; it does not guarantee title generation. + // + // This attestation applies only to the stronger combined operation. It does + // not reject or otherwise change existing ungated `initialMessage` requests, + // including legacy fork requests on third-party hosts. + ForkWithInitialMessage *bool `json:"forkWithInitialMessage,omitempty"` } // Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 26c86c022..100925e48 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -718,6 +718,16 @@ data class CreateChatParams( val chat: String, /** * Optional initial message for the new chat. + * + * This existing field remains ungated, including for legacy fork requests. + * A client that needs the stronger composed fork-and-message guarantees MUST + * require both {@link MultipleChatsCapability.fork} and + * {@link MultipleChatsCapability.forkWithInitialMessage}. For that + * capability-backed operation, a host that has created the fork but can + * definitely not accept this message MUST return `InitialMessageRejected` + * (-32012) with the created chat URI and underlying rejection cause. A + * successful `null` result confirms message acceptance, not completion of + * the message's model turn. */ val initialMessage: Message? = null, /** diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt index c1ba5c42b..c5edf3854 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt @@ -57,6 +57,10 @@ object AhpErrorCodes { const val PERMISSION_DENIED: Int = -32009 /** The target resource already exists and the operation does not allow overwriting */ const val ALREADY_EXISTS: Int = -32010 + /** An optimistic-concurrency precondition failed */ + const val CONFLICT: Int = -32011 + /** A fork chat was created, but its initial message was rejected */ + const val INITIAL_MESSAGE_REJECTED: Int = -32012 } // ─── Error Detail Payloads ────────────────────────────────────────────────── @@ -90,3 +94,31 @@ data class UnsupportedProtocolVersionErrorData( */ val supportedVersions: List ) + +@Serializable +data class InitialMessageRejectedErrorData( + /** + * URI of the successfully created fork chat. + */ + val chat: String, + /** + * Underlying reason the initial message could not be accepted. + */ + val cause: InitialMessageRejectedCause +) + +@Serializable +data class InitialMessageRejectedCause( + /** + * Underlying JSON-RPC or application error code. + */ + val code: Long, + /** + * Underlying human-readable error message. + */ + val message: String, + /** + * Underlying structured error detail, when supplied. + */ + val data: JsonElement? = null +) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index c802790ac..eb713da89 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -1439,7 +1439,20 @@ data class MultipleChatsCapability( * A later explicit rename is still allowed. When absent or `false`, clients * MUST omit `title` when calling `createChat`. */ - val initialTitle: Boolean? = null + val initialTitle: Boolean? = null, + /** + * The agent supports the stronger composed fork-and-initial-message + * operation. Clients relying on its preserved client context, definite + * acceptance or rejection result, and prompt-aware naming context MUST + * require this and {@link MultipleChatsCapability.fork} to be `true`. + * The naming context is available to the host's normal strategy at its + * normal time; it does not guarantee title generation. + * + * This attestation applies only to the stronger combined operation. It does + * not reject or otherwise change existing ungated `initialMessage` requests, + * including legacy fork requests on third-party hosts. + */ + val forkWithInitialMessage: Boolean? = null ) @Serializable diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt index 3948cb1e4..90fdf0b0c 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt @@ -31,5 +31,6 @@ class ConstructorCompatibilityTest { assertTrue(multipleChats.fork == true) assertTrue(multipleChats.sideChat == true) assertNull(multipleChats.initialTitle) + assertNull(multipleChats.forkWithInitialMessage) } } diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 2478e7b4c..4b65c7db6 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -728,6 +728,16 @@ pub struct CreateChatParams { /// Chat URI (client-chosen, e.g. `ahp-chat:/`). pub chat: Uri, /// Optional initial message for the new chat. + /// + /// This existing field remains ungated, including for legacy fork requests. + /// A client that needs the stronger composed fork-and-message guarantees MUST + /// require both {@link MultipleChatsCapability.fork} and + /// {@link MultipleChatsCapability.forkWithInitialMessage}. For that + /// capability-backed operation, a host that has created the fork but can + /// definitely not accept this message MUST return `InitialMessageRejected` + /// (-32012) with the created chat URI and underlying rejection cause. A + /// successful `null` result confirms message acceptance, not completion of + /// the message's model turn. #[serde(default, skip_serializing_if = "Option::is_none")] pub initial_message: Option, /// Optional source chat and source turn. diff --git a/clients/rust/crates/ahp-types/src/errors.rs b/clients/rust/crates/ahp-types/src/errors.rs index 3c181c259..bf7e9083c 100644 --- a/clients/rust/crates/ahp-types/src/errors.rs +++ b/clients/rust/crates/ahp-types/src/errors.rs @@ -55,6 +55,8 @@ pub mod ahp_error_codes { pub const ALREADY_EXISTS: i32 = -32010; /// An optimistic-concurrency precondition failed: a request's precondition token (e.g. `ResourceWriteParams.if_match`) no longer matches the resource's current state. pub const CONFLICT: i32 = -32011; + /// A fork chat was created, but its initial message was rejected. + pub const INITIAL_MESSAGE_REJECTED: i32 = -32012; } /// Type alias: AHP application error code. @@ -93,3 +95,22 @@ pub struct UnsupportedProtocolVersionErrorData { /// SemVer range constraint (e.g. `">=0.1.0 <0.3.0"` or `"^0.2.0"`). pub supported_versions: Vec, } + +/// Details carried in the `data` field of an `InitialMessageRejected` +/// (-32012) error. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InitialMessageRejectedErrorData { + pub chat: Uri, + pub cause: InitialMessageRejectedCause, +} + +/// The preserved underlying error for an `InitialMessageRejected` error. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InitialMessageRejectedCause { + pub code: i64, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data: Option, +} diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index d9d0cca02..a36475cc5 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1729,6 +1729,18 @@ pub struct MultipleChatsCapability { /// MUST omit `title` when calling `createChat`. #[serde(default, skip_serializing_if = "Option::is_none")] pub initial_title: Option, + /// The agent supports the stronger composed fork-and-initial-message + /// operation. Clients relying on its preserved client context, definite + /// acceptance or rejection result, and prompt-aware naming context MUST + /// require this and {@link MultipleChatsCapability.fork} to be `true`. + /// The naming context is available to the host's normal strategy at its + /// normal time; it does not guarantee title generation. + /// + /// This attestation applies only to the stronger combined operation. It does + /// not reject or otherwise change existing ungated `initialMessage` requests, + /// including legacy fork requests on third-party hosts. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fork_with_initial_message: Option, } /// Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 163135b2b..810b78c0c 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -777,6 +777,16 @@ public struct CreateChatParams: Codable, Sendable { /// Chat URI (client-chosen, e.g. `ahp-chat:/`). public var chat: String /// Optional initial message for the new chat. + /// + /// This existing field remains ungated, including for legacy fork requests. + /// A client that needs the stronger composed fork-and-message guarantees MUST + /// require both {@link MultipleChatsCapability.fork} and + /// {@link MultipleChatsCapability.forkWithInitialMessage}. For that + /// capability-backed operation, a host that has created the fork but can + /// definitely not accept this message MUST return `InitialMessageRejected` + /// (-32012) with the created chat URI and underlying rejection cause. A + /// successful `null` result confirms message acceptance, not completion of + /// the message's model turn. public var initialMessage: Message? /// Optional source chat and source turn. /// diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift index bd3d1ddd2..1d7744db1 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift @@ -39,6 +39,10 @@ public enum AhpErrorCodes { public static let permissionDenied = -32009 /// The target resource already exists and the operation does not allow overwriting public static let alreadyExists = -32010 + /// An optimistic-concurrency precondition failed + public static let conflict = -32011 + /// A fork chat was created, but its initial message was rejected + public static let initialMessageRejected = -32012 } // MARK: - Error Detail Payloads @@ -81,3 +85,37 @@ public struct UnsupportedProtocolVersionErrorData: Codable, Sendable { self.supportedVersions = supportedVersions } } + +public struct InitialMessageRejectedErrorData: Codable, Sendable { + /// URI of the successfully created fork chat. + public var chat: String + /// Underlying reason the initial message could not be accepted. + public var cause: InitialMessageRejectedCause + + public init( + chat: String, + cause: InitialMessageRejectedCause + ) { + self.chat = chat + self.cause = cause + } +} + +public struct InitialMessageRejectedCause: Codable, Sendable { + /// Underlying JSON-RPC or application error code. + public var code: Int + /// Underlying human-readable error message. + public var message: String + /// Underlying structured error detail, when supplied. + public var data: AnyCodable? + + public init( + code: Int, + message: String, + data: AnyCodable? = nil + ) { + self.code = code + self.message = message + self.data = data + } +} diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 9f4a05361..e779e25a7 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1415,15 +1415,28 @@ public struct MultipleChatsCapability: Codable, Sendable { /// A later explicit rename is still allowed. When absent or `false`, clients /// MUST omit `title` when calling `createChat`. public var initialTitle: Bool? + /// The agent supports the stronger composed fork-and-initial-message + /// operation. Clients relying on its preserved client context, definite + /// acceptance or rejection result, and prompt-aware naming context MUST + /// require this and {@link MultipleChatsCapability.fork} to be `true`. + /// The naming context is available to the host's normal strategy at its + /// normal time; it does not guarantee title generation. + /// + /// This attestation applies only to the stronger combined operation. It does + /// not reject or otherwise change existing ungated `initialMessage` requests, + /// including legacy fork requests on third-party hosts. + public var forkWithInitialMessage: Bool? public init( fork: Bool? = nil, sideChat: Bool? = nil, - initialTitle: Bool? = nil + initialTitle: Bool? = nil, + forkWithInitialMessage: Bool? = nil ) { self.fork = fork self.sideChat = sideChat self.initialTitle = initialTitle + self.forkWithInitialMessage = forkWithInitialMessage } } diff --git a/docs/.changes/20261008-fork-initial-message.json b/docs/.changes/20261008-fork-initial-message.json new file mode 100644 index 000000000..d46434a2d --- /dev/null +++ b/docs/.changes/20261008-fork-initial-message.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Capability-gated initial messages for forked chats, including a typed rejection that preserves the created chat and underlying cause." +} diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index f1f7a26e7..c77876ec0 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -104,6 +104,14 @@ fully replaces the catalogue or clears it when `changesets` is absent. `selection: { text, responsePartId? }`, an immutable selected-text snapshot captured when the host accepts `createChat`. +`initialMessage` remains available without a capability, including for legacy +fork requests on third-party hosts. A client that relies on the stronger +composed fork-and-message guarantees MUST require both +`multipleChats.fork: true` and `multipleChats.forkWithInitialMessage: true`; +the latter capability attests that the agent supports that stronger operation. +Its absence does not retroactively reject or otherwise gate legacy +`initialMessage` requests. + The server allocates the chat URI and adds the chat to the session's catalog (`session/chatAdded` on the session channel) before returning. Clients MUST send `title` only when the selected agent advertises @@ -125,11 +133,35 @@ Clients MUST also gate source-based creation using the selected - `fork: true` permits `source.kind: "fork"`. - `sideChat: true` permits `source.kind: "sideChat"`. -Absence or `false` means the corresponding source form is unsupported. The host -MUST reject an unsupported source. It MUST also reject a source chat outside the -target session, an unknown source chat or turn, or a source that names the chat -being created. For forks, the host MUST additionally reject any source whose -`kind` is not `"fork"` — forks only target completed turns. +- `fork: true` and `forkWithInitialMessage: true` together attest the stronger + capability-backed fork-with-initial-message operation. +Absence or `false` means the corresponding source form is unsupported, except +that absence of `forkWithInitialMessage` does not reject an existing legacy +fork request carrying `initialMessage`. The host MUST reject an unsupported +source. It MUST also reject a source chat outside the target session, an +unknown source chat or turn, or a source that names the chat being created. For +forks, the host MUST additionally reject any source whose `kind` is not +`"fork"` — forks only target completed turns. + +For the capability-backed fork-with-initial-message operation, the host admits +and attaches the fork before accepting the first user message, and applies that +message's model and agent selection normally. It makes the source title and the +new prompt available to its normal naming strategy; the protocol does not +prescribe a naming algorithm. An explicit non-empty `title` still wins over +automatic naming. + +The normal `null` `createChat` success result for this operation means that the +chat was created and the initial message was accepted; it does not mean the +new chat's model turn completed. If the host has created the fork but can +definitely determine before accepting the initial message that it must fail, it +MUST return `InitialMessageRejected` (`-32012`). Its error data carries the +created `chat` URI and the full underlying `cause` (`code`, `message`, and +optional `data`), including authentication details. A client facing an unknown +network outcome MUST NOT infer this rejection; it should reconcile chat state. +When a capability-backed request names an already existing target chat, the +host returns `AlreadyExists` and MUST NOT send the initial message. This does +not alter existing no-message/default-chat creation or other legacy +`initialMessage` behavior. For side chats, `turnId` is a stable identity, not a lifecycle snapshot. Hosts and clients resolve it against the source chat's current `activeTurn` or its diff --git a/docs/specification/root-channel.md b/docs/specification/root-channel.md index b9c8b8006..b145c0640 100644 --- a/docs/specification/root-channel.md +++ b/docs/specification/root-channel.md @@ -47,6 +47,15 @@ When `initialTitle` is absent or `false`, clients MUST omit `title` from multi-chat capabilities permit it; the host then chooses the initial title normally. +`forkWithInitialMessage: true`, together with `fork: true`, attests the +stronger composed operation that combines a fork source with +[`CreateChatParams.initialMessage`](/reference/chat#createchatparams). Clients +that rely on its preserved context, acceptance/rejection, and prompt-aware +naming timing MUST require both capabilities before issuing it; this does not +guarantee title generation. This capability is deliberately narrow: it does +not gate the long-standing `initialMessage` field, including legacy fork uses +on third-party hosts. + ### Paginating the catalogue A large catalogue can be fetched incrementally. [`listSessions`](/reference/root#listsessions) accepts an optional `limit` (the maximum number of entries the client wants in the page — the server SHOULD respect it, but MAY return fewer and MAY impose its own upper cap) and an optional opaque `cursor`. The result carries the page in `items` plus an optional `nextCursor`: diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 7425eb5ca..25a060fca 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -3227,6 +3227,10 @@ "initialTitle": { "type": "boolean", "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." + }, + "forkWithInitialMessage": { + "type": "boolean", + "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 6ad2bcd94..aedb437c6 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1319,7 +1319,7 @@ }, "initialMessage": { "$ref": "#/$defs/Message", - "description": "Optional initial message for the new chat." + "description": "Optional initial message for the new chat.\n\nThis existing field remains ungated, including for legacy fork requests.\nA client that needs the stronger composed fork-and-message guarantees MUST\nrequire both {@link MultipleChatsCapability.fork} and\n{@link MultipleChatsCapability.forkWithInitialMessage}. For that\ncapability-backed operation, a host that has created the fork but can\ndefinitely not accept this message MUST return `InitialMessageRejected`\n(-32012) with the created chat URI and underlying rejection cause. A\nsuccessful `null` result confirms message acceptance, not completion of\nthe message's model turn." }, "source": { "$ref": "#/$defs/ChatSource", @@ -2407,6 +2407,10 @@ "initialTitle": { "type": "boolean", "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." + }, + "forkWithInitialMessage": { + "type": "boolean", + "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 4aeaf4e8b..33e50d91e 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -74,6 +74,45 @@ "supportedVersions" ] }, + "InitialMessageRejectedErrorData": { + "type": "object", + "description": "Details carried in the `data` field of an `InitialMessageRejected` (-32012)\nerror.\n\nThe fork chat identified by `chat` was created successfully. `cause`\npreserves the underlying rejection, including structured details such as an\nauthentication challenge, so clients can inspect or surface it.", + "properties": { + "chat": { + "$ref": "#/$defs/URI", + "description": "URI of the successfully created fork chat." + }, + "cause": { + "$ref": "#/$defs/InitialMessageRejectedCause", + "description": "Underlying reason the initial message could not be accepted." + } + }, + "required": [ + "chat", + "cause" + ] + }, + "InitialMessageRejectedCause": { + "type": "object", + "description": "The preserved underlying error for an `InitialMessageRejected` error.", + "properties": { + "code": { + "type": "number", + "description": "Underlying JSON-RPC or application error code." + }, + "message": { + "type": "string", + "description": "Underlying human-readable error message." + }, + "data": { + "description": "Underlying structured error detail, when supplied." + } + }, + "required": [ + "code", + "message" + ] + }, "AhpErrorDetailsMap": { "type": "object", "description": "Maps each AHP error code that carries structured `data` to the type of\nthat data.\n\nError codes not present in this map either have no `data` payload or\ncarry an unspecified payload that callers SHOULD treat as `unknown`.", @@ -86,12 +125,16 @@ }, "[AhpErrorCodes.UnsupportedProtocolVersion]": { "$ref": "#/$defs/UnsupportedProtocolVersionErrorData" + }, + "[AhpErrorCodes.InitialMessageRejected]": { + "$ref": "#/$defs/InitialMessageRejectedErrorData" } }, "required": [ "[AhpErrorCodes.AuthRequired]", "[AhpErrorCodes.PermissionDenied]", - "[AhpErrorCodes.UnsupportedProtocolVersion]" + "[AhpErrorCodes.UnsupportedProtocolVersion]", + "[AhpErrorCodes.InitialMessageRejected]" ] }, "Icon": { @@ -666,6 +709,10 @@ "initialTitle": { "type": "boolean", "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." + }, + "forkWithInitialMessage": { + "type": "boolean", + "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, @@ -7303,7 +7350,7 @@ }, "initialMessage": { "$ref": "#/$defs/Message", - "description": "Optional initial message for the new chat." + "description": "Optional initial message for the new chat.\n\nThis existing field remains ungated, including for legacy fork requests.\nA client that needs the stronger composed fork-and-message guarantees MUST\nrequire both {@link MultipleChatsCapability.fork} and\n{@link MultipleChatsCapability.forkWithInitialMessage}. For that\ncapability-backed operation, a host that has created the fork but can\ndefinitely not accept this message MUST return `InitialMessageRejected`\n(-32012) with the created chat URI and underlying rejection cause. A\nsuccessful `null` result confirms message acceptance, not completion of\nthe message's model turn." }, "source": { "$ref": "#/$defs/ChatSource", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index d3cd41a46..7c1597c9c 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -844,6 +844,10 @@ "initialTitle": { "type": "boolean", "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." + }, + "forkWithInitialMessage": { + "type": "boolean", + "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, diff --git a/schema/state.schema.json b/schema/state.schema.json index bc389e97b..1da2475fe 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -577,6 +577,10 @@ "initialTitle": { "type": "boolean", "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." + }, + "forkWithInitialMessage": { + "type": "boolean", + "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index 61e397f03..cd3ce915c 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -2562,6 +2562,22 @@ public sealed record UnsupportedProtocolVersionErrorData { public required List SupportedVersions { get; init; } } + +/// Detail payload of an InitialMessageRejected (-32012) error. +public sealed record InitialMessageRejectedErrorData +{ + public required string Chat { get; init; } + public required InitialMessageRejectedCause Cause { get; init; } +} + +/// The preserved underlying error for an InitialMessageRejected error. +public sealed record InitialMessageRejectedCause +{ + public required int Code { get; init; } + public required string Message { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonElement? Data { get; init; } +} `; } @@ -2831,6 +2847,7 @@ function checkExhaustiveness(project: Project): void { 'SessionInputRequest', 'BackgroundWork', 'ToolCallConfirmationState', 'ToolCallRiskAssessment', 'ReconnectResult', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', + 'InitialMessageRejectedErrorData', 'InitialMessageRejectedCause', 'AhpError', 'AhpErrorDetailsMap', 'AhpErrorCode', 'AhpErrorCodeWithData', 'JsonRpcErrorCode', 'ChangesetOperationTarget', 'CustomizationEnablement', 'ChatOrigin', 'ChatInputQuestion', 'ChatInputAnswerValue', 'ChatInputAnswer', diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 80e675d9d..26d9666c9 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -2097,6 +2097,8 @@ function generateNotificationsFile(project: Project): string { function generateErrorsFile(): string { return `${GENERATED_BANNER} +import "encoding/json" + // ─── Standard JSON-RPC Error Codes ───────────────────────────────────────── // Standard JSON-RPC 2.0 error codes. @@ -2129,6 +2131,7 @@ const ( \tErrorCodeNotFound int32 = -32008 \tErrorCodePermissionDenied int32 = -32009 \tErrorCodeAlreadyExists int32 = -32010 +\tErrorCodeInitialMessageRejected int32 = -32012 ) // AhpErrorCode is the type alias used by AHP application error codes. @@ -2156,6 +2159,21 @@ type PermissionDeniedErrorData struct { type UnsupportedProtocolVersionErrorData struct { \tSupportedVersions []string \`json:"supportedVersions"\` } + +// InitialMessageRejectedErrorData is the detail payload of an +// InitialMessageRejected (-32012) error. +type InitialMessageRejectedErrorData struct { +\tChat URI \`json:"chat"\` +\tCause InitialMessageRejectedCause \`json:"cause"\` +} + +// InitialMessageRejectedCause is the preserved underlying error for an +// InitialMessageRejected error. +type InitialMessageRejectedCause struct { +\tCode int32 \`json:"code"\` +\tMessage string \`json:"message"\` +\tData json.RawMessage \`json:"data,omitempty"\` +} `; } @@ -2462,6 +2480,8 @@ function checkExhaustiveness(project: Project): void { 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', + 'InitialMessageRejectedErrorData', + 'InitialMessageRejectedCause', 'AhpError', 'AhpErrorDetailsMap', 'AhpErrorCode', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 3b979e9d8..64c4c6f5a 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -2149,12 +2149,16 @@ function generateErrorsFile(project: Project): string { lines.push(' const val PERMISSION_DENIED: Int = -32009'); lines.push(' /** The target resource already exists and the operation does not allow overwriting */'); lines.push(' const val ALREADY_EXISTS: Int = -32010'); + lines.push(' /** An optimistic-concurrency precondition failed */'); + lines.push(' const val CONFLICT: Int = -32011'); + lines.push(' /** A fork chat was created, but its initial message was rejected */'); + lines.push(' const val INITIAL_MESSAGE_REJECTED: Int = -32012'); lines.push('}'); lines.push(''); lines.push('// ─── Error Detail Payloads ──────────────────────────────────────────────────'); lines.push(''); - for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData']) { + for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'InitialMessageRejectedErrorData', 'InitialMessageRejectedCause']) { try { lines.push(generateDataClassFromInterface(project, ifaceName)); lines.push(''); @@ -2417,6 +2421,8 @@ function checkExhaustiveness(project: Project): void { 'AuthRequiredErrorData', // emitted by generateErrorsFile() 'PermissionDeniedErrorData', // emitted by generateErrorsFile() 'UnsupportedProtocolVersionErrorData', // emitted by generateErrorsFile() + 'InitialMessageRejectedErrorData', // emitted by generateErrorsFile() + 'InitialMessageRejectedCause', // emitted by generateErrorsFile() 'AhpError', // typed via JsonRpcError; not a Kotlin data class 'AhpErrorDetailsMap', // type-level mapping; not a Kotlin type 'AhpErrorCode', // type-level alias over AhpErrorCodes const enum diff --git a/scripts/generate-markdown.ts b/scripts/generate-markdown.ts index e2c0b9497..bc7eb8c38 100644 --- a/scripts/generate-markdown.ts +++ b/scripts/generate-markdown.ts @@ -1252,7 +1252,7 @@ function generateErrorCodesPage(project: Project): string { // Typed error-data shapes lines.push('## Typed Error Data\n'); lines.push('A handful of error codes carry a typed `data` payload. The mapping is captured by `AhpErrorDetailsMap`; the typed `AhpError` union narrows `data` based on the code.\n'); - for (const name of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'AhpErrorDetailsMap']) { + for (const name of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'InitialMessageRejectedErrorData', 'InitialMessageRejectedCause', 'AhpErrorDetailsMap']) { const iface = errorsFile.getInterface(name); if (iface) lines.push(renderInterfaceBlock(iface)); } diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 8d0accf0c..54a5b2014 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -2131,6 +2131,8 @@ pub mod ahp_error_codes { pub const ALREADY_EXISTS: i32 = -32010; /// An optimistic-concurrency precondition failed: a request's precondition token (e.g. \`ResourceWriteParams.if_match\`) no longer matches the resource's current state. pub const CONFLICT: i32 = -32011; + /// A fork chat was created, but its initial message was rejected. + pub const INITIAL_MESSAGE_REJECTED: i32 = -32012; } /// Type alias: AHP application error code. @@ -2169,6 +2171,25 @@ pub struct UnsupportedProtocolVersionErrorData { /// SemVer range constraint (e.g. \`">=0.1.0 <0.3.0"\` or \`"^0.2.0"\`). pub supported_versions: Vec, } + +/// Details carried in the \`data\` field of an \`InitialMessageRejected\` +/// (-32012) error. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InitialMessageRejectedErrorData { + pub chat: Uri, + pub cause: InitialMessageRejectedCause, +} + +/// The preserved underlying error for an \`InitialMessageRejected\` error. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InitialMessageRejectedCause { + pub code: i64, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data: Option, +} `; } @@ -2392,6 +2413,8 @@ function checkExhaustiveness(project: Project): void { 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', + 'InitialMessageRejectedErrorData', + 'InitialMessageRejectedCause', 'AhpError', 'AhpErrorDetailsMap', 'AhpErrorCode', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 8a23e60fb..6d1bb9052 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -2039,11 +2039,15 @@ function generateErrorsFile(project: Project): string { lines.push(' public static let permissionDenied = -32009'); lines.push(' /// The target resource already exists and the operation does not allow overwriting'); lines.push(' public static let alreadyExists = -32010'); + lines.push(' /// An optimistic-concurrency precondition failed'); + lines.push(' public static let conflict = -32011'); + lines.push(' /// A fork chat was created, but its initial message was rejected'); + lines.push(' public static let initialMessageRejected = -32012'); lines.push('}'); lines.push(''); lines.push('// MARK: - Error Detail Payloads\n'); - for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData']) { + for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'InitialMessageRejectedErrorData', 'InitialMessageRejectedCause']) { try { lines.push(generateStructFromInterface(project, ifaceName)); lines.push(''); @@ -2433,6 +2437,8 @@ function checkExhaustiveness(project: Project): void { 'AuthRequiredErrorData', // emitted by generateErrorsFile() 'PermissionDeniedErrorData', // emitted by generateErrorsFile() 'UnsupportedProtocolVersionErrorData', // emitted by generateErrorsFile() + 'InitialMessageRejectedErrorData', // emitted by generateErrorsFile() + 'InitialMessageRejectedCause', // emitted by generateErrorsFile() 'AhpError', // typed via JsonRpcError; not a Swift struct 'AhpErrorDetailsMap', // type-level mapping; not a Swift struct 'AhpErrorCode', // type-level alias over AhpErrorCodes const enum diff --git a/scripts/kotlin-constructor-order.test.ts b/scripts/kotlin-constructor-order.test.ts index 49f4251d2..44e9bd74c 100644 --- a/scripts/kotlin-constructor-order.test.ts +++ b/scripts/kotlin-constructor-order.test.ts @@ -20,13 +20,13 @@ describe('generated Kotlin constructor order', () => { ); }); - it('appends MultipleChatsCapability.initialTitle after the existing constructor properties', () => { + it('appends MultipleChatsCapability capability fields after the existing constructor properties', () => { assert.deepEqual( getConstructorPropertyOrder( new URL('../clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt', import.meta.url), 'MultipleChatsCapability', ), - ['fork', 'sideChat', 'initialTitle'], + ['fork', 'sideChat', 'initialTitle', 'forkWithInitialMessage'], ); }); }); diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 4807f0b7d..4147c1db5 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -88,7 +88,19 @@ export interface CreateChatParams extends BaseParams { channel: URI; /** Chat URI (client-chosen, e.g. `ahp-chat:/`). */ chat: URI; - /** Optional initial message for the new chat. */ + /** + * Optional initial message for the new chat. + * + * This existing field remains ungated, including for legacy fork requests. + * A client that needs the stronger composed fork-and-message guarantees MUST + * require both {@link MultipleChatsCapability.fork} and + * {@link MultipleChatsCapability.forkWithInitialMessage}. For that + * capability-backed operation, a host that has created the fork but can + * definitely not accept this message MUST return `InitialMessageRejected` + * (-32012) with the created chat URI and underlying rejection cause. A + * successful `null` result confirms message acceptance, not completion of + * the message's model turn. + */ initialMessage?: Message; /** * Optional source chat and source turn. diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index 048b12d32..8a79999b9 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -161,6 +161,19 @@ export interface MultipleChatsCapability { * MUST omit `title` when calling `createChat`. */ initialTitle?: boolean; + /** + * The agent supports the stronger composed fork-and-initial-message + * operation. Clients relying on its preserved client context, definite + * acceptance or rejection result, and prompt-aware naming context MUST + * require this and {@link MultipleChatsCapability.fork} to be `true`. + * The naming context is available to the host's normal strategy at its + * normal time; it does not guarantee title generation. + * + * This attestation applies only to the stronger combined operation. It does + * not reject or otherwise change existing ungated `initialMessage` requests, + * including legacy fork requests on third-party hosts. + */ + forkWithInitialMessage?: boolean; } /** diff --git a/types/common/errors.ts b/types/common/errors.ts index 66afca0f2..5685ee7ba 100644 --- a/types/common/errors.ts +++ b/types/common/errors.ts @@ -8,6 +8,7 @@ */ import type { ProtectedResourceMetadata } from './state.js'; +import type { URI } from './state.js'; import type { ResourceRequestParams } from './commands.js'; // ─── Standard JSON-RPC Codes ───────────────────────────────────────────────── @@ -93,6 +94,11 @@ export const AhpErrorCodes = { * fresh token or surface the conflict to the user. */ Conflict: -32011, + /** + * A fork chat was created, but its requested initial message was definitely + * rejected before it could be accepted for processing. + */ + InitialMessageRejected: -32012, } as const; /** Union type of all AHP application error codes. */ @@ -157,6 +163,39 @@ export interface UnsupportedProtocolVersionErrorData { supportedVersions: string[]; } +/** + * Details carried in the `data` field of an `InitialMessageRejected` (-32012) + * error. + * + * The fork chat identified by `chat` was created successfully. `cause` + * preserves the underlying rejection, including structured details such as an + * authentication challenge, so clients can inspect or surface it. + * + * @category Error Details + * @version 1 + */ +export interface InitialMessageRejectedErrorData { + /** URI of the successfully created fork chat. */ + chat: URI; + /** Underlying reason the initial message could not be accepted. */ + cause: InitialMessageRejectedCause; +} + +/** + * The preserved underlying error for an `InitialMessageRejected` error. + * + * @category Error Details + * @version 1 + */ +export interface InitialMessageRejectedCause { + /** Underlying JSON-RPC or application error code. */ + code: number; + /** Underlying human-readable error message. */ + message: string; + /** Underlying structured error detail, when supplied. */ + data?: unknown; +} + /** * Maps each AHP error code that carries structured `data` to the type of * that data. @@ -171,6 +210,7 @@ export interface AhpErrorDetailsMap { [AhpErrorCodes.AuthRequired]: AuthRequiredErrorData; [AhpErrorCodes.PermissionDenied]: PermissionDeniedErrorData; [AhpErrorCodes.UnsupportedProtocolVersion]: UnsupportedProtocolVersionErrorData; + [AhpErrorCodes.InitialMessageRejected]: InitialMessageRejectedErrorData; } /** AHP error codes that carry a structured `data` payload. */ diff --git a/types/messages.test.ts b/types/messages.test.ts index f2474fe89..867e99641 100644 --- a/types/messages.test.ts +++ b/types/messages.test.ts @@ -10,6 +10,9 @@ import { strict as assert } from 'node:assert'; import { readFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { AhpErrorCodes, type AhpError } from './common/errors.js'; +import type { CreateChatParams } from './channels-chat/commands.js'; +import type { MultipleChatsCapability } from './channels-root/state.js'; const root = resolve(dirname(fileURLToPath(import.meta.url))); @@ -213,3 +216,57 @@ describe('command params/result naming conventions', () => { }); } }); + +describe('fork initial-message contract', () => { + it('models the additive capability-backed fork and initial-message operation', () => { + const capability: MultipleChatsCapability = { + fork: true, + forkWithInitialMessage: true, + }; + const params: CreateChatParams = { + channel: 'ahp-session:/session', + chat: 'ahp-chat:/fork', + source: { + kind: 'fork', + chat: 'ahp-chat:/source', + turnId: 'turn-1', + }, + initialMessage: { + text: 'Continue from this point.', + origin: { kind: 'user' }, + }, + }; + + assert.equal(capability.forkWithInitialMessage, true); + assert.equal(params.source?.kind, 'fork'); + assert.equal(params.initialMessage?.text, 'Continue from this point.'); + }); + + it('preserves the created chat and structured underlying cause on rejection', () => { + const error: AhpError = { + code: AhpErrorCodes.InitialMessageRejected, + message: 'The fork was created, but the initial message was rejected.', + data: { + chat: 'ahp-chat:/fork', + cause: { + code: AhpErrorCodes.AuthRequired, + message: 'Authentication required for the selected model.', + data: { + resources: [{ resource: 'https://example.test/models' }], + }, + }, + }, + }; + + assert.deepStrictEqual(error.data, { + chat: 'ahp-chat:/fork', + cause: { + code: AhpErrorCodes.AuthRequired, + message: 'Authentication required for the selected model.', + data: { + resources: [{ resource: 'https://example.test/models' }], + }, + }, + }); + }); +}); diff --git a/types/test-cases/round-trips/057-create-chat-fork-initial-message.json b/types/test-cases/round-trips/057-create-chat-fork-initial-message.json new file mode 100644 index 000000000..2aa569bf1 --- /dev/null +++ b/types/test-cases/round-trips/057-create-chat-fork-initial-message.json @@ -0,0 +1,50 @@ +{ + "name": "create-chat-fork-initial-message", + "group": "A", + "description": "CreateChatParams preserves an initial user message, including its model and agent selections, when it is combined with a fork source.", + "type": "CreateChatParams", + "input": { + "channel": "ahp-session:/s1", + "chat": "ahp-chat:/fork", + "source": { + "kind": "fork", + "chat": "ahp-chat:/source", + "turnId": "turn-12" + }, + "initialMessage": { + "text": "Continue by extracting the parser.", + "origin": { + "kind": "user" + }, + "model": { + "id": "model-2" + }, + "agent": { + "uri": "ahp-agent:/agent-2" + } + } + }, + "acceptableOutputs": [ + { + "channel": "ahp-session:/s1", + "chat": "ahp-chat:/fork", + "source": { + "kind": "fork", + "chat": "ahp-chat:/source", + "turnId": "turn-12" + }, + "initialMessage": { + "text": "Continue by extracting the parser.", + "origin": { + "kind": "user" + }, + "model": { + "id": "model-2" + }, + "agent": { + "uri": "ahp-agent:/agent-2" + } + } + } + ] +} diff --git a/types/test-cases/round-trips/058-initial-message-rejected-error.json b/types/test-cases/round-trips/058-initial-message-rejected-error.json new file mode 100644 index 000000000..5c03f4d9d --- /dev/null +++ b/types/test-cases/round-trips/058-initial-message-rejected-error.json @@ -0,0 +1,58 @@ +{ + "name": "initial-message-rejected-error", + "group": "A", + "description": "An InitialMessageRejected JSON-RPC error preserves the created fork chat URI and the complete underlying authentication cause.", + "type": "JsonRpcMessage", + "input": { + "jsonrpc": "2.0", + "id": 12, + "error": { + "code": -32012, + "message": "The fork was created, but the initial message was rejected.", + "data": { + "chat": "ahp-chat:/fork", + "cause": { + "code": -32007, + "message": "Authentication required for the selected model.", + "data": { + "resources": [ + { + "resource": "https://example.test/models", + "authorization_servers": [ + "https://auth.example.test" + ] + } + ] + } + } + } + } + }, + "acceptableOutputs": [ + { + "jsonrpc": "2.0", + "id": 12, + "error": { + "code": -32012, + "message": "The fork was created, but the initial message was rejected.", + "data": { + "chat": "ahp-chat:/fork", + "cause": { + "code": -32007, + "message": "Authentication required for the selected model.", + "data": { + "resources": [ + { + "resource": "https://example.test/models", + "authorization_servers": [ + "https://auth.example.test" + ] + } + ] + } + } + } + } + } + ] +} diff --git a/types/test-cases/round-trips/059-fork-initial-message-capability.json b/types/test-cases/round-trips/059-fork-initial-message-capability.json new file mode 100644 index 000000000..e3e1f68a3 --- /dev/null +++ b/types/test-cases/round-trips/059-fork-initial-message-capability.json @@ -0,0 +1,62 @@ +{ + "name": "agent-fork-initial-message-capability", + "group": "A", + "description": "AgentInfo advertises support for the composed fork-and-initial-message operation through capabilities.multipleChats.forkWithInitialMessage.", + "type": "InitializeResult", + "input": { + "protocolVersion": "0.9.0", + "serverSeq": 3, + "snapshots": [ + { + "resource": "ahp-root://", + "state": { + "agents": [ + { + "provider": "example", + "displayName": "Example", + "description": "Example agent", + "models": [], + "capabilities": { + "multipleChats": { + "fork": true, + "initialTitle": true, + "forkWithInitialMessage": true + } + } + } + ] + }, + "fromSeq": 3 + } + ] + }, + "acceptableOutputs": [ + { + "protocolVersion": "0.9.0", + "serverSeq": 3, + "snapshots": [ + { + "resource": "ahp-root://", + "state": { + "agents": [ + { + "provider": "example", + "displayName": "Example", + "description": "Example agent", + "models": [], + "capabilities": { + "multipleChats": { + "fork": true, + "initialTitle": true, + "forkWithInitialMessage": true + } + } + } + ] + }, + "fromSeq": 3 + } + ] + } + ] +} From 8f9b8e1f03c6c5d4e7111eacab84d1829c1f8fea Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 14 Oct 2026 04:47:41 +1100 Subject: [PATCH 3/4] protocol: simplify initial chat titles and message errors Keep title optional without new capabilities or naming mandates. Reuse normal turn errors for initial messages and regenerate the schemas and clients. Update compatibility fixtures and the TypeScript round-trip dispatcher. Addresses review on #526; companion implementation microsoft/vscode#341230. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Commands.generated.cs | 23 +------ .../Generated/Errors.generated.cs | 18 ------ .../JsonSerializerContext.generated.cs | 2 - .../Generated/State.generated.cs | 20 ------ clients/go/ahptypes/commands.generated.go | 19 ------ clients/go/ahptypes/errors.generated.go | 26 ++------ clients/go/ahptypes/state.generated.go | 16 ----- .../generated/Commands.generated.kt | 19 ------ .../generated/Errors.generated.kt | 32 ---------- .../generated/State.generated.kt | 22 +------ .../ConstructorCompatibilityTest.kt | 9 +-- clients/rust/crates/ahp-types/src/commands.rs | 19 ------ clients/rust/crates/ahp-types/src/errors.rs | 21 ------- clients/rust/crates/ahp-types/src/state.rs | 18 ------ .../Generated/Commands.generated.swift | 19 ------ .../Generated/Errors.generated.swift | 38 ------------ .../Generated/State.generated.swift | 22 +------ .../typescript/test/types-round-trip.test.ts | 3 +- .../.changes/20261005-initial-chat-title.json | 2 +- ...otlin-trailing-constructor-properties.json | 2 +- ...261006-go-initial-title-struct-fields.json | 2 +- ...1006-rust-initial-title-struct-fields.json | 2 +- .../20261008-fork-initial-message.json | 4 -- docs/specification/chat-channel.md | 62 +++---------------- docs/specification/root-channel.md | 24 ------- schema/actions.schema.json | 8 --- schema/commands.schema.json | 12 +--- schema/errors.schema.json | 57 +---------------- schema/notifications.schema.json | 8 --- schema/state.schema.json | 8 --- scripts/generate-csharp.ts | 17 ----- scripts/generate-go.ts | 20 ------ scripts/generate-kotlin.ts | 8 +-- scripts/generate-markdown.ts | 2 +- scripts/generate-rust.ts | 23 ------- scripts/generate-swift.ts | 8 +-- scripts/kotlin-constructor-order.test.ts | 10 --- types/channels-chat/commands.ts | 27 +------- types/channels-root/state.ts | 20 ------ types/common/errors.ts | 40 ------------ types/messages.test.ts | 57 ----------------- .../056-agent-initial-title-capability.json | 60 ------------------ .../058-initial-message-rejected-error.json | 58 ----------------- .../059-fork-initial-message-capability.json | 62 ------------------- 44 files changed, 35 insertions(+), 914 deletions(-) delete mode 100644 docs/.changes/20261008-fork-initial-message.json delete mode 100644 types/test-cases/round-trips/056-agent-initial-title-capability.json delete mode 100644 types/test-cases/round-trips/058-initial-message-rejected-error.json delete mode 100644 types/test-cases/round-trips/059-fork-initial-message-capability.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 92adb3825..709f6dbed 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -788,17 +788,7 @@ public sealed record CreateChatParams /// Chat URI (client-chosen, e.g. `ahp-chat:/<uuid>`). public required string Chat { get; init; } - /// Optional initial message for the new chat. - /// - /// This existing field remains ungated, including for legacy fork requests. - /// A client that needs the stronger composed fork-and-message guarantees MUST - /// require both {@link MultipleChatsCapability.fork} and - /// {@link MultipleChatsCapability.forkWithInitialMessage}. For that - /// capability-backed operation, a host that has created the fork but can - /// definitely not accept this message MUST return `InitialMessageRejected` - /// (-32012) with the created chat URI and underlying rejection cause. A - /// successful `null` result confirms message acceptance, not completion of - /// the message's model turn. + /// Optional initial message for the new chat. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Message? InitialMessage { get; init; } @@ -830,16 +820,7 @@ public sealed record CreateChatParams [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; init; } - /// Client-selected initial display title for the new chat. - /// - /// Clients MUST NOT supply this field unless the selected agent advertises - /// {@link MultipleChatsCapability.initialTitle}. - /// - /// A server honoring this field MUST trim leading and trailing whitespace. - /// If the result is empty, it MUST treat the field as absent and choose its - /// normal initial title. Otherwise, it MUST preserve the remaining text - /// exactly: it MUST NOT collapse internal whitespace, strip quotes or - /// punctuation, or truncate it. The protocol defines no maximum title length. + /// Client-selected initial display title for the new chat. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Title { get; init; } } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Errors.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Errors.generated.cs index e1ff60b15..02a52462b 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Errors.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Errors.generated.cs @@ -45,8 +45,6 @@ public static class AhpErrorCodes public const int AlreadyExists = -32010; /// An optimistic-concurrency precondition failed. Returned when a request carries a precondition token that no longer matches the receiver's current state — for example, `resourceWrite` with an `ifMatch` etag that has been superseded by a concurrent write. Callers SHOULD re-read the resource (e.g. via `resourceResolve`) and decide whether to retry the operation with the fresh token or surface the conflict to the user. public const int Conflict = -32011; - /// A fork chat was created, but its requested initial message was definitely rejected before it could be accepted for processing. - public const int InitialMessageRejected = -32012; } /// Detail payload of an AuthRequired (-32007) error. @@ -67,19 +65,3 @@ public sealed record UnsupportedProtocolVersionErrorData { public required List SupportedVersions { get; init; } } - -/// Detail payload of an InitialMessageRejected (-32012) error. -public sealed record InitialMessageRejectedErrorData -{ - public required string Chat { get; init; } - public required InitialMessageRejectedCause Cause { get; init; } -} - -/// The preserved underlying error for an InitialMessageRejected error. -public sealed record InitialMessageRejectedCause -{ - public required int Code { get; init; } - public required string Message { get; init; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonElement? Data { get; init; } -} diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index bf10c4a11..5ee1af5c7 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -232,8 +232,6 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(Implementation))] [JsonSerializable(typeof(InitializeParams))] [JsonSerializable(typeof(InitializeResult))] -[JsonSerializable(typeof(InitialMessageRejectedCause))] -[JsonSerializable(typeof(InitialMessageRejectedErrorData))] [JsonSerializable(typeof(InputRequestResponsePart))] [JsonSerializable(typeof(InvokeChangesetOperationParams))] [JsonSerializable(typeof(InvokeChangesetOperationResult))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 19d51a850..ccff10f6c 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -2002,26 +2002,6 @@ public sealed record MultipleChatsCapability /// time. Side-chat support always implies multi-chat support. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? SideChat { get; init; } - - /// The agent honors {@link CreateChatParams.title} as the chat's first - /// published title and does not replace it through automatic title generation. - /// A later explicit rename is still allowed. When absent or `false`, clients - /// MUST omit `title` when calling `createChat`. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? InitialTitle { get; init; } - - /// The agent supports the stronger composed fork-and-initial-message - /// operation. Clients relying on its preserved client context, definite - /// acceptance or rejection result, and prompt-aware naming context MUST - /// require this and {@link MultipleChatsCapability.fork} to be `true`. - /// The naming context is available to the host's normal strategy at its - /// normal time; it does not guarantee title generation. - /// - /// This attestation applies only to the stronger combined operation. It does - /// not reject or otherwise change existing ungated `initialMessage` requests, - /// including legacy fork requests on third-party hosts. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? ForkWithInitialMessage { get; init; } } /// Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index efeacf317..e3c8182bc 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -500,16 +500,6 @@ type CreateChatParams struct { // Chat URI (client-chosen, e.g. `ahp-chat:/`). Chat URI `json:"chat"` // Optional initial message for the new chat. - // - // This existing field remains ungated, including for legacy fork requests. - // A client that needs the stronger composed fork-and-message guarantees MUST - // require both {@link MultipleChatsCapability.fork} and - // {@link MultipleChatsCapability.forkWithInitialMessage}. For that - // capability-backed operation, a host that has created the fork but can - // definitely not accept this message MUST return `InitialMessageRejected` - // (-32012) with the created chat URI and underlying rejection cause. A - // successful `null` result confirms message acceptance, not completion of - // the message's model turn. InitialMessage *Message `json:"initialMessage,omitempty"` // Optional source chat and source turn. // @@ -536,15 +526,6 @@ type CreateChatParams struct { // {@link AgentCapabilities.multipleWorkingDirectories}. WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Client-selected initial display title for the new chat. - // - // Clients MUST NOT supply this field unless the selected agent advertises - // {@link MultipleChatsCapability.initialTitle}. - // - // A server honoring this field MUST trim leading and trailing whitespace. - // If the result is empty, it MUST treat the field as absent and choose its - // normal initial title. Otherwise, it MUST preserve the remaining text - // exactly: it MUST NOT collapse internal whitespace, strip quotes or - // punctuation, or truncate it. The protocol defines no maximum title length. Title *string `json:"title,omitempty"` } diff --git a/clients/go/ahptypes/errors.generated.go b/clients/go/ahptypes/errors.generated.go index fdebb2505..e6a4fd395 100644 --- a/clients/go/ahptypes/errors.generated.go +++ b/clients/go/ahptypes/errors.generated.go @@ -4,8 +4,6 @@ package ahptypes -import "encoding/json" - // ─── Standard JSON-RPC Error Codes ───────────────────────────────────────── // Standard JSON-RPC 2.0 error codes. @@ -34,11 +32,10 @@ const ( ErrorCodeTurnInProgress int32 = -32004 ErrorCodeUnsupportedProtocolVersion int32 = -32005 // -32006 is intentionally reserved and unassigned. - ErrorCodeAuthRequired int32 = -32007 - ErrorCodeNotFound int32 = -32008 - ErrorCodePermissionDenied int32 = -32009 - ErrorCodeAlreadyExists int32 = -32010 - ErrorCodeInitialMessageRejected int32 = -32012 + ErrorCodeAuthRequired int32 = -32007 + ErrorCodeNotFound int32 = -32008 + ErrorCodePermissionDenied int32 = -32009 + ErrorCodeAlreadyExists int32 = -32010 ) // AhpErrorCode is the type alias used by AHP application error codes. @@ -66,18 +63,3 @@ type PermissionDeniedErrorData struct { type UnsupportedProtocolVersionErrorData struct { SupportedVersions []string `json:"supportedVersions"` } - -// InitialMessageRejectedErrorData is the detail payload of an -// InitialMessageRejected (-32012) error. -type InitialMessageRejectedErrorData struct { - Chat URI `json:"chat"` - Cause InitialMessageRejectedCause `json:"cause"` -} - -// InitialMessageRejectedCause is the preserved underlying error for an -// InitialMessageRejected error. -type InitialMessageRejectedCause struct { - Code int32 `json:"code"` - Message string `json:"message"` - Data json.RawMessage `json:"data,omitempty"` -} diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index d73de576f..6f75e003d 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -739,22 +739,6 @@ type MultipleChatsCapability struct { // the host snapshots the available partial assistant response at creation // time. Side-chat support always implies multi-chat support. SideChat *bool `json:"sideChat,omitempty"` - // The agent honors {@link CreateChatParams.title} as the chat's first - // published title and does not replace it through automatic title generation. - // A later explicit rename is still allowed. When absent or `false`, clients - // MUST omit `title` when calling `createChat`. - InitialTitle *bool `json:"initialTitle,omitempty"` - // The agent supports the stronger composed fork-and-initial-message - // operation. Clients relying on its preserved client context, definite - // acceptance or rejection result, and prompt-aware naming context MUST - // require this and {@link MultipleChatsCapability.fork} to be `true`. - // The naming context is available to the host's normal strategy at its - // normal time; it does not guarantee title generation. - // - // This attestation applies only to the stronger combined operation. It does - // not reject or otherwise change existing ungated `initialMessage` requests, - // including legacy fork requests on third-party hosts. - ForkWithInitialMessage *bool `json:"forkWithInitialMessage,omitempty"` } // Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 100925e48..dea9d4523 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -718,16 +718,6 @@ data class CreateChatParams( val chat: String, /** * Optional initial message for the new chat. - * - * This existing field remains ungated, including for legacy fork requests. - * A client that needs the stronger composed fork-and-message guarantees MUST - * require both {@link MultipleChatsCapability.fork} and - * {@link MultipleChatsCapability.forkWithInitialMessage}. For that - * capability-backed operation, a host that has created the fork but can - * definitely not accept this message MUST return `InitialMessageRejected` - * (-32012) with the created chat URI and underlying rejection cause. A - * successful `null` result confirms message acceptance, not completion of - * the message's model turn. */ val initialMessage: Message? = null, /** @@ -760,15 +750,6 @@ data class CreateChatParams( val workingDirectories: List? = null, /** * Client-selected initial display title for the new chat. - * - * Clients MUST NOT supply this field unless the selected agent advertises - * {@link MultipleChatsCapability.initialTitle}. - * - * A server honoring this field MUST trim leading and trailing whitespace. - * If the result is empty, it MUST treat the field as absent and choose its - * normal initial title. Otherwise, it MUST preserve the remaining text - * exactly: it MUST NOT collapse internal whitespace, strip quotes or - * punctuation, or truncate it. The protocol defines no maximum title length. */ val title: String? = null ) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt index c5edf3854..c1ba5c42b 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt @@ -57,10 +57,6 @@ object AhpErrorCodes { const val PERMISSION_DENIED: Int = -32009 /** The target resource already exists and the operation does not allow overwriting */ const val ALREADY_EXISTS: Int = -32010 - /** An optimistic-concurrency precondition failed */ - const val CONFLICT: Int = -32011 - /** A fork chat was created, but its initial message was rejected */ - const val INITIAL_MESSAGE_REJECTED: Int = -32012 } // ─── Error Detail Payloads ────────────────────────────────────────────────── @@ -94,31 +90,3 @@ data class UnsupportedProtocolVersionErrorData( */ val supportedVersions: List ) - -@Serializable -data class InitialMessageRejectedErrorData( - /** - * URI of the successfully created fork chat. - */ - val chat: String, - /** - * Underlying reason the initial message could not be accepted. - */ - val cause: InitialMessageRejectedCause -) - -@Serializable -data class InitialMessageRejectedCause( - /** - * Underlying JSON-RPC or application error code. - */ - val code: Long, - /** - * Underlying human-readable error message. - */ - val message: String, - /** - * Underlying structured error detail, when supplied. - */ - val data: JsonElement? = null -) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index eb713da89..c9bb7e8a2 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -1432,27 +1432,7 @@ data class MultipleChatsCapability( * the host snapshots the available partial assistant response at creation * time. Side-chat support always implies multi-chat support. */ - val sideChat: Boolean? = null, - /** - * The agent honors {@link CreateChatParams.title} as the chat's first - * published title and does not replace it through automatic title generation. - * A later explicit rename is still allowed. When absent or `false`, clients - * MUST omit `title` when calling `createChat`. - */ - val initialTitle: Boolean? = null, - /** - * The agent supports the stronger composed fork-and-initial-message - * operation. Clients relying on its preserved client context, definite - * acceptance or rejection result, and prompt-aware naming context MUST - * require this and {@link MultipleChatsCapability.fork} to be `true`. - * The naming context is available to the host's normal strategy at its - * normal time; it does not guarantee title generation. - * - * This attestation applies only to the stronger combined operation. It does - * not reject or otherwise change existing ungated `initialMessage` requests, - * including legacy fork requests on third-party hosts. - */ - val forkWithInitialMessage: Boolean? = null + val sideChat: Boolean? = null ) @Serializable diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt index 90fdf0b0c..f1f48ef0b 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt @@ -4,15 +4,13 @@ import com.microsoft.agenthostprotocol.generated.CreateChatParams import com.microsoft.agenthostprotocol.generated.Message import com.microsoft.agenthostprotocol.generated.MessageKind import com.microsoft.agenthostprotocol.generated.MessageOrigin -import com.microsoft.agenthostprotocol.generated.MultipleChatsCapability import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNull -import kotlin.test.assertTrue class ConstructorCompatibilityTest { @Test - fun `old full positional constructors retain their source semantics`() { + fun `old full positional CreateChatParams constructor retains its source semantics`() { val message = Message("Hello", MessageOrigin(MessageKind.USER)) val createChat = CreateChatParams( @@ -23,14 +21,9 @@ class ConstructorCompatibilityTest { null, listOf("file:///workspace"), ) - val multipleChats = MultipleChatsCapability(true, true) assertEquals(message, createChat.initialMessage) assertEquals(listOf("file:///workspace"), createChat.workingDirectories) assertNull(createChat.title) - assertTrue(multipleChats.fork == true) - assertTrue(multipleChats.sideChat == true) - assertNull(multipleChats.initialTitle) - assertNull(multipleChats.forkWithInitialMessage) } } diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 4b65c7db6..cc21110cd 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -728,16 +728,6 @@ pub struct CreateChatParams { /// Chat URI (client-chosen, e.g. `ahp-chat:/`). pub chat: Uri, /// Optional initial message for the new chat. - /// - /// This existing field remains ungated, including for legacy fork requests. - /// A client that needs the stronger composed fork-and-message guarantees MUST - /// require both {@link MultipleChatsCapability.fork} and - /// {@link MultipleChatsCapability.forkWithInitialMessage}. For that - /// capability-backed operation, a host that has created the fork but can - /// definitely not accept this message MUST return `InitialMessageRejected` - /// (-32012) with the created chat URI and underlying rejection cause. A - /// successful `null` result confirms message acceptance, not completion of - /// the message's model turn. #[serde(default, skip_serializing_if = "Option::is_none")] pub initial_message: Option, /// Optional source chat and source turn. @@ -767,15 +757,6 @@ pub struct CreateChatParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, /// Client-selected initial display title for the new chat. - /// - /// Clients MUST NOT supply this field unless the selected agent advertises - /// {@link MultipleChatsCapability.initialTitle}. - /// - /// A server honoring this field MUST trim leading and trailing whitespace. - /// If the result is empty, it MUST treat the field as absent and choose its - /// normal initial title. Otherwise, it MUST preserve the remaining text - /// exactly: it MUST NOT collapse internal whitespace, strip quotes or - /// punctuation, or truncate it. The protocol defines no maximum title length. #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, } diff --git a/clients/rust/crates/ahp-types/src/errors.rs b/clients/rust/crates/ahp-types/src/errors.rs index bf7e9083c..3c181c259 100644 --- a/clients/rust/crates/ahp-types/src/errors.rs +++ b/clients/rust/crates/ahp-types/src/errors.rs @@ -55,8 +55,6 @@ pub mod ahp_error_codes { pub const ALREADY_EXISTS: i32 = -32010; /// An optimistic-concurrency precondition failed: a request's precondition token (e.g. `ResourceWriteParams.if_match`) no longer matches the resource's current state. pub const CONFLICT: i32 = -32011; - /// A fork chat was created, but its initial message was rejected. - pub const INITIAL_MESSAGE_REJECTED: i32 = -32012; } /// Type alias: AHP application error code. @@ -95,22 +93,3 @@ pub struct UnsupportedProtocolVersionErrorData { /// SemVer range constraint (e.g. `">=0.1.0 <0.3.0"` or `"^0.2.0"`). pub supported_versions: Vec, } - -/// Details carried in the `data` field of an `InitialMessageRejected` -/// (-32012) error. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InitialMessageRejectedErrorData { - pub chat: Uri, - pub cause: InitialMessageRejectedCause, -} - -/// The preserved underlying error for an `InitialMessageRejected` error. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InitialMessageRejectedCause { - pub code: i64, - pub message: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub data: Option, -} diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index a36475cc5..b7d5cd6be 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1723,24 +1723,6 @@ pub struct MultipleChatsCapability { /// time. Side-chat support always implies multi-chat support. #[serde(default, skip_serializing_if = "Option::is_none")] pub side_chat: Option, - /// The agent honors {@link CreateChatParams.title} as the chat's first - /// published title and does not replace it through automatic title generation. - /// A later explicit rename is still allowed. When absent or `false`, clients - /// MUST omit `title` when calling `createChat`. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub initial_title: Option, - /// The agent supports the stronger composed fork-and-initial-message - /// operation. Clients relying on its preserved client context, definite - /// acceptance or rejection result, and prompt-aware naming context MUST - /// require this and {@link MultipleChatsCapability.fork} to be `true`. - /// The naming context is available to the host's normal strategy at its - /// normal time; it does not guarantee title generation. - /// - /// This attestation applies only to the stronger combined operation. It does - /// not reject or otherwise change existing ungated `initialMessage` requests, - /// including legacy fork requests on third-party hosts. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fork_with_initial_message: Option, } /// Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability. diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 810b78c0c..cf105d59a 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -777,16 +777,6 @@ public struct CreateChatParams: Codable, Sendable { /// Chat URI (client-chosen, e.g. `ahp-chat:/`). public var chat: String /// Optional initial message for the new chat. - /// - /// This existing field remains ungated, including for legacy fork requests. - /// A client that needs the stronger composed fork-and-message guarantees MUST - /// require both {@link MultipleChatsCapability.fork} and - /// {@link MultipleChatsCapability.forkWithInitialMessage}. For that - /// capability-backed operation, a host that has created the fork but can - /// definitely not accept this message MUST return `InitialMessageRejected` - /// (-32012) with the created chat URI and underlying rejection cause. A - /// successful `null` result confirms message acceptance, not completion of - /// the message's model turn. public var initialMessage: Message? /// Optional source chat and source turn. /// @@ -813,15 +803,6 @@ public struct CreateChatParams: Codable, Sendable { /// {@link AgentCapabilities.multipleWorkingDirectories}. public var workingDirectories: [String]? /// Client-selected initial display title for the new chat. - /// - /// Clients MUST NOT supply this field unless the selected agent advertises - /// {@link MultipleChatsCapability.initialTitle}. - /// - /// A server honoring this field MUST trim leading and trailing whitespace. - /// If the result is empty, it MUST treat the field as absent and choose its - /// normal initial title. Otherwise, it MUST preserve the remaining text - /// exactly: it MUST NOT collapse internal whitespace, strip quotes or - /// punctuation, or truncate it. The protocol defines no maximum title length. public var title: String? enum CodingKeys: String, CodingKey { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift index 1d7744db1..bd3d1ddd2 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Errors.generated.swift @@ -39,10 +39,6 @@ public enum AhpErrorCodes { public static let permissionDenied = -32009 /// The target resource already exists and the operation does not allow overwriting public static let alreadyExists = -32010 - /// An optimistic-concurrency precondition failed - public static let conflict = -32011 - /// A fork chat was created, but its initial message was rejected - public static let initialMessageRejected = -32012 } // MARK: - Error Detail Payloads @@ -85,37 +81,3 @@ public struct UnsupportedProtocolVersionErrorData: Codable, Sendable { self.supportedVersions = supportedVersions } } - -public struct InitialMessageRejectedErrorData: Codable, Sendable { - /// URI of the successfully created fork chat. - public var chat: String - /// Underlying reason the initial message could not be accepted. - public var cause: InitialMessageRejectedCause - - public init( - chat: String, - cause: InitialMessageRejectedCause - ) { - self.chat = chat - self.cause = cause - } -} - -public struct InitialMessageRejectedCause: Codable, Sendable { - /// Underlying JSON-RPC or application error code. - public var code: Int - /// Underlying human-readable error message. - public var message: String - /// Underlying structured error detail, when supplied. - public var data: AnyCodable? - - public init( - code: Int, - message: String, - data: AnyCodable? = nil - ) { - self.code = code - self.message = message - self.data = data - } -} diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index e779e25a7..ba255f605 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1410,33 +1410,13 @@ public struct MultipleChatsCapability: Codable, Sendable { /// the host snapshots the available partial assistant response at creation /// time. Side-chat support always implies multi-chat support. public var sideChat: Bool? - /// The agent honors {@link CreateChatParams.title} as the chat's first - /// published title and does not replace it through automatic title generation. - /// A later explicit rename is still allowed. When absent or `false`, clients - /// MUST omit `title` when calling `createChat`. - public var initialTitle: Bool? - /// The agent supports the stronger composed fork-and-initial-message - /// operation. Clients relying on its preserved client context, definite - /// acceptance or rejection result, and prompt-aware naming context MUST - /// require this and {@link MultipleChatsCapability.fork} to be `true`. - /// The naming context is available to the host's normal strategy at its - /// normal time; it does not guarantee title generation. - /// - /// This attestation applies only to the stronger combined operation. It does - /// not reject or otherwise change existing ungated `initialMessage` requests, - /// including legacy fork requests on third-party hosts. - public var forkWithInitialMessage: Bool? public init( fork: Bool? = nil, - sideChat: Bool? = nil, - initialTitle: Bool? = nil, - forkWithInitialMessage: Bool? = nil + sideChat: Bool? = nil ) { self.fork = fork self.sideChat = sideChat - self.initialTitle = initialTitle - self.forkWithInitialMessage = forkWithInitialMessage } } diff --git a/clients/typescript/test/types-round-trip.test.ts b/clients/typescript/test/types-round-trip.test.ts index e7f9aece6..1e8a281b9 100644 --- a/clients/typescript/test/types-round-trip.test.ts +++ b/clients/typescript/test/types-round-trip.test.ts @@ -60,7 +60,7 @@ import type { } from '../src/types/channels-session/state.js'; import type { SessionAddedParams } from '../src/types/channels-root/notifications.js'; import type { Implementation, InitializeResult } from '../src/types/common/commands.js'; -import type { ChatSource } from '../src/types/channels-chat/commands.js'; +import type { ChatSource, CreateChatParams } from '../src/types/channels-chat/commands.js'; // ─── Fixture directory ─────────────────────────────────────────────────────── @@ -243,6 +243,7 @@ function bindToType(file: string, type: string, parsed: unknown): void { case 'PartialSessionSummary': void (parsed as Partial); break; case 'Implementation': void (parsed as Implementation); break; case 'InitializeResult': void (parsed as InitializeResult); break; + case 'CreateChatParams': void (parsed as CreateChatParams); break; case 'ChatSource': void (parsed as ChatSource); break; case 'Snapshot': void (parsed as Snapshot); break; default: diff --git a/docs/.changes/20261005-initial-chat-title.json b/docs/.changes/20261005-initial-chat-title.json index 34d4443a6..74f8bf7dc 100644 --- a/docs/.changes/20261005-initial-chat-title.json +++ b/docs/.changes/20261005-initial-chat-title.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "Optional initial titles for newly created chats, advertised through the multiple-chat capability." + "message": "Optional `CreateChatParams.title` for a client-selected initial display title." } diff --git a/docs/.changes/20261005-kotlin-trailing-constructor-properties.json b/docs/.changes/20261005-kotlin-trailing-constructor-properties.json index 57c48b92d..b76a513f9 100644 --- a/docs/.changes/20261005-kotlin-trailing-constructor-properties.json +++ b/docs/.changes/20261005-kotlin-trailing-constructor-properties.json @@ -1,5 +1,5 @@ { "type": "changed", - "message": "Generated models gain trailing optional constructor properties; Kotlin binaries must be recompiled against the new client version.", + "message": "Generated `CreateChatParams` gains a trailing optional `title` constructor property; Kotlin binaries must be recompiled against the new client version.", "targets": ["kotlin"] } diff --git a/docs/.changes/20261006-go-initial-title-struct-fields.json b/docs/.changes/20261006-go-initial-title-struct-fields.json index c9f0e6742..e587ea48e 100644 --- a/docs/.changes/20261006-go-initial-title-struct-fields.json +++ b/docs/.changes/20261006-go-initial-title-struct-fields.json @@ -1,5 +1,5 @@ { "type": "changed", - "message": "Generated `CreateChatParams.Title` and `MultipleChatsCapability.InitialTitle` fields require unkeyed Go composite literals to add trailing `nil` values or migrate to keyed literals.", + "message": "The generated `CreateChatParams.Title` field requires unkeyed Go composite literals to add a trailing `nil` value or migrate to keyed literals.", "targets": ["go"] } diff --git a/docs/.changes/20261006-rust-initial-title-struct-fields.json b/docs/.changes/20261006-rust-initial-title-struct-fields.json index 8a4fd655f..4364c02c3 100644 --- a/docs/.changes/20261006-rust-initial-title-struct-fields.json +++ b/docs/.changes/20261006-rust-initial-title-struct-fields.json @@ -1,5 +1,5 @@ { "type": "changed", - "message": "Generated `CreateChatParams.title` and `MultipleChatsCapability.initialTitle` fields require existing Rust struct literals to add `None` or use defaults/update syntax.", + "message": "The generated `CreateChatParams.title` field requires existing Rust struct literals to add `None` or use defaults/update syntax.", "targets": ["rust"] } diff --git a/docs/.changes/20261008-fork-initial-message.json b/docs/.changes/20261008-fork-initial-message.json deleted file mode 100644 index d46434a2d..000000000 --- a/docs/.changes/20261008-fork-initial-message.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "added", - "message": "Capability-gated initial messages for forked chats, including a typed rejection that preserves the created chat and underlying cause." -} diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index c77876ec0..2589fd1c0 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -104,64 +104,22 @@ fully replaces the catalogue or clears it when `changesets` is absent. `selection: { text, responsePartId? }`, an immutable selected-text snapshot captured when the host accepts `createChat`. -`initialMessage` remains available without a capability, including for legacy -fork requests on third-party hosts. A client that relies on the stronger -composed fork-and-message guarantees MUST require both -`multipleChats.fork: true` and `multipleChats.forkWithInitialMessage: true`; -the latter capability attests that the agent supports that stronger operation. -Its absence does not retroactively reject or otherwise gate legacy -`initialMessage` requests. - The server allocates the chat URI and adds the chat to the session's catalog (`session/chatAdded` on the session channel) before returning. -Clients MUST send `title` only when the selected agent advertises -[`multipleChats.initialTitle`](/reference/root#multiplechatscapability). A -supporting server MUST trim leading and trailing whitespace. If the result is -empty, it MUST treat `title` as absent and choose its normal initial title. -Otherwise, it MUST use the remaining text in the first published chat summary -and [`ChatState`](/reference/chat#chatstate) and preserve it exactly: it MUST -NOT collapse internal whitespace, strip quotes or punctuation, or truncate it. -The protocol defines no maximum title length. The server MUST NOT replace a -non-empty supplied title through automatic title generation. A later explicit -rename is still allowed. When the capability is absent or `false`, clients MUST -omit `title` but MAY continue creating or forking the chat normally, allowing -the server to choose its initial title. The top-level `title` describes the -target chat and is independent of source provenance. - -Clients MUST also gate source-based creation using the selected +The optional `title` field is additive. Older hosts that do not recognize it ignore it as an unknown field. + +Errors while processing `initialMessage` are reported through the chat's normal turn state, as with any other turn. + +Clients MUST gate source-based creation using the selected [`AgentInfo.capabilities.multipleChats`](/reference/root#multiplechatscapability): - `fork: true` permits `source.kind: "fork"`. - `sideChat: true` permits `source.kind: "sideChat"`. -- `fork: true` and `forkWithInitialMessage: true` together attest the stronger - capability-backed fork-with-initial-message operation. -Absence or `false` means the corresponding source form is unsupported, except -that absence of `forkWithInitialMessage` does not reject an existing legacy -fork request carrying `initialMessage`. The host MUST reject an unsupported -source. It MUST also reject a source chat outside the target session, an -unknown source chat or turn, or a source that names the chat being created. For -forks, the host MUST additionally reject any source whose `kind` is not -`"fork"` — forks only target completed turns. - -For the capability-backed fork-with-initial-message operation, the host admits -and attaches the fork before accepting the first user message, and applies that -message's model and agent selection normally. It makes the source title and the -new prompt available to its normal naming strategy; the protocol does not -prescribe a naming algorithm. An explicit non-empty `title` still wins over -automatic naming. - -The normal `null` `createChat` success result for this operation means that the -chat was created and the initial message was accepted; it does not mean the -new chat's model turn completed. If the host has created the fork but can -definitely determine before accepting the initial message that it must fail, it -MUST return `InitialMessageRejected` (`-32012`). Its error data carries the -created `chat` URI and the full underlying `cause` (`code`, `message`, and -optional `data`), including authentication details. A client facing an unknown -network outcome MUST NOT infer this rejection; it should reconcile chat state. -When a capability-backed request names an already existing target chat, the -host returns `AlreadyExists` and MUST NOT send the initial message. This does -not alter existing no-message/default-chat creation or other legacy -`initialMessage` behavior. +Absence or `false` means the corresponding source form is unsupported. The host +MUST reject an unsupported source. It MUST also reject a source chat outside the +target session, an unknown source chat or turn, or a source that names the chat +being created. For forks, the host MUST additionally reject any source whose +`kind` is not `"fork"` — forks only target completed turns. For side chats, `turnId` is a stable identity, not a lifecycle snapshot. Hosts and clients resolve it against the source chat's current `activeTurn` or its diff --git a/docs/specification/root-channel.md b/docs/specification/root-channel.md index b145c0640..2ccdad606 100644 --- a/docs/specification/root-channel.md +++ b/docs/specification/root-channel.md @@ -32,30 +32,6 @@ RootState { The session list is **not** part of root state. Clients fetch it imperatively via [`listSessions`](/reference/root#listsessions) and patch it from `root/*` notifications described below. -### Agent capabilities - -Each [`AgentInfo`](/reference/root#agentinfo) may advertise optional -capabilities. Under -[`multipleChats`](/reference/root#multiplechatscapability), -`initialTitle: true` guarantees that the host honors -[`CreateChatParams.title`](/reference/chat#createchatparams) as the target -chat's first published title in its summary and state, without replacing it -through automatic title generation. A later explicit rename remains allowed. - -When `initialTitle` is absent or `false`, clients MUST omit `title` from -`createChat`. They MAY still create or fork a chat when the corresponding -multi-chat capabilities permit it; the host then chooses the initial title -normally. - -`forkWithInitialMessage: true`, together with `fork: true`, attests the -stronger composed operation that combines a fork source with -[`CreateChatParams.initialMessage`](/reference/chat#createchatparams). Clients -that rely on its preserved context, acceptance/rejection, and prompt-aware -naming timing MUST require both capabilities before issuing it; this does not -guarantee title generation. This capability is deliberately narrow: it does -not gate the long-standing `initialMessage` field, including legacy fork uses -on third-party hosts. - ### Paginating the catalogue A large catalogue can be fetched incrementally. [`listSessions`](/reference/root#listsessions) accepts an optional `limit` (the maximum number of entries the client wants in the page — the server SHOULD respect it, but MAY return fewer and MAY impose its own upper cap) and an optional opaque `cursor`. The result carries the page in `items` plus an optional `nextCursor`: diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 25a060fca..5f115582d 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -3223,14 +3223,6 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." - }, - "initialTitle": { - "type": "boolean", - "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." - }, - "forkWithInitialMessage": { - "type": "boolean", - "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index aedb437c6..a79c74fb4 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1319,7 +1319,7 @@ }, "initialMessage": { "$ref": "#/$defs/Message", - "description": "Optional initial message for the new chat.\n\nThis existing field remains ungated, including for legacy fork requests.\nA client that needs the stronger composed fork-and-message guarantees MUST\nrequire both {@link MultipleChatsCapability.fork} and\n{@link MultipleChatsCapability.forkWithInitialMessage}. For that\ncapability-backed operation, a host that has created the fork but can\ndefinitely not accept this message MUST return `InitialMessageRejected`\n(-32012) with the created chat URI and underlying rejection cause. A\nsuccessful `null` result confirms message acceptance, not completion of\nthe message's model turn." + "description": "Optional initial message for the new chat." }, "source": { "$ref": "#/$defs/ChatSource", @@ -1334,7 +1334,7 @@ }, "title": { "type": "string", - "description": "Client-selected initial display title for the new chat.\n\nClients MUST NOT supply this field unless the selected agent advertises\n{@link MultipleChatsCapability.initialTitle}.\n\nA server honoring this field MUST trim leading and trailing whitespace.\nIf the result is empty, it MUST treat the field as absent and choose its\nnormal initial title. Otherwise, it MUST preserve the remaining text\nexactly: it MUST NOT collapse internal whitespace, strip quotes or\npunctuation, or truncate it. The protocol defines no maximum title length." + "description": "Client-selected initial display title for the new chat." } }, "required": [ @@ -2403,14 +2403,6 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." - }, - "initialTitle": { - "type": "boolean", - "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." - }, - "forkWithInitialMessage": { - "type": "boolean", - "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 33e50d91e..bff9ed452 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -74,45 +74,6 @@ "supportedVersions" ] }, - "InitialMessageRejectedErrorData": { - "type": "object", - "description": "Details carried in the `data` field of an `InitialMessageRejected` (-32012)\nerror.\n\nThe fork chat identified by `chat` was created successfully. `cause`\npreserves the underlying rejection, including structured details such as an\nauthentication challenge, so clients can inspect or surface it.", - "properties": { - "chat": { - "$ref": "#/$defs/URI", - "description": "URI of the successfully created fork chat." - }, - "cause": { - "$ref": "#/$defs/InitialMessageRejectedCause", - "description": "Underlying reason the initial message could not be accepted." - } - }, - "required": [ - "chat", - "cause" - ] - }, - "InitialMessageRejectedCause": { - "type": "object", - "description": "The preserved underlying error for an `InitialMessageRejected` error.", - "properties": { - "code": { - "type": "number", - "description": "Underlying JSON-RPC or application error code." - }, - "message": { - "type": "string", - "description": "Underlying human-readable error message." - }, - "data": { - "description": "Underlying structured error detail, when supplied." - } - }, - "required": [ - "code", - "message" - ] - }, "AhpErrorDetailsMap": { "type": "object", "description": "Maps each AHP error code that carries structured `data` to the type of\nthat data.\n\nError codes not present in this map either have no `data` payload or\ncarry an unspecified payload that callers SHOULD treat as `unknown`.", @@ -125,16 +86,12 @@ }, "[AhpErrorCodes.UnsupportedProtocolVersion]": { "$ref": "#/$defs/UnsupportedProtocolVersionErrorData" - }, - "[AhpErrorCodes.InitialMessageRejected]": { - "$ref": "#/$defs/InitialMessageRejectedErrorData" } }, "required": [ "[AhpErrorCodes.AuthRequired]", "[AhpErrorCodes.PermissionDenied]", - "[AhpErrorCodes.UnsupportedProtocolVersion]", - "[AhpErrorCodes.InitialMessageRejected]" + "[AhpErrorCodes.UnsupportedProtocolVersion]" ] }, "Icon": { @@ -705,14 +662,6 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." - }, - "initialTitle": { - "type": "boolean", - "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." - }, - "forkWithInitialMessage": { - "type": "boolean", - "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, @@ -7350,7 +7299,7 @@ }, "initialMessage": { "$ref": "#/$defs/Message", - "description": "Optional initial message for the new chat.\n\nThis existing field remains ungated, including for legacy fork requests.\nA client that needs the stronger composed fork-and-message guarantees MUST\nrequire both {@link MultipleChatsCapability.fork} and\n{@link MultipleChatsCapability.forkWithInitialMessage}. For that\ncapability-backed operation, a host that has created the fork but can\ndefinitely not accept this message MUST return `InitialMessageRejected`\n(-32012) with the created chat URI and underlying rejection cause. A\nsuccessful `null` result confirms message acceptance, not completion of\nthe message's model turn." + "description": "Optional initial message for the new chat." }, "source": { "$ref": "#/$defs/ChatSource", @@ -7365,7 +7314,7 @@ }, "title": { "type": "string", - "description": "Client-selected initial display title for the new chat.\n\nClients MUST NOT supply this field unless the selected agent advertises\n{@link MultipleChatsCapability.initialTitle}.\n\nA server honoring this field MUST trim leading and trailing whitespace.\nIf the result is empty, it MUST treat the field as absent and choose its\nnormal initial title. Otherwise, it MUST preserve the remaining text\nexactly: it MUST NOT collapse internal whitespace, strip quotes or\npunctuation, or truncate it. The protocol defines no maximum title length." + "description": "Client-selected initial display title for the new chat." } }, "required": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 7c1597c9c..723ffa747 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -840,14 +840,6 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." - }, - "initialTitle": { - "type": "boolean", - "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." - }, - "forkWithInitialMessage": { - "type": "boolean", - "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, diff --git a/schema/state.schema.json b/schema/state.schema.json index 1da2475fe..043ef7fde 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -573,14 +573,6 @@ "sideChat": { "type": "boolean", "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." - }, - "initialTitle": { - "type": "boolean", - "description": "The agent honors {@link CreateChatParams.title} as the chat's first\npublished title and does not replace it through automatic title generation.\nA later explicit rename is still allowed. When absent or `false`, clients\nMUST omit `title` when calling `createChat`." - }, - "forkWithInitialMessage": { - "type": "boolean", - "description": "The agent supports the stronger composed fork-and-initial-message\noperation. Clients relying on its preserved client context, definite\nacceptance or rejection result, and prompt-aware naming context MUST\nrequire this and {@link MultipleChatsCapability.fork} to be `true`.\nThe naming context is available to the host's normal strategy at its\nnormal time; it does not guarantee title generation.\n\nThis attestation applies only to the stronger combined operation. It does\nnot reject or otherwise change existing ungated `initialMessage` requests,\nincluding legacy fork requests on third-party hosts." } } }, diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index cd3ce915c..61e397f03 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -2562,22 +2562,6 @@ public sealed record UnsupportedProtocolVersionErrorData { public required List SupportedVersions { get; init; } } - -/// Detail payload of an InitialMessageRejected (-32012) error. -public sealed record InitialMessageRejectedErrorData -{ - public required string Chat { get; init; } - public required InitialMessageRejectedCause Cause { get; init; } -} - -/// The preserved underlying error for an InitialMessageRejected error. -public sealed record InitialMessageRejectedCause -{ - public required int Code { get; init; } - public required string Message { get; init; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonElement? Data { get; init; } -} `; } @@ -2847,7 +2831,6 @@ function checkExhaustiveness(project: Project): void { 'SessionInputRequest', 'BackgroundWork', 'ToolCallConfirmationState', 'ToolCallRiskAssessment', 'ReconnectResult', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', - 'InitialMessageRejectedErrorData', 'InitialMessageRejectedCause', 'AhpError', 'AhpErrorDetailsMap', 'AhpErrorCode', 'AhpErrorCodeWithData', 'JsonRpcErrorCode', 'ChangesetOperationTarget', 'CustomizationEnablement', 'ChatOrigin', 'ChatInputQuestion', 'ChatInputAnswerValue', 'ChatInputAnswer', diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 26d9666c9..80e675d9d 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -2097,8 +2097,6 @@ function generateNotificationsFile(project: Project): string { function generateErrorsFile(): string { return `${GENERATED_BANNER} -import "encoding/json" - // ─── Standard JSON-RPC Error Codes ───────────────────────────────────────── // Standard JSON-RPC 2.0 error codes. @@ -2131,7 +2129,6 @@ const ( \tErrorCodeNotFound int32 = -32008 \tErrorCodePermissionDenied int32 = -32009 \tErrorCodeAlreadyExists int32 = -32010 -\tErrorCodeInitialMessageRejected int32 = -32012 ) // AhpErrorCode is the type alias used by AHP application error codes. @@ -2159,21 +2156,6 @@ type PermissionDeniedErrorData struct { type UnsupportedProtocolVersionErrorData struct { \tSupportedVersions []string \`json:"supportedVersions"\` } - -// InitialMessageRejectedErrorData is the detail payload of an -// InitialMessageRejected (-32012) error. -type InitialMessageRejectedErrorData struct { -\tChat URI \`json:"chat"\` -\tCause InitialMessageRejectedCause \`json:"cause"\` -} - -// InitialMessageRejectedCause is the preserved underlying error for an -// InitialMessageRejected error. -type InitialMessageRejectedCause struct { -\tCode int32 \`json:"code"\` -\tMessage string \`json:"message"\` -\tData json.RawMessage \`json:"data,omitempty"\` -} `; } @@ -2480,8 +2462,6 @@ function checkExhaustiveness(project: Project): void { 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', - 'InitialMessageRejectedErrorData', - 'InitialMessageRejectedCause', 'AhpError', 'AhpErrorDetailsMap', 'AhpErrorCode', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 64c4c6f5a..3b979e9d8 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -2149,16 +2149,12 @@ function generateErrorsFile(project: Project): string { lines.push(' const val PERMISSION_DENIED: Int = -32009'); lines.push(' /** The target resource already exists and the operation does not allow overwriting */'); lines.push(' const val ALREADY_EXISTS: Int = -32010'); - lines.push(' /** An optimistic-concurrency precondition failed */'); - lines.push(' const val CONFLICT: Int = -32011'); - lines.push(' /** A fork chat was created, but its initial message was rejected */'); - lines.push(' const val INITIAL_MESSAGE_REJECTED: Int = -32012'); lines.push('}'); lines.push(''); lines.push('// ─── Error Detail Payloads ──────────────────────────────────────────────────'); lines.push(''); - for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'InitialMessageRejectedErrorData', 'InitialMessageRejectedCause']) { + for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData']) { try { lines.push(generateDataClassFromInterface(project, ifaceName)); lines.push(''); @@ -2421,8 +2417,6 @@ function checkExhaustiveness(project: Project): void { 'AuthRequiredErrorData', // emitted by generateErrorsFile() 'PermissionDeniedErrorData', // emitted by generateErrorsFile() 'UnsupportedProtocolVersionErrorData', // emitted by generateErrorsFile() - 'InitialMessageRejectedErrorData', // emitted by generateErrorsFile() - 'InitialMessageRejectedCause', // emitted by generateErrorsFile() 'AhpError', // typed via JsonRpcError; not a Kotlin data class 'AhpErrorDetailsMap', // type-level mapping; not a Kotlin type 'AhpErrorCode', // type-level alias over AhpErrorCodes const enum diff --git a/scripts/generate-markdown.ts b/scripts/generate-markdown.ts index bc7eb8c38..e2c0b9497 100644 --- a/scripts/generate-markdown.ts +++ b/scripts/generate-markdown.ts @@ -1252,7 +1252,7 @@ function generateErrorCodesPage(project: Project): string { // Typed error-data shapes lines.push('## Typed Error Data\n'); lines.push('A handful of error codes carry a typed `data` payload. The mapping is captured by `AhpErrorDetailsMap`; the typed `AhpError` union narrows `data` based on the code.\n'); - for (const name of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'InitialMessageRejectedErrorData', 'InitialMessageRejectedCause', 'AhpErrorDetailsMap']) { + for (const name of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'AhpErrorDetailsMap']) { const iface = errorsFile.getInterface(name); if (iface) lines.push(renderInterfaceBlock(iface)); } diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 54a5b2014..8d0accf0c 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -2131,8 +2131,6 @@ pub mod ahp_error_codes { pub const ALREADY_EXISTS: i32 = -32010; /// An optimistic-concurrency precondition failed: a request's precondition token (e.g. \`ResourceWriteParams.if_match\`) no longer matches the resource's current state. pub const CONFLICT: i32 = -32011; - /// A fork chat was created, but its initial message was rejected. - pub const INITIAL_MESSAGE_REJECTED: i32 = -32012; } /// Type alias: AHP application error code. @@ -2171,25 +2169,6 @@ pub struct UnsupportedProtocolVersionErrorData { /// SemVer range constraint (e.g. \`">=0.1.0 <0.3.0"\` or \`"^0.2.0"\`). pub supported_versions: Vec, } - -/// Details carried in the \`data\` field of an \`InitialMessageRejected\` -/// (-32012) error. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InitialMessageRejectedErrorData { - pub chat: Uri, - pub cause: InitialMessageRejectedCause, -} - -/// The preserved underlying error for an \`InitialMessageRejected\` error. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InitialMessageRejectedCause { - pub code: i64, - pub message: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub data: Option, -} `; } @@ -2413,8 +2392,6 @@ function checkExhaustiveness(project: Project): void { 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', - 'InitialMessageRejectedErrorData', - 'InitialMessageRejectedCause', 'AhpError', 'AhpErrorDetailsMap', 'AhpErrorCode', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 6d1bb9052..8a23e60fb 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -2039,15 +2039,11 @@ function generateErrorsFile(project: Project): string { lines.push(' public static let permissionDenied = -32009'); lines.push(' /// The target resource already exists and the operation does not allow overwriting'); lines.push(' public static let alreadyExists = -32010'); - lines.push(' /// An optimistic-concurrency precondition failed'); - lines.push(' public static let conflict = -32011'); - lines.push(' /// A fork chat was created, but its initial message was rejected'); - lines.push(' public static let initialMessageRejected = -32012'); lines.push('}'); lines.push(''); lines.push('// MARK: - Error Detail Payloads\n'); - for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'InitialMessageRejectedErrorData', 'InitialMessageRejectedCause']) { + for (const ifaceName of ['AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData']) { try { lines.push(generateStructFromInterface(project, ifaceName)); lines.push(''); @@ -2437,8 +2433,6 @@ function checkExhaustiveness(project: Project): void { 'AuthRequiredErrorData', // emitted by generateErrorsFile() 'PermissionDeniedErrorData', // emitted by generateErrorsFile() 'UnsupportedProtocolVersionErrorData', // emitted by generateErrorsFile() - 'InitialMessageRejectedErrorData', // emitted by generateErrorsFile() - 'InitialMessageRejectedCause', // emitted by generateErrorsFile() 'AhpError', // typed via JsonRpcError; not a Swift struct 'AhpErrorDetailsMap', // type-level mapping; not a Swift struct 'AhpErrorCode', // type-level alias over AhpErrorCodes const enum diff --git a/scripts/kotlin-constructor-order.test.ts b/scripts/kotlin-constructor-order.test.ts index 44e9bd74c..3593df232 100644 --- a/scripts/kotlin-constructor-order.test.ts +++ b/scripts/kotlin-constructor-order.test.ts @@ -19,14 +19,4 @@ describe('generated Kotlin constructor order', () => { ['channel', 'meta', 'chat', 'initialMessage', 'source', 'workingDirectories', 'title'], ); }); - - it('appends MultipleChatsCapability capability fields after the existing constructor properties', () => { - assert.deepEqual( - getConstructorPropertyOrder( - new URL('../clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt', import.meta.url), - 'MultipleChatsCapability', - ), - ['fork', 'sideChat', 'initialTitle', 'forkWithInitialMessage'], - ); - }); }); diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 4147c1db5..381cd2ae7 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -88,19 +88,7 @@ export interface CreateChatParams extends BaseParams { channel: URI; /** Chat URI (client-chosen, e.g. `ahp-chat:/`). */ chat: URI; - /** - * Optional initial message for the new chat. - * - * This existing field remains ungated, including for legacy fork requests. - * A client that needs the stronger composed fork-and-message guarantees MUST - * require both {@link MultipleChatsCapability.fork} and - * {@link MultipleChatsCapability.forkWithInitialMessage}. For that - * capability-backed operation, a host that has created the fork but can - * definitely not accept this message MUST return `InitialMessageRejected` - * (-32012) with the created chat URI and underlying rejection cause. A - * successful `null` result confirms message acceptance, not completion of - * the message's model turn. - */ + /** Optional initial message for the new chat. */ initialMessage?: Message; /** * Optional source chat and source turn. @@ -130,18 +118,7 @@ export interface CreateChatParams extends BaseParams { * {@link AgentCapabilities.multipleWorkingDirectories}. */ workingDirectories?: URI[]; - /** - * Client-selected initial display title for the new chat. - * - * Clients MUST NOT supply this field unless the selected agent advertises - * {@link MultipleChatsCapability.initialTitle}. - * - * A server honoring this field MUST trim leading and trailing whitespace. - * If the result is empty, it MUST treat the field as absent and choose its - * normal initial title. Otherwise, it MUST preserve the remaining text - * exactly: it MUST NOT collapse internal whitespace, strip quotes or - * punctuation, or truncate it. The protocol defines no maximum title length. - */ + /** Client-selected initial display title for the new chat. */ title?: string; } diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index 8a79999b9..e9a3bc91b 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -154,26 +154,6 @@ export interface MultipleChatsCapability { * time. Side-chat support always implies multi-chat support. */ sideChat?: boolean; - /** - * The agent honors {@link CreateChatParams.title} as the chat's first - * published title and does not replace it through automatic title generation. - * A later explicit rename is still allowed. When absent or `false`, clients - * MUST omit `title` when calling `createChat`. - */ - initialTitle?: boolean; - /** - * The agent supports the stronger composed fork-and-initial-message - * operation. Clients relying on its preserved client context, definite - * acceptance or rejection result, and prompt-aware naming context MUST - * require this and {@link MultipleChatsCapability.fork} to be `true`. - * The naming context is available to the host's normal strategy at its - * normal time; it does not guarantee title generation. - * - * This attestation applies only to the stronger combined operation. It does - * not reject or otherwise change existing ungated `initialMessage` requests, - * including legacy fork requests on third-party hosts. - */ - forkWithInitialMessage?: boolean; } /** diff --git a/types/common/errors.ts b/types/common/errors.ts index 5685ee7ba..66afca0f2 100644 --- a/types/common/errors.ts +++ b/types/common/errors.ts @@ -8,7 +8,6 @@ */ import type { ProtectedResourceMetadata } from './state.js'; -import type { URI } from './state.js'; import type { ResourceRequestParams } from './commands.js'; // ─── Standard JSON-RPC Codes ───────────────────────────────────────────────── @@ -94,11 +93,6 @@ export const AhpErrorCodes = { * fresh token or surface the conflict to the user. */ Conflict: -32011, - /** - * A fork chat was created, but its requested initial message was definitely - * rejected before it could be accepted for processing. - */ - InitialMessageRejected: -32012, } as const; /** Union type of all AHP application error codes. */ @@ -163,39 +157,6 @@ export interface UnsupportedProtocolVersionErrorData { supportedVersions: string[]; } -/** - * Details carried in the `data` field of an `InitialMessageRejected` (-32012) - * error. - * - * The fork chat identified by `chat` was created successfully. `cause` - * preserves the underlying rejection, including structured details such as an - * authentication challenge, so clients can inspect or surface it. - * - * @category Error Details - * @version 1 - */ -export interface InitialMessageRejectedErrorData { - /** URI of the successfully created fork chat. */ - chat: URI; - /** Underlying reason the initial message could not be accepted. */ - cause: InitialMessageRejectedCause; -} - -/** - * The preserved underlying error for an `InitialMessageRejected` error. - * - * @category Error Details - * @version 1 - */ -export interface InitialMessageRejectedCause { - /** Underlying JSON-RPC or application error code. */ - code: number; - /** Underlying human-readable error message. */ - message: string; - /** Underlying structured error detail, when supplied. */ - data?: unknown; -} - /** * Maps each AHP error code that carries structured `data` to the type of * that data. @@ -210,7 +171,6 @@ export interface AhpErrorDetailsMap { [AhpErrorCodes.AuthRequired]: AuthRequiredErrorData; [AhpErrorCodes.PermissionDenied]: PermissionDeniedErrorData; [AhpErrorCodes.UnsupportedProtocolVersion]: UnsupportedProtocolVersionErrorData; - [AhpErrorCodes.InitialMessageRejected]: InitialMessageRejectedErrorData; } /** AHP error codes that carry a structured `data` payload. */ diff --git a/types/messages.test.ts b/types/messages.test.ts index 867e99641..f2474fe89 100644 --- a/types/messages.test.ts +++ b/types/messages.test.ts @@ -10,9 +10,6 @@ import { strict as assert } from 'node:assert'; import { readFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { AhpErrorCodes, type AhpError } from './common/errors.js'; -import type { CreateChatParams } from './channels-chat/commands.js'; -import type { MultipleChatsCapability } from './channels-root/state.js'; const root = resolve(dirname(fileURLToPath(import.meta.url))); @@ -216,57 +213,3 @@ describe('command params/result naming conventions', () => { }); } }); - -describe('fork initial-message contract', () => { - it('models the additive capability-backed fork and initial-message operation', () => { - const capability: MultipleChatsCapability = { - fork: true, - forkWithInitialMessage: true, - }; - const params: CreateChatParams = { - channel: 'ahp-session:/session', - chat: 'ahp-chat:/fork', - source: { - kind: 'fork', - chat: 'ahp-chat:/source', - turnId: 'turn-1', - }, - initialMessage: { - text: 'Continue from this point.', - origin: { kind: 'user' }, - }, - }; - - assert.equal(capability.forkWithInitialMessage, true); - assert.equal(params.source?.kind, 'fork'); - assert.equal(params.initialMessage?.text, 'Continue from this point.'); - }); - - it('preserves the created chat and structured underlying cause on rejection', () => { - const error: AhpError = { - code: AhpErrorCodes.InitialMessageRejected, - message: 'The fork was created, but the initial message was rejected.', - data: { - chat: 'ahp-chat:/fork', - cause: { - code: AhpErrorCodes.AuthRequired, - message: 'Authentication required for the selected model.', - data: { - resources: [{ resource: 'https://example.test/models' }], - }, - }, - }, - }; - - assert.deepStrictEqual(error.data, { - chat: 'ahp-chat:/fork', - cause: { - code: AhpErrorCodes.AuthRequired, - message: 'Authentication required for the selected model.', - data: { - resources: [{ resource: 'https://example.test/models' }], - }, - }, - }); - }); -}); diff --git a/types/test-cases/round-trips/056-agent-initial-title-capability.json b/types/test-cases/round-trips/056-agent-initial-title-capability.json deleted file mode 100644 index b930486a4..000000000 --- a/types/test-cases/round-trips/056-agent-initial-title-capability.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "agent-initial-title-capability", - "group": "A", - "description": "AgentInfo advertises support for client-selected initial chat titles through capabilities.multipleChats.initialTitle.", - "type": "InitializeResult", - "input": { - "protocolVersion": "0.9.0", - "serverSeq": 3, - "snapshots": [ - { - "resource": "ahp-root://", - "state": { - "agents": [ - { - "provider": "example", - "displayName": "Example", - "description": "Example agent", - "models": [], - "capabilities": { - "multipleChats": { - "fork": true, - "initialTitle": true - } - } - } - ] - }, - "fromSeq": 3 - } - ] - }, - "acceptableOutputs": [ - { - "protocolVersion": "0.9.0", - "serverSeq": 3, - "snapshots": [ - { - "resource": "ahp-root://", - "state": { - "agents": [ - { - "provider": "example", - "displayName": "Example", - "description": "Example agent", - "models": [], - "capabilities": { - "multipleChats": { - "fork": true, - "initialTitle": true - } - } - } - ] - }, - "fromSeq": 3 - } - ] - } - ] -} diff --git a/types/test-cases/round-trips/058-initial-message-rejected-error.json b/types/test-cases/round-trips/058-initial-message-rejected-error.json deleted file mode 100644 index 5c03f4d9d..000000000 --- a/types/test-cases/round-trips/058-initial-message-rejected-error.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "initial-message-rejected-error", - "group": "A", - "description": "An InitialMessageRejected JSON-RPC error preserves the created fork chat URI and the complete underlying authentication cause.", - "type": "JsonRpcMessage", - "input": { - "jsonrpc": "2.0", - "id": 12, - "error": { - "code": -32012, - "message": "The fork was created, but the initial message was rejected.", - "data": { - "chat": "ahp-chat:/fork", - "cause": { - "code": -32007, - "message": "Authentication required for the selected model.", - "data": { - "resources": [ - { - "resource": "https://example.test/models", - "authorization_servers": [ - "https://auth.example.test" - ] - } - ] - } - } - } - } - }, - "acceptableOutputs": [ - { - "jsonrpc": "2.0", - "id": 12, - "error": { - "code": -32012, - "message": "The fork was created, but the initial message was rejected.", - "data": { - "chat": "ahp-chat:/fork", - "cause": { - "code": -32007, - "message": "Authentication required for the selected model.", - "data": { - "resources": [ - { - "resource": "https://example.test/models", - "authorization_servers": [ - "https://auth.example.test" - ] - } - ] - } - } - } - } - } - ] -} diff --git a/types/test-cases/round-trips/059-fork-initial-message-capability.json b/types/test-cases/round-trips/059-fork-initial-message-capability.json deleted file mode 100644 index e3e1f68a3..000000000 --- a/types/test-cases/round-trips/059-fork-initial-message-capability.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "agent-fork-initial-message-capability", - "group": "A", - "description": "AgentInfo advertises support for the composed fork-and-initial-message operation through capabilities.multipleChats.forkWithInitialMessage.", - "type": "InitializeResult", - "input": { - "protocolVersion": "0.9.0", - "serverSeq": 3, - "snapshots": [ - { - "resource": "ahp-root://", - "state": { - "agents": [ - { - "provider": "example", - "displayName": "Example", - "description": "Example agent", - "models": [], - "capabilities": { - "multipleChats": { - "fork": true, - "initialTitle": true, - "forkWithInitialMessage": true - } - } - } - ] - }, - "fromSeq": 3 - } - ] - }, - "acceptableOutputs": [ - { - "protocolVersion": "0.9.0", - "serverSeq": 3, - "snapshots": [ - { - "resource": "ahp-root://", - "state": { - "agents": [ - { - "provider": "example", - "displayName": "Example", - "description": "Example agent", - "models": [], - "capabilities": { - "multipleChats": { - "fork": true, - "initialTitle": true, - "forkWithInitialMessage": true - } - } - } - ] - }, - "fromSeq": 3 - } - ] - } - ] -} From 88a0f183e00aefae1a785af727cc0db3fc8dcd92 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 14 Oct 2026 23:06:58 +1100 Subject: [PATCH 4/4] docs: simplify initial chat title review changes Remove redundant creation prose, the generated Kotlin source-order test, and per-client changelog fragments. Keep the shared title change note, native constructor compatibility coverage, and protocol definitions unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...otlin-trailing-constructor-properties.json | 5 ----- ...261006-go-initial-title-struct-fields.json | 5 ----- ...1006-rust-initial-title-struct-fields.json | 5 ----- docs/specification/chat-channel.md | 4 ---- scripts/kotlin-constructor-order.test.ts | 22 ------------------- 5 files changed, 41 deletions(-) delete mode 100644 docs/.changes/20261005-kotlin-trailing-constructor-properties.json delete mode 100644 docs/.changes/20261006-go-initial-title-struct-fields.json delete mode 100644 docs/.changes/20261006-rust-initial-title-struct-fields.json delete mode 100644 scripts/kotlin-constructor-order.test.ts diff --git a/docs/.changes/20261005-kotlin-trailing-constructor-properties.json b/docs/.changes/20261005-kotlin-trailing-constructor-properties.json deleted file mode 100644 index b76a513f9..000000000 --- a/docs/.changes/20261005-kotlin-trailing-constructor-properties.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "changed", - "message": "Generated `CreateChatParams` gains a trailing optional `title` constructor property; Kotlin binaries must be recompiled against the new client version.", - "targets": ["kotlin"] -} diff --git a/docs/.changes/20261006-go-initial-title-struct-fields.json b/docs/.changes/20261006-go-initial-title-struct-fields.json deleted file mode 100644 index e587ea48e..000000000 --- a/docs/.changes/20261006-go-initial-title-struct-fields.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "changed", - "message": "The generated `CreateChatParams.Title` field requires unkeyed Go composite literals to add a trailing `nil` value or migrate to keyed literals.", - "targets": ["go"] -} diff --git a/docs/.changes/20261006-rust-initial-title-struct-fields.json b/docs/.changes/20261006-rust-initial-title-struct-fields.json deleted file mode 100644 index 4364c02c3..000000000 --- a/docs/.changes/20261006-rust-initial-title-struct-fields.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "changed", - "message": "The generated `CreateChatParams.title` field requires existing Rust struct literals to add `None` or use defaults/update syntax.", - "targets": ["rust"] -} diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index 2589fd1c0..5db4e294c 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -106,10 +106,6 @@ fully replaces the catalogue or clears it when `changesets` is absent. The server allocates the chat URI and adds the chat to the session's catalog (`session/chatAdded` on the session channel) before returning. -The optional `title` field is additive. Older hosts that do not recognize it ignore it as an unknown field. - -Errors while processing `initialMessage` are reported through the chat's normal turn state, as with any other turn. - Clients MUST gate source-based creation using the selected [`AgentInfo.capabilities.multipleChats`](/reference/root#multiplechatscapability): diff --git a/scripts/kotlin-constructor-order.test.ts b/scripts/kotlin-constructor-order.test.ts deleted file mode 100644 index 3593df232..000000000 --- a/scripts/kotlin-constructor-order.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { strict as assert } from 'node:assert'; -import { readFileSync } from 'node:fs'; -import { describe, it } from 'node:test'; - -function getConstructorPropertyOrder(file: URL, className: string): string[] { - const source = readFileSync(file, 'utf8'); - const declaration = source.match(new RegExp(`data class ${className}\\(([\\s\\S]*?)\\n\\)`)); - assert.ok(declaration, `missing generated Kotlin data class ${className}`); - return [...declaration[1].matchAll(/val (\w+):/g)].map(match => match[1]); -} - -describe('generated Kotlin constructor order', () => { - it('appends CreateChatParams.title after the existing constructor properties', () => { - assert.deepEqual( - getConstructorPropertyOrder( - new URL('../clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt', import.meta.url), - 'CreateChatParams', - ), - ['channel', 'meta', 'chat', 'initialMessage', 'source', 'workingDirectories', 'title'], - ); - }); -});