From c68eb2b293c624c699bb2dc5cded10262310ddc0 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 29 Oct 2026 12:46:34 -0700 Subject: [PATCH] Allow non-user input through existing chat actions Remove the User-only dispatch restriction, regenerate schema and client documentation, and add shared conformance fixtures for non-user turn and pending-message input. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../general-instructions.instructions.md | 1 + .../Generated/Actions.generated.cs | 8 +- clients/go/ahptypes/actions.generated.go | 4 - clients/rust/crates/ahp-types/src/actions.rs | 4 - .../20261029-non-user-chat-input.json | 5 + schema/actions.schema.json | 4 +- schema/commands.schema.json | 4 +- schema/errors.schema.json | 4 +- types/channels-chat/actions.ts | 4 - .../324-non-user-turn-origins-preserved.json | 96 +++++++++++++++++++ ...25-non-user-pending-origins-preserved.json | 96 +++++++++++++++++++ 11 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 docs/.changes/20261029-non-user-chat-input.json create mode 100644 types/test-cases/reducers/324-non-user-turn-origins-preserved.json create mode 100644 types/test-cases/reducers/325-non-user-pending-origins-preserved.json diff --git a/.github/instructions/general-instructions.instructions.md b/.github/instructions/general-instructions.instructions.md index acb55cac5..d6cdf9c2a 100644 --- a/.github/instructions/general-instructions.instructions.md +++ b/.github/instructions/general-instructions.instructions.md @@ -67,6 +67,7 @@ A spec change ripples into every `clients//` mirror, so you often need to | Toolchain | Image | Notes | | --- | --- | --- | | JDK 17 (Kotlin client) | `docker.io/library/eclipse-temurin:17-jdk` | Gradle comes from the repo's `./gradlew` wrapper. | +| .NET SDK 10 (.NET client) | `mcr.microsoft.com/dotnet/sdk:10.0` | Supports `.slnx`; the test host rolls forward from .NET 8. | Mount the **repo root** (the shared conformance fixtures under `types/test-cases/` are resolved by walking up from the working directory, so a client-dir-only mount fails) and keep build caches out of `$HOME`. Example — run the Kotlin client's tests: diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index cc2719067..010387007 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -1212,9 +1212,7 @@ public sealed record SessionDefaultChatChangedAction public string? DefaultChat { get; init; } } -/// A new message has been sent to the agent, and a new turn starts. -/// -/// A client is only allowed to send {@link MessageKind.User} messages. +/// A new message has been sent to the agent, and a new turn starts. public sealed record ChatTurnStartedAction { public ActionType Type { get; init; } = ActionType.ChatTurnStarted; @@ -1988,9 +1986,7 @@ public sealed record ChatTurnsLoadedAction /// message and kind in place. /// /// If the chat is idle when a queued message is set, the server SHOULD -/// immediately consume it and start a new turn. -/// -/// A client is only allowed to send {@link MessageKind.User} messages. +/// immediately consume it and start a new turn. public sealed record ChatPendingMessageSetAction { public ActionType Type { get; init; } = ActionType.ChatPendingMessageSet; diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 49182b749..3bbefc898 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -251,8 +251,6 @@ type SessionDefaultChatChangedAction struct { } // A new message has been sent to the agent, and a new turn starts. -// -// A client is only allowed to send {@link MessageKind.User} messages. type ChatTurnStartedAction struct { Type ActionType `json:"type"` // Turn identifier @@ -789,8 +787,6 @@ type ChatReasoningAction struct { // // If the chat is idle when a queued message is set, the server SHOULD // immediately consume it and start a new turn. -// -// A client is only allowed to send {@link MessageKind.User} messages. type ChatPendingMessageSetAction struct { Type ActionType `json:"type"` // Whether this is a steering or queued message diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 6b005fcf2..dcfffce2e 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -590,8 +590,6 @@ pub struct SessionDefaultChatChangedAction { } /// A new message has been sent to the agent, and a new turn starts. -/// -/// A client is only allowed to send {@link MessageKind.User} messages. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatTurnStartedAction { @@ -1461,8 +1459,6 @@ pub struct SessionInputNeededRemovedAction { /// /// If the chat is idle when a queued message is set, the server SHOULD /// immediately consume it and start a new turn. -/// -/// A client is only allowed to send {@link MessageKind.User} messages. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatPendingMessageSetAction { diff --git a/docs/.changes/20261029-non-user-chat-input.json b/docs/.changes/20261029-non-user-chat-input.json new file mode 100644 index 000000000..be40cc91d --- /dev/null +++ b/docs/.changes/20261029-non-user-chat-input.json @@ -0,0 +1,5 @@ +{ + "type": "changed", + "message": "Remove the User-only restriction on client-submitted messages in `chat/turnStarted` and `chat/pendingMessageSet`.", + "issues": [542] +} diff --git a/schema/actions.schema.json b/schema/actions.schema.json index fae03cb55..30909b494 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -747,7 +747,7 @@ }, "ChatTurnStartedAction": { "type": "object", - "description": "A new message has been sent to the agent, and a new turn starts.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "A new message has been sent to the agent, and a new turn starts.", "properties": { "type": { "const": "chat/turnStarted" @@ -1595,7 +1595,7 @@ }, "ChatPendingMessageSetAction": { "type": "object", - "description": "A pending message was set (upsert semantics: creates or replaces).\n\nBoth kinds are stored in {@link ChatState.queuedMessages}, including this\naction's `kind`. A new `id` appends an entry; an existing `id` replaces its\nmessage and kind in place.\n\nIf the chat is idle when a queued message is set, the server SHOULD\nimmediately consume it and start a new turn.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "A pending message was set (upsert semantics: creates or replaces).\n\nBoth kinds are stored in {@link ChatState.queuedMessages}, including this\naction's `kind`. A new `id` appends an entry; an existing `id` replaces its\nmessage and kind in place.\n\nIf the chat is idle when a queued message is set, the server SHOULD\nimmediately consume it and start a new turn.", "properties": { "type": { "const": "chat/pendingMessageSet" diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 1452ca64a..1f639a2b2 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -8783,7 +8783,7 @@ }, "ChatTurnStartedAction": { "type": "object", - "description": "A new message has been sent to the agent, and a new turn starts.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "A new message has been sent to the agent, and a new turn starts.", "properties": { "type": { "const": "chat/turnStarted" @@ -9631,7 +9631,7 @@ }, "ChatPendingMessageSetAction": { "type": "object", - "description": "A pending message was set (upsert semantics: creates or replaces).\n\nBoth kinds are stored in {@link ChatState.queuedMessages}, including this\naction's `kind`. A new `id` appends an entry; an existing `id` replaces its\nmessage and kind in place.\n\nIf the chat is idle when a queued message is set, the server SHOULD\nimmediately consume it and start a new turn.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "A pending message was set (upsert semantics: creates or replaces).\n\nBoth kinds are stored in {@link ChatState.queuedMessages}, including this\naction's `kind`. A new `id` appends an entry; an existing `id` replaces its\nmessage and kind in place.\n\nIf the chat is idle when a queued message is set, the server SHOULD\nimmediately consume it and start a new turn.", "properties": { "type": { "const": "chat/pendingMessageSet" diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 4130e2926..59f4080e3 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -9953,7 +9953,7 @@ }, "ChatTurnStartedAction": { "type": "object", - "description": "A new message has been sent to the agent, and a new turn starts.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "A new message has been sent to the agent, and a new turn starts.", "properties": { "type": { "const": "chat/turnStarted" @@ -10690,7 +10690,7 @@ }, "ChatPendingMessageSetAction": { "type": "object", - "description": "A pending message was set (upsert semantics: creates or replaces).\n\nBoth kinds are stored in {@link ChatState.queuedMessages}, including this\naction's `kind`. A new `id` appends an entry; an existing `id` replaces its\nmessage and kind in place.\n\nIf the chat is idle when a queued message is set, the server SHOULD\nimmediately consume it and start a new turn.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "A pending message was set (upsert semantics: creates or replaces).\n\nBoth kinds are stored in {@link ChatState.queuedMessages}, including this\naction's `kind`. A new `id` appends an entry; an existing `id` replaces its\nmessage and kind in place.\n\nIf the chat is idle when a queued message is set, the server SHOULD\nimmediately consume it and start a new turn.", "properties": { "type": { "const": "chat/pendingMessageSet" diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index ca1fb4155..f018e1c12 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -62,8 +62,6 @@ interface ToolCallActionBase { /** * A new message has been sent to the agent, and a new turn starts. * - * A client is only allowed to send {@link MessageKind.User} messages. - * * @category Chat Actions * @version 1 * @clientDispatchable @@ -777,8 +775,6 @@ export interface ChatTurnsLoadedAction { * If the chat is idle when a queued message is set, the server SHOULD * immediately consume it and start a new turn. * - * A client is only allowed to send {@link MessageKind.User} messages. - * * @category Chat Actions * @version 1 * @clientDispatchable diff --git a/types/test-cases/reducers/324-non-user-turn-origins-preserved.json b/types/test-cases/reducers/324-non-user-turn-origins-preserved.json new file mode 100644 index 000000000..64e5a2f9c --- /dev/null +++ b/types/test-cases/reducers/324-non-user-turn-origins-preserved.json @@ -0,0 +1,96 @@ +{ + "description": "non-user turn input preserves authorship and payloads in active turns and completed history without fabricated responses", + "reducer": "chat", + "initial": { + "resource": "ahp-chat:/non-user", + "title": "Non-user input", + "status": 1, + "modifiedAt": "2026-10-29T00:00:00.000Z", + "turns": [] + }, + "actions": [ + { + "type": "chat/turnStarted", + "turnId": "agent-turn", + "startedAt": "2026-10-29T00:00:00.000Z", + "message": { + "text": "The worker finished its analysis.", + "origin": { "kind": "agent" }, + "attachments": [ + { "type": "simple", "label": "Worker result", "modelRepresentation": "The analysis passed.", "_meta": { "resultId": "result-1" } } + ], + "model": { "id": "model-1" }, + "agent": { "uri": "ahp-agent:/reviewer" }, + "_meta": { "sourceChat": "ahp-chat:/worker" } + } + }, + { "type": "chat/turnComplete", "turnId": "agent-turn", "duration": 1000 }, + { + "type": "chat/turnStarted", + "turnId": "notification-turn", + "startedAt": "2026-10-29T00:00:01.000Z", + "message": { + "text": "The worker conversation is now idle.", + "origin": { "kind": "systemNotification" } + } + }, + { "type": "chat/turnComplete", "turnId": "notification-turn", "duration": 0 }, + { + "type": "chat/turnStarted", + "turnId": "tool-turn", + "startedAt": "2026-10-29T00:00:02.000Z", + "message": { + "text": "Background work completed.", + "origin": { "kind": "tool" } + } + } + ], + "expected": { + "resource": "ahp-chat:/non-user", + "title": "Non-user input", + "status": 8, + "modifiedAt": "2026-10-29T00:00:02.000Z", + "turns": [ + { + "id": "agent-turn", + "startedAt": "2026-10-29T00:00:00.000Z", + "duration": 1000, + "message": { + "text": "The worker finished its analysis.", + "origin": { "kind": "agent" }, + "attachments": [ + { "type": "simple", "label": "Worker result", "modelRepresentation": "The analysis passed.", "_meta": { "resultId": "result-1" } } + ], + "model": { "id": "model-1" }, + "agent": { "uri": "ahp-agent:/reviewer" }, + "_meta": { "sourceChat": "ahp-chat:/worker" } + }, + "responseParts": [], + "usage": null, + "state": "complete" + }, + { + "id": "notification-turn", + "startedAt": "2026-10-29T00:00:01.000Z", + "duration": 0, + "message": { + "text": "The worker conversation is now idle.", + "origin": { "kind": "systemNotification" } + }, + "responseParts": [], + "usage": null, + "state": "complete" + } + ], + "activeTurn": { + "id": "tool-turn", + "startedAt": "2026-10-29T00:00:02.000Z", + "message": { + "text": "Background work completed.", + "origin": { "kind": "tool" } + }, + "responseParts": [], + "usage": null + } + } +} diff --git a/types/test-cases/reducers/325-non-user-pending-origins-preserved.json b/types/test-cases/reducers/325-non-user-pending-origins-preserved.json new file mode 100644 index 000000000..8a73d5aad --- /dev/null +++ b/types/test-cases/reducers/325-non-user-pending-origins-preserved.json @@ -0,0 +1,96 @@ +{ + "description": "non-user queued and steering input preserves authorship through upserts and queued turn consumption", + "reducer": "chat", + "initial": { + "resource": "ahp-chat:/non-user", + "title": "Non-user input", + "status": 1, + "modifiedAt": "2026-10-29T00:00:00.000Z", + "turns": [] + }, + "actions": [ + { + "type": "chat/pendingMessageSet", + "kind": "queued", + "id": "agent-result", + "message": { + "text": "The worker finished its analysis.", + "origin": { "kind": "agent" }, + "model": { "id": "model-1" }, + "agent": { "uri": "ahp-agent:/reviewer" } + } + }, + { + "type": "chat/pendingMessageSet", + "kind": "queued", + "id": "tool-result", + "message": { "text": "Background work completed.", "origin": { "kind": "tool" } } + }, + { + "type": "chat/pendingMessageSet", + "kind": "steering", + "id": "notification", + "message": { "text": "Worker finishing.", "origin": { "kind": "systemNotification" } } + }, + { + "type": "chat/pendingMessageSet", + "kind": "steering", + "id": "notification", + "message": { "text": "Worker finished.", "origin": { "kind": "systemNotification" }, "_meta": { "workerId": "worker-1" } } + }, + { + "type": "chat/pendingMessageSet", + "kind": "steering", + "id": "notification", + "message": { "text": "Worker finished.", "origin": { "kind": "systemNotification" }, "_meta": { "workerId": "worker-1" } } + }, + { + "type": "chat/turnStarted", + "turnId": "agent-turn", + "startedAt": "2026-10-29T00:00:01.000Z", + "queuedMessageId": "agent-result", + "message": { + "text": "The worker finished its analysis.", + "origin": { "kind": "agent" }, + "model": { "id": "model-1" }, + "agent": { "uri": "ahp-agent:/reviewer" } + } + } + ], + "expected": { + "resource": "ahp-chat:/non-user", + "title": "Non-user input", + "status": 8, + "modifiedAt": "2026-10-29T00:00:01.000Z", + "turns": [], + "activeTurn": { + "id": "agent-turn", + "startedAt": "2026-10-29T00:00:01.000Z", + "message": { + "text": "The worker finished its analysis.", + "origin": { "kind": "agent" }, + "model": { "id": "model-1" }, + "agent": { "uri": "ahp-agent:/reviewer" } + }, + "responseParts": [], + "usage": null + }, + "queuedMessages": [ + { + "id": "tool-result", + "kind": "queued", + "message": { "text": "Background work completed.", "origin": { "kind": "tool" } } + }, + { + "id": "notification", + "kind": "steering", + "message": { "text": "Worker finished.", "origin": { "kind": "systemNotification" }, "_meta": { "workerId": "worker-1" } } + } + ], + "steeringMessage": { + "id": "notification", + "kind": "steering", + "message": { "text": "Worker finished.", "origin": { "kind": "systemNotification" }, "_meta": { "workerId": "worker-1" } } + } + } +}