diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index fbea93a86..01840ca71 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -962,6 +962,10 @@ 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. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Title { get; init; } } /// Disposes a chat and cleans up server-side resources. diff --git a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs index f5f04dc1e..998dee871 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)); case "SubscribeParams": diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 930dec771..aab1cd030 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -643,6 +643,8 @@ 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. + 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 b75cc82e9..4bd69e346 100644 --- a/clients/go/ahptypes/roundtrip_fixture_test.go +++ b/clients/go/ahptypes/roundtrip_fixture_test.go @@ -258,6 +258,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/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 0bdf151c6..b337c6fa4 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 @@ -900,7 +900,11 @@ 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. + */ + val title: String? = 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..f1f48ef0b --- /dev/null +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ConstructorCompatibilityTest.kt @@ -0,0 +1,29 @@ +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 org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ConstructorCompatibilityTest { + @Test + fun `old full positional CreateChatParams constructor retains its source semantics`() { + val message = Message("Hello", MessageOrigin(MessageKind.USER)) + val createChat = + CreateChatParams( + "ahp-session:/session", + null, + "ahp-chat:/chat", + message, + null, + listOf("file:///workspace"), + ) + + assertEquals(message, createChat.initialMessage) + assertEquals(listOf("file:///workspace"), createChat.workingDirectories) + assertNull(createChat.title) + } +} 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 a45b6881b..984cdfc11 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 @@ -263,6 +264,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()) "SubscribeParams" -> rt(SubscribeParams.serializer()) diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index e306d1851..7834cf264 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -896,6 +896,9 @@ 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. + #[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/tests/roundtrip_corpus.rs b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs index c5d27fe85..e9e986e63 100644 --- a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs +++ b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs @@ -29,8 +29,9 @@ use ahp_types::{ actions::{ActionEnvelope, StateAction}, commands::{ - ChangesetOperationTarget, ChatSource, CreateTcpConnectionParams, Implementation, - InitializeResult, ReconnectParams, ReconnectResult, SubscribeParams, SubscribeResult, + ChangesetOperationTarget, ChatSource, CreateChatParams, CreateTcpConnectionParams, + Implementation, InitializeResult, ReconnectParams, ReconnectResult, SubscribeParams, + SubscribeResult, }, common::StringOrMarkdown, messages::JsonRpcMessage, @@ -229,6 +230,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), "SubscribeParams" => round_trip!(SubscribeParams), diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 2db6f2c42..2f41e820e 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -973,6 +973,8 @@ 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. + public var title: String? enum CodingKeys: String, CodingKey { case channel @@ -981,6 +983,7 @@ public struct CreateChatParams: Codable, Sendable { case initialMessage case source case workingDirectories + case title } public init( @@ -989,7 +992,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 @@ -997,6 +1001,7 @@ public struct CreateChatParams: Codable, Sendable { self.initialMessage = initialMessage self.source = source self.workingDirectories = workingDirectories + self.title = title } } diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift index 49a6bc5c0..0cc73a517 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/clients/typescript/test/types-round-trip.test.ts b/clients/typescript/test/types-round-trip.test.ts index 6770c73a4..fff4a2d04 100644 --- a/clients/typescript/test/types-round-trip.test.ts +++ b/clients/typescript/test/types-round-trip.test.ts @@ -65,7 +65,7 @@ import type { import type { ChannelFrameParams, ChannelCreditParams } from '../src/types/common/notifications.js'; import type { TcpDataParams, TcpEofParams, TcpResetParams } from '../src/types/channels-tcp/notifications.js'; import type { CreateTcpConnectionParams } from '../src/types/channels-tcp/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 ─────────────────────────────────────────────────────── @@ -248,6 +248,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; case 'SubscribeParams': void (parsed as SubscribeParams); break; diff --git a/docs/.changes/20261005-initial-chat-title.json b/docs/.changes/20261005-initial-chat-title.json new file mode 100644 index 000000000..74f8bf7dc --- /dev/null +++ b/docs/.changes/20261005-initial-chat-title.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Optional `CreateChatParams.title` for a client-selected initial display title." +} diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md index cfd7afe2f..c52652f5a 100644 --- a/docs/specification/chat-channel.md +++ b/docs/specification/chat-channel.md @@ -113,6 +113,7 @@ omits the property. Every other property is changed on the session with [`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 }`, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 363fd6846..a30f2b814 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1422,6 +1422,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." } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 77db7848d..4d466da55 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -7508,6 +7508,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." } }, "required": [ diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts index 744b773ec..381cd2ae7 100644 --- a/types/channels-chat/commands.ts +++ b/types/channels-chat/commands.ts @@ -118,6 +118,8 @@ export interface CreateChatParams extends BaseParams { * {@link AgentCapabilities.multipleWorkingDirectories}. */ workingDirectories?: URI[]; + /** Client-selected initial display title for the new chat. */ + title?: string; } // ─── moveChat ──────────────────────────────────────────────────────────────── 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/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" + } + } + } + ] +}