From ea386ee907d3c66424412b6c981fe1b08be252a4 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 25 Jun 2026 12:48:31 -0700 Subject: [PATCH 1/2] sessions: add session-level inputNeeded aggregate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface every outstanding input request across a session's chats on the session channel so a client (e.g. a mobile app or tool-providing client) can discover and answer them without subscribing to individual chats. What changed and why: - Add `SessionState.inputNeeded` — a host-managed aggregate of outstanding input across all chats, keyed by an opaque `id`. Each entry carries the owning `chat` URI plus the identifiers needed to respond, so clients answer by dispatching the ordinary `chat/*` action to that chat's channel (no chat subscription required). State-only: no new response actions. - Add the `SessionInputRequest` union with three variants: `SessionChatInputRequest` (mirrors a `ChatInputRequest`), `SessionToolConfirmationRequest` (a `ToolCallConfirmationState`), and `SessionToolClientExecutionRequest` (a running tool the session delegates to an active client). - Add `ToolCallConfirmationState` union (`ToolCallPendingConfirmationState | ToolCallPendingResultConfirmationState`); an inline anonymous union isn't generatable across clients. - Add `session/inputNeededSet` / `session/inputNeededRemoved` server actions (upsert/remove) plus the canonical reducer and 6 fixtures (100% branch coverage). - Wire the new types/unions/actions through all four code generators and hand-port the reducer logic into the Rust, Go, Kotlin, and Swift clients. - Document the aggregate in the session-channel spec and add Unreleased entries to the root and all five client CHANGELOGs. Note: `SessionToolClientExecutionRequest.toolCall` is typed `ToolCallState` (documented as `running`) rather than `ToolCallRunningState`, because a direct single-variant struct reference can't round-trip the `status` discriminant on the wire in the Rust/Go generators. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 12 + clients/go/CHANGELOG.md | 7 + clients/go/ahp/reducers.go | 33 +++ clients/go/ahptypes/actions.generated.go | 48 ++++ clients/go/ahptypes/state.generated.go | 241 ++++++++++++++++++ clients/kotlin/CHANGELOG.md | 6 + .../microsoft/agenthostprotocol/Reducers.kt | 36 +++ .../generated/Actions.generated.kt | 28 ++ .../generated/State.generated.kt | 229 +++++++++++++++++ clients/rust/CHANGELOG.md | 6 + clients/rust/crates/ahp-types/src/actions.rs | 48 +++- clients/rust/crates/ahp-types/src/state.rs | 150 +++++++++++ clients/rust/crates/ahp/src/reducers.rs | 39 +++ .../ahp/tests/multi_host_state_mirror.rs | 1 + .../Generated/Actions.generated.swift | 38 +++ .../Generated/State.generated.swift | 201 +++++++++++++++ .../Sources/AgentHostProtocol/Reducers.swift | 30 +++ clients/swift/CHANGELOG.md | 6 + clients/typescript/CHANGELOG.md | 7 + docs/specification/session-channel.md | 18 +- schema/actions.schema.json | 188 ++++++++++++++ schema/commands.schema.json | 155 +++++++++++ schema/errors.schema.json | 121 +++++++++ schema/notifications.schema.json | 121 +++++++++ schema/state.schema.json | 148 +++++++++++ scripts/generate-go.ts | 36 ++- scripts/generate-kotlin.ts | 32 ++- scripts/generate-rust.ts | 38 ++- scripts/generate-swift.ts | 34 ++- types/action-origin.generated.ts | 8 + types/channels-chat/state.ts | 14 + types/channels-session/actions.ts | 45 ++++ types/channels-session/reducer.ts | 27 ++ types/channels-session/state.ts | 154 ++++++++++- types/common/actions.ts | 6 + types/index.ts | 8 + ...ession-inputneededset-adds-chat-input.json | 77 ++++++ ...utneededset-appends-tool-confirmation.json | 79 ++++++ ...sion-inputneededset-replaces-existing.json | 84 ++++++ ...sion-inputneededremoved-removes-entry.json | 58 +++++ ...n-inputneededremoved-no-op-unknown-id.json | 52 ++++ ...ssion-inputneededremoved-no-op-absent.json | 36 +++ types/version/registry.ts | 2 + 43 files changed, 2697 insertions(+), 10 deletions(-) create mode 100644 types/test-cases/reducers/223-session-inputneededset-adds-chat-input.json create mode 100644 types/test-cases/reducers/224-session-inputneededset-appends-tool-confirmation.json create mode 100644 types/test-cases/reducers/225-session-inputneededset-replaces-existing.json create mode 100644 types/test-cases/reducers/226-session-inputneededremoved-removes-entry.json create mode 100644 types/test-cases/reducers/227-session-inputneededremoved-no-op-unknown-id.json create mode 100644 types/test-cases/reducers/228-session-inputneededremoved-no-op-absent.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f55ce7f..3cf74343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,18 @@ changes accumulate. Track in-flight protocol changes via PRs touching - `JsonPrimitive` type alias (`string | number | boolean | null`) in `types/common/state.ts`. - `session/activeClientRemoved` action to release a single active client from a session by `clientId`. +- `SessionState.inputNeeded` — a session-level aggregate of outstanding input + requests across all chats, so a client can discover and answer elicitations, + tool confirmations, and client-tool execution requests from the session + channel without subscribing to individual chats. Each entry + (`SessionChatInputRequest`, `SessionToolConfirmationRequest`, + `SessionToolClientExecutionRequest`, unioned as `SessionInputRequest`) carries + the owning chat URI plus the identifiers needed to respond. +- `session/inputNeededSet` and `session/inputNeededRemoved` actions for the host + to upsert and remove `SessionState.inputNeeded` entries. +- `ToolCallConfirmationState` union (`ToolCallPendingConfirmationState | + ToolCallPendingResultConfirmationState`) for the tool call carried by + `SessionToolConfirmationRequest`. ### Changed diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index e8ff02d6..9dc0948d 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -22,6 +22,13 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. lightweight session-list presentation hints. - `SessionActiveClientRemovedAction` (wire `session/activeClientRemoved`) to release a single active client by `ClientId`. +- `SessionState.InputNeeded` — a session-level aggregate of outstanding input + requests across all chats (`SessionInputRequest` union with + `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and + `SessionToolClientExecutionRequest`), plus the `SessionInputNeededSetAction` + (wire `session/inputNeededSet`) and `SessionInputNeededRemovedAction` (wire + `session/inputNeededRemoved`) actions and the `ToolCallConfirmationState` + union. ### Changed diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 1348b7a3..2c97e283 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -263,6 +263,18 @@ func customizationID(c ahptypes.Customization) (string, bool) { return "", false } +func sessionInputRequestID(r ahptypes.SessionInputRequest) (string, bool) { + switch v := r.Value.(type) { + case *ahptypes.SessionChatInputRequest: + return v.Id, true + case *ahptypes.SessionToolConfirmationRequest: + return v.Id, true + case *ahptypes.SessionToolClientExecutionRequest: + return v.Id, true + } + return "", false +} + func childCustomizationID(c ahptypes.ChildCustomization) (string, bool) { switch v := c.Value.(type) { case *ahptypes.AgentCustomization: @@ -743,6 +755,27 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct } } return ReduceOutcomeNoOp + case *ahptypes.SessionInputNeededSetAction: + id, ok := sessionInputRequestID(a.Request) + if !ok { + return ReduceOutcomeNoOp + } + for i := range state.InputNeeded { + if got, ok := sessionInputRequestID(state.InputNeeded[i]); ok && got == id { + state.InputNeeded[i] = a.Request + return ReduceOutcomeApplied + } + } + state.InputNeeded = append(state.InputNeeded, a.Request) + return ReduceOutcomeApplied + case *ahptypes.SessionInputNeededRemovedAction: + for i := range state.InputNeeded { + if got, ok := sessionInputRequestID(state.InputNeeded[i]); ok && got == a.Id { + state.InputNeeded = append(state.InputNeeded[:i], state.InputNeeded[i+1:]...) + return ReduceOutcomeApplied + } + } + return ReduceOutcomeNoOp case *ahptypes.SessionCustomizationsChangedAction: state.Customizations = append([]ahptypes.Customization(nil), a.Customizations...) return ReduceOutcomeApplied diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index e98e7e90..b49e0f3b 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -48,6 +48,8 @@ const ( ActionTypeSessionServerToolsChanged ActionType = "session/serverToolsChanged" ActionTypeSessionActiveClientSet ActionType = "session/activeClientSet" ActionTypeSessionActiveClientRemoved ActionType = "session/activeClientRemoved" + ActionTypeSessionInputNeededSet ActionType = "session/inputNeededSet" + ActionTypeSessionInputNeededRemoved ActionType = "session/inputNeededRemoved" ActionTypeChatPendingMessageSet ActionType = "chat/pendingMessageSet" ActionTypeChatPendingMessageRemoved ActionType = "chat/pendingMessageRemoved" ActionTypeChatQueuedMessagesReordered ActionType = "chat/queuedMessagesReordered" @@ -754,6 +756,38 @@ type SessionActiveClientRemovedAction struct { ClientId string `json:"clientId"` } +// A session-level input request was added or updated. +// +// Upsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the +// host dispatches this with the full {@link SessionInputRequest} to append a new +// entry to {@link SessionState.inputNeeded} or replace the existing entry with +// the same `id`. +// +// Server-originated: the host mirrors chat-level requests (elicitations, tool +// confirmations, client-tool executions) into the session aggregate so clients +// subscribed only to the session channel can discover them. Clients respond by +// dispatching the ordinary `chat/*` action to the entry's `chat` channel — see +// {@link SessionInputRequest}. +type SessionInputNeededSetAction struct { + Type ActionType `json:"type"` + // The input request to add or update, matched by `id`. + Request SessionInputRequest `json:"request"` +} + +// A session-level input request was removed. +// +// Removes the entry identified by `id` from +// {@link SessionState.inputNeeded}; a no-op when no entry matches. +// +// Server-originated: the host dispatches this once the underlying request +// resolves (the user answers, the tool call is confirmed, or the client +// reports its result). +type SessionInputNeededRemovedAction struct { + Type ActionType `json:"type"` + // The `id` of the input request to remove. + Id string `json:"id"` +} + // The session's customizations have changed. // // Full-replacement semantics: the `customizations` array replaces the @@ -1241,6 +1275,8 @@ func (*SessionChangesetsChangedAction) isStateAction() {} func (*SessionServerToolsChangedAction) isStateAction() {} func (*SessionActiveClientSetAction) isStateAction() {} func (*SessionActiveClientRemovedAction) isStateAction() {} +func (*SessionInputNeededSetAction) isStateAction() {} +func (*SessionInputNeededRemovedAction) isStateAction() {} func (*SessionCustomizationsChangedAction) isStateAction() {} func (*SessionCustomizationToggledAction) isStateAction() {} func (*SessionCustomizationUpdatedAction) isStateAction() {} @@ -1534,6 +1570,18 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "session/inputNeededSet": + var value SessionInputNeededSetAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "session/inputNeededRemoved": + var value SessionInputNeededRemovedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value case "session/customizationsChanged": var value SessionCustomizationsChangedAction if err := json.Unmarshal(data, &value); err != nil { diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 8a5f885a..ddccc092 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -145,6 +145,22 @@ const ( ChatInputResponseKindCancel ChatInputResponseKind = "cancel" ) +// Discriminant for the kinds of outstanding input a session can surface in +// {@link SessionState.inputNeeded}. +// +// This is a general/typological union (not a lifecycle), so the discriminant is +// a `*Kind`. +type SessionInputRequestKind string + +const ( + // A user-facing elicitation mirrored from a chat's `inputRequests`. + SessionInputRequestKindChatInput SessionInputRequestKind = "chatInput" + // A tool call awaiting parameter- or result-confirmation. + SessionInputRequestKindToolConfirmation SessionInputRequestKind = "toolConfirmation" + // A running tool the session wants an active client to execute. + SessionInputRequestKindToolClientExecution SessionInputRequestKind = "toolClientExecution" +) + // How a turn ended. type TurnState string @@ -687,6 +703,21 @@ type SessionState struct { // before subscribing. See {@link Changeset} for the full shape and // {@link /guide/changesets | Changesets} for an overview of the model. Changesets []Changeset `json:"changesets,omitempty"` + // Outstanding input the session is blocked on, aggregated across every chat + // so a client can discover and answer it from the session channel alone, + // without subscribing to individual chats. + // + // Each entry is self-sufficient: it carries the owning chat's URI plus every + // identifier the client needs to respond. A client answers by dispatching the + // ordinary `chat/*` action to that chat's channel — see + // {@link SessionInputRequest} for the per-variant response path. A present, + // non-empty list implies {@link SessionStatus.InputNeeded} on + // {@link SessionSummary.status}. + // + // Host-managed: the host upserts entries with `session/inputNeededSet` as + // chats raise requests and removes them with `session/inputNeededRemoved` + // once the underlying request resolves. + InputNeeded []SessionInputRequest `json:"inputNeeded,omitempty"` // Additional provider-specific metadata for this session. // // Clients MAY look for well-known keys here to provide enhanced UI. @@ -716,6 +747,89 @@ type SessionActiveClient struct { Customizations []ClientPluginCustomization `json:"customizations,omitempty"` } +// A user-input elicitation surfaced at the session level, mirroring one entry +// of the owning chat's {@link ChatState.inputRequests}. +// +// Respond by dispatching `chat/inputCompleted` (or syncing drafts with +// `chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`}, +// keyed by {@link ChatInputRequest.id | `request.id`}. +type SessionChatInputRequest struct { + // Stable key for this entry, unique within the session's + // {@link SessionState.inputNeeded} list. The host derives it however it likes + // (for example from the chat URI plus the underlying request or tool-call + // id); consumers MUST treat it as opaque. It is the key for the + // `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + Id string `json:"id"` + // The chat the underlying request lives in. This is the channel a client + // dispatches its response to — it does not need to have subscribed to that + // chat first. + Chat URI `json:"chat"` + Kind SessionInputRequestKind `json:"kind"` + // The mirrored chat input request. + Request ChatInputRequest `json:"request"` +} + +// A tool call blocked on confirmation — either parameter confirmation before +// execution or result confirmation after — surfaced at the session level. +// +// Respond by dispatching `chat/toolCallConfirmed` (for +// {@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed` +// (for {@link ToolCallPendingResultConfirmationState}) to +// {@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and +// `toolCall.toolCallId`. +type SessionToolConfirmationRequest struct { + // Stable key for this entry, unique within the session's + // {@link SessionState.inputNeeded} list. The host derives it however it likes + // (for example from the chat URI plus the underlying request or tool-call + // id); consumers MUST treat it as opaque. It is the key for the + // `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + Id string `json:"id"` + // The chat the underlying request lives in. This is the channel a client + // dispatches its response to — it does not need to have subscribed to that + // chat first. + Chat URI `json:"chat"` + Kind SessionInputRequestKind `json:"kind"` + // The turn the tool call belongs to. + TurnId string `json:"turnId"` + // The tool call awaiting confirmation. + ToolCall ToolCallConfirmationState `json:"toolCall"` +} + +// A running tool whose execution is delegated to an active client. Surfaced so +// a client that provides the tool can pick up the work without subscribing to +// the owning chat. +// +// The {@link toolCall} is always a {@link ToolCallRunningState} (a +// {@link ToolCallState} in `running` status) whose +// {@link ToolCallRunningState.contributor | `contributor`} is a client +// {@link ToolCallClientContributor} whose `clientId` matches the denormalized +// {@link clientId} here. Execute and report the result by dispatching +// `chat/toolCallComplete` (and optionally streaming with +// `chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat | +// `chat`}, keyed by `turnId` and `toolCall.toolCallId`. +type SessionToolClientExecutionRequest struct { + // Stable key for this entry, unique within the session's + // {@link SessionState.inputNeeded} list. The host derives it however it likes + // (for example from the chat URI plus the underlying request or tool-call + // id); consumers MUST treat it as opaque. It is the key for the + // `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + Id string `json:"id"` + // The chat the underlying request lives in. This is the channel a client + // dispatches its response to — it does not need to have subscribed to that + // chat first. + Chat URI `json:"chat"` + Kind SessionInputRequestKind `json:"kind"` + // The turn the tool call belongs to. + TurnId string `json:"turnId"` + // The `clientId` expected to execute the tool. Matches the `clientId` of the + // tool call's client {@link ToolCallContributor}. + ClientId string `json:"clientId"` + // The running tool call the session wants the owning client to execute. The + // host only ever populates this with a {@link ToolCallRunningState} (i.e. a + // {@link ToolCallState} in `running` status). + ToolCall ToolCallState `json:"toolCall"` +} + // Lightweight catalog entry summarizing one session. Surfaced via // {@link RootChannelCommands.listSessions | `root/listSessions`} and // `root/sessionAdded`/`root/sessionSummaryChanged` notifications. @@ -2941,6 +3055,66 @@ func (u ToolCallState) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } +// ToolCallConfirmationState is a tool call blocked on parameter- or result-confirmation. +type ToolCallConfirmationState struct { + Value isToolCallConfirmationState +} + +// isToolCallConfirmationState is the marker interface implemented by every +// concrete variant of ToolCallConfirmationState. +type isToolCallConfirmationState interface{ isToolCallConfirmationState() } + +func (*ToolCallPendingConfirmationState) isToolCallConfirmationState() {} +func (*ToolCallPendingResultConfirmationState) isToolCallConfirmationState() {} + +// ToolCallConfirmationStateUnknown carries an unrecognized ToolCallConfirmationState variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully. +type ToolCallConfirmationStateUnknown struct { + Raw json.RawMessage +} + +func (*ToolCallConfirmationStateUnknown) isToolCallConfirmationState() {} + +// UnmarshalJSON decodes the variant indicated by the "status" discriminator. +func (u *ToolCallConfirmationState) UnmarshalJSON(data []byte) error { + disc, _, err := readDiscriminator(data, "status") + if err != nil { + return err + } + switch disc { + case "pending-confirmation": + var value ToolCallPendingConfirmationState + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "pending-result-confirmation": + var value ToolCallPendingResultConfirmationState + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + raw := make(json.RawMessage, len(data)) + copy(raw, data) + u.Value = &ToolCallConfirmationStateUnknown{Raw: raw} + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u ToolCallConfirmationState) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*ToolCallConfirmationStateUnknown); ok { + if len(unk.Raw) == 0 { + return []byte("null"), nil + } + return unk.Raw, nil + } + if u.Value == nil { + return []byte("null"), nil + } + return json.Marshal(u.Value) +} + // TerminalClaim identifies who currently holds a terminal. type TerminalClaim struct { Value isTerminalClaim @@ -3827,6 +4001,73 @@ func (u ToolCallContributor) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } +// SessionInputRequest is one outstanding piece of input a session is blocked on, aggregated across all chats. +type SessionInputRequest struct { + Value isSessionInputRequest +} + +// isSessionInputRequest is the marker interface implemented by every +// concrete variant of SessionInputRequest. +type isSessionInputRequest interface{ isSessionInputRequest() } + +func (*SessionChatInputRequest) isSessionInputRequest() {} +func (*SessionToolConfirmationRequest) isSessionInputRequest() {} +func (*SessionToolClientExecutionRequest) isSessionInputRequest() {} + +// SessionInputRequestUnknown carries an unrecognized SessionInputRequest variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully. +type SessionInputRequestUnknown struct { + Raw json.RawMessage +} + +func (*SessionInputRequestUnknown) isSessionInputRequest() {} + +// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. +func (u *SessionInputRequest) UnmarshalJSON(data []byte) error { + disc, _, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + switch disc { + case "chatInput": + var value SessionChatInputRequest + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "toolConfirmation": + var value SessionToolConfirmationRequest + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "toolClientExecution": + var value SessionToolClientExecutionRequest + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + raw := make(json.RawMessage, len(data)) + copy(raw, data) + u.Value = &SessionInputRequestUnknown{Raw: raw} + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u SessionInputRequest) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*SessionInputRequestUnknown); ok { + if len(unk.Raw) == 0 { + return []byte("null"), nil + } + return unk.Raw, nil + } + if u.Value == nil { + return []byte("null"), nil + } + return json.Marshal(u.Value) +} + // ChatOrigin describes how a chat came into existence. type ChatOrigin struct { Value isChatOrigin diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index 9a564c32..86efe95c 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -24,6 +24,12 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump - `SessionActiveClientRemovedAction` (`StateActionSessionActiveClientRemoved`, wire `session/activeClientRemoved`) to release a single active client by `clientId`. +- `SessionState.inputNeeded` — a session-level aggregate of outstanding input + requests across all chats (`SessionInputRequest` sealed interface with + `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and + `SessionToolClientExecutionRequest`), plus the `SessionInputNeededSetAction` / + `SessionInputNeededRemovedAction` actions and the `ToolCallConfirmationState` + union. ### Changed diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index 7d78ad5d..fcea316f 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -189,6 +189,14 @@ private fun customizationId(c: Customization): String? = when (c) { is CustomizationUnknown -> null } +private fun sessionInputRequestId(r: SessionInputRequest): String? = when (r) { + is SessionInputRequestChatInput -> r.value.id + is SessionInputRequestToolConfirmation -> r.value.id + is SessionInputRequestToolClientExecution -> r.value.id + // Unknown variants carry an opaque `raw` JSON object — no id to expose. + is SessionInputRequestUnknown -> null +} + private fun customizationChildren(c: Customization): List? = when (c) { is CustomizationPlugin -> c.value.children is CustomizationDirectory -> c.value.children @@ -553,6 +561,34 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } + is StateActionSessionInputNeededSet -> { + val request = action.value.request + val id = sessionInputRequestId(request) + if (id == null) state else { + val list = state.inputNeeded ?: emptyList() + val idx = list.indexOfFirst { sessionInputRequestId(it) == id } + if (idx < 0) { + state.copy(inputNeeded = list + request) + } else { + val updated = list.toMutableList() + updated[idx] = request + state.copy(inputNeeded = updated) + } + } + } + + is StateActionSessionInputNeededRemoved -> { + val list = state.inputNeeded + if (list == null) state else { + val idx = list.indexOfFirst { sessionInputRequestId(it) == action.value.id } + if (idx < 0) state else { + val updated = list.toMutableList() + updated.removeAt(idx) + state.copy(inputNeeded = updated) + } + } + } + is StateActionSessionCustomizationsChanged -> state.copy(customizations = action.value.customizations) is StateActionSessionCustomizationToggled -> { diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index db42d1ac..0611e7f6 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt @@ -84,6 +84,10 @@ enum class ActionType { SESSION_ACTIVE_CLIENT_SET, @SerialName("session/activeClientRemoved") SESSION_ACTIVE_CLIENT_REMOVED, + @SerialName("session/inputNeededSet") + SESSION_INPUT_NEEDED_SET, + @SerialName("session/inputNeededRemoved") + SESSION_INPUT_NEEDED_REMOVED, @SerialName("chat/pendingMessageSet") CHAT_PENDING_MESSAGE_SET, @SerialName("chat/pendingMessageRemoved") @@ -793,6 +797,24 @@ data class SessionActiveClientRemovedAction( val clientId: String ) +@Serializable +data class SessionInputNeededSetAction( + val type: ActionType, + /** + * The input request to add or update, matched by `id`. + */ + val request: SessionInputRequest +) + +@Serializable +data class SessionInputNeededRemovedAction( + val type: ActionType, + /** + * The `id` of the input request to remove. + */ + val id: String +) + @Serializable data class ChatPendingMessageSetAction( val type: ActionType, @@ -1367,6 +1389,8 @@ sealed interface StateAction @JvmInline value class StateActionSessionServerToolsChanged(val value: SessionServerToolsChangedAction) : StateAction @JvmInline value class StateActionSessionActiveClientSet(val value: SessionActiveClientSetAction) : StateAction @JvmInline value class StateActionSessionActiveClientRemoved(val value: SessionActiveClientRemovedAction) : StateAction +@JvmInline value class StateActionSessionInputNeededSet(val value: SessionInputNeededSetAction) : StateAction +@JvmInline value class StateActionSessionInputNeededRemoved(val value: SessionInputNeededRemovedAction) : StateAction @JvmInline value class StateActionChatPendingMessageSet(val value: ChatPendingMessageSetAction) : StateAction @JvmInline value class StateActionChatPendingMessageRemoved(val value: ChatPendingMessageRemovedAction) : StateAction @JvmInline value class StateActionChatQueuedMessagesReordered(val value: ChatQueuedMessagesReorderedAction) : StateAction @@ -1455,6 +1479,8 @@ internal object StateActionSerializer : KSerializer { "session/serverToolsChanged" -> StateActionSessionServerToolsChanged(input.json.decodeFromJsonElement(SessionServerToolsChangedAction.serializer(), element)) "session/activeClientSet" -> StateActionSessionActiveClientSet(input.json.decodeFromJsonElement(SessionActiveClientSetAction.serializer(), element)) "session/activeClientRemoved" -> StateActionSessionActiveClientRemoved(input.json.decodeFromJsonElement(SessionActiveClientRemovedAction.serializer(), element)) + "session/inputNeededSet" -> StateActionSessionInputNeededSet(input.json.decodeFromJsonElement(SessionInputNeededSetAction.serializer(), element)) + "session/inputNeededRemoved" -> StateActionSessionInputNeededRemoved(input.json.decodeFromJsonElement(SessionInputNeededRemovedAction.serializer(), element)) "chat/pendingMessageSet" -> StateActionChatPendingMessageSet(input.json.decodeFromJsonElement(ChatPendingMessageSetAction.serializer(), element)) "chat/pendingMessageRemoved" -> StateActionChatPendingMessageRemoved(input.json.decodeFromJsonElement(ChatPendingMessageRemovedAction.serializer(), element)) "chat/queuedMessagesReordered" -> StateActionChatQueuedMessagesReordered(input.json.decodeFromJsonElement(ChatQueuedMessagesReorderedAction.serializer(), element)) @@ -1536,6 +1562,8 @@ internal object StateActionSerializer : KSerializer { is StateActionSessionServerToolsChanged -> output.json.encodeToJsonElement(SessionServerToolsChangedAction.serializer(), value.value) is StateActionSessionActiveClientSet -> output.json.encodeToJsonElement(SessionActiveClientSetAction.serializer(), value.value) is StateActionSessionActiveClientRemoved -> output.json.encodeToJsonElement(SessionActiveClientRemovedAction.serializer(), value.value) + is StateActionSessionInputNeededSet -> output.json.encodeToJsonElement(SessionInputNeededSetAction.serializer(), value.value) + is StateActionSessionInputNeededRemoved -> output.json.encodeToJsonElement(SessionInputNeededRemovedAction.serializer(), value.value) is StateActionChatPendingMessageSet -> output.json.encodeToJsonElement(ChatPendingMessageSetAction.serializer(), value.value) is StateActionChatPendingMessageRemoved -> output.json.encodeToJsonElement(ChatPendingMessageRemovedAction.serializer(), value.value) is StateActionChatQueuedMessagesReordered -> output.json.encodeToJsonElement(ChatQueuedMessagesReorderedAction.serializer(), value.value) 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 55638b10..c2e71077 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 @@ -283,6 +283,32 @@ enum class ChatInputResponseKind { CANCEL } +/** + * Discriminant for the kinds of outstanding input a session can surface in + * {@link SessionState.inputNeeded}. + * + * This is a general/typological union (not a lifecycle), so the discriminant is + * a `*Kind`. + */ +@Serializable +enum class SessionInputRequestKind { + /** + * A user-facing elicitation mirrored from a chat's `inputRequests`. + */ + @SerialName("chatInput") + CHAT_INPUT, + /** + * A tool call awaiting parameter- or result-confirmation. + */ + @SerialName("toolConfirmation") + TOOL_CONFIRMATION, + /** + * A running tool the session wants an active client to execute. + */ + @SerialName("toolClientExecution") + TOOL_CLIENT_EXECUTION +} + /** * How a turn ended. */ @@ -1234,6 +1260,23 @@ data class SessionState( * {@link /guide/changesets | Changesets} for an overview of the model. */ val changesets: List? = null, + /** + * Outstanding input the session is blocked on, aggregated across every chat + * so a client can discover and answer it from the session channel alone, + * without subscribing to individual chats. + * + * Each entry is self-sufficient: it carries the owning chat's URI plus every + * identifier the client needs to respond. A client answers by dispatching the + * ordinary `chat/​*` action to that chat's channel — see + * {@link SessionInputRequest} for the per-variant response path. A present, + * non-empty list implies {@link SessionStatus.InputNeeded} on + * {@link SessionSummary.status}. + * + * Host-managed: the host upserts entries with `session/inputNeededSet` as + * chats raise requests and removes them with `session/inputNeededRemoved` + * once the underlying request resolves. + */ + val inputNeeded: List? = null, /** * Additional provider-specific metadata for this session. * @@ -1270,6 +1313,90 @@ data class SessionActiveClient( val customizations: List? = null ) +@Serializable +data class SessionChatInputRequest( + /** + * Stable key for this entry, unique within the session's + * {@link SessionState.inputNeeded} list. The host derives it however it likes + * (for example from the chat URI plus the underlying request or tool-call + * id); consumers MUST treat it as opaque. It is the key for the + * `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + */ + val id: String, + /** + * The chat the underlying request lives in. This is the channel a client + * dispatches its response to — it does not need to have subscribed to that + * chat first. + */ + val chat: String, + val kind: SessionInputRequestKind, + /** + * The mirrored chat input request. + */ + val request: ChatInputRequest +) + +@Serializable +data class SessionToolConfirmationRequest( + /** + * Stable key for this entry, unique within the session's + * {@link SessionState.inputNeeded} list. The host derives it however it likes + * (for example from the chat URI plus the underlying request or tool-call + * id); consumers MUST treat it as opaque. It is the key for the + * `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + */ + val id: String, + /** + * The chat the underlying request lives in. This is the channel a client + * dispatches its response to — it does not need to have subscribed to that + * chat first. + */ + val chat: String, + val kind: SessionInputRequestKind, + /** + * The turn the tool call belongs to. + */ + val turnId: String, + /** + * The tool call awaiting confirmation. + */ + val toolCall: ToolCallConfirmationState +) + +@Serializable +data class SessionToolClientExecutionRequest( + /** + * Stable key for this entry, unique within the session's + * {@link SessionState.inputNeeded} list. The host derives it however it likes + * (for example from the chat URI plus the underlying request or tool-call + * id); consumers MUST treat it as opaque. It is the key for the + * `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + */ + val id: String, + /** + * The chat the underlying request lives in. This is the channel a client + * dispatches its response to — it does not need to have subscribed to that + * chat first. + */ + val chat: String, + val kind: SessionInputRequestKind, + /** + * The turn the tool call belongs to. + */ + val turnId: String, + /** + * The `clientId` expected to execute the tool. Matches the `clientId` of the + * tool call's client {@link ToolCallContributor}. + */ + val clientId: String, + /** + * The running tool call the session wants the owning client to execute. The + * host only ever populates this with a {@link ToolCallRunningState} (i.e. a + * {@link ToolCallState} in `running` status). + */ + val toolCall: ToolCallState +) + @Serializable data class SessionSummary( /** @@ -3951,6 +4078,55 @@ internal object ToolCallStateSerializer : KSerializer { } } +@Serializable(with = ToolCallConfirmationStateSerializer::class) +sealed interface ToolCallConfirmationState + +@JvmInline +value class ToolCallConfirmationStatePendingConfirmation(val value: ToolCallPendingConfirmationState) : ToolCallConfirmationState +@JvmInline +value class ToolCallConfirmationStatePendingResultConfirmation(val value: ToolCallPendingResultConfirmationState) : ToolCallConfirmationState +/** + * Forward-compat catch-all for unknown ToolCallConfirmationState discriminators. + * + * Older clients may receive newer wire variants they don't recognise; capturing + * the raw `JsonObject` lets such payloads round-trip through the client unchanged. + * Reducers handle this variant conservatively on a per-union basis (typically + * as a no-op, but see `Reducers.kt` for the exact treatment). + */ +@JvmInline +value class ToolCallConfirmationStateUnknown(val raw: JsonObject) : ToolCallConfirmationState + +internal object ToolCallConfirmationStateSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("ToolCallConfirmationState") + + override fun deserialize(decoder: Decoder): ToolCallConfirmationState { + val input = decoder as? JsonDecoder + ?: error("ToolCallConfirmationState can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for ToolCallConfirmationState") + val discriminant = (obj["status"] as? JsonPrimitive)?.content + ?: return ToolCallConfirmationStateUnknown(obj) + return when (discriminant) { + "pending-confirmation" -> ToolCallConfirmationStatePendingConfirmation(input.json.decodeFromJsonElement(ToolCallPendingConfirmationState.serializer(), element)) + "pending-result-confirmation" -> ToolCallConfirmationStatePendingResultConfirmation(input.json.decodeFromJsonElement(ToolCallPendingResultConfirmationState.serializer(), element)) + else -> ToolCallConfirmationStateUnknown(obj) + } + } + + override fun serialize(encoder: Encoder, value: ToolCallConfirmationState) { + val output = encoder as? JsonEncoder + ?: error("ToolCallConfirmationState can only be serialized to JSON") + val element: JsonElement = when (value) { + is ToolCallConfirmationStatePendingConfirmation -> output.json.encodeToJsonElement(ToolCallPendingConfirmationState.serializer(), value.value) + is ToolCallConfirmationStatePendingResultConfirmation -> output.json.encodeToJsonElement(ToolCallPendingResultConfirmationState.serializer(), value.value) + is ToolCallConfirmationStateUnknown -> value.raw + } + output.encodeJsonElement(element) + } +} + @Serializable(with = TerminalClaimSerializer::class) sealed interface TerminalClaim @@ -4564,6 +4740,59 @@ internal object ToolCallContributorSerializer : KSerializer } } +@Serializable(with = SessionInputRequestSerializer::class) +sealed interface SessionInputRequest + +@JvmInline +value class SessionInputRequestChatInput(val value: SessionChatInputRequest) : SessionInputRequest +@JvmInline +value class SessionInputRequestToolConfirmation(val value: SessionToolConfirmationRequest) : SessionInputRequest +@JvmInline +value class SessionInputRequestToolClientExecution(val value: SessionToolClientExecutionRequest) : SessionInputRequest +/** + * Forward-compat catch-all for unknown SessionInputRequest discriminators. + * + * Older clients may receive newer wire variants they don't recognise; capturing + * the raw `JsonObject` lets such payloads round-trip through the client unchanged. + * Reducers handle this variant conservatively on a per-union basis (typically + * as a no-op, but see `Reducers.kt` for the exact treatment). + */ +@JvmInline +value class SessionInputRequestUnknown(val raw: JsonObject) : SessionInputRequest + +internal object SessionInputRequestSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("SessionInputRequest") + + override fun deserialize(decoder: Decoder): SessionInputRequest { + val input = decoder as? JsonDecoder + ?: error("SessionInputRequest can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for SessionInputRequest") + val discriminant = (obj["kind"] as? JsonPrimitive)?.content + ?: return SessionInputRequestUnknown(obj) + return when (discriminant) { + "chatInput" -> SessionInputRequestChatInput(input.json.decodeFromJsonElement(SessionChatInputRequest.serializer(), element)) + "toolConfirmation" -> SessionInputRequestToolConfirmation(input.json.decodeFromJsonElement(SessionToolConfirmationRequest.serializer(), element)) + "toolClientExecution" -> SessionInputRequestToolClientExecution(input.json.decodeFromJsonElement(SessionToolClientExecutionRequest.serializer(), element)) + else -> SessionInputRequestUnknown(obj) + } + } + + override fun serialize(encoder: Encoder, value: SessionInputRequest) { + val output = encoder as? JsonEncoder + ?: error("SessionInputRequest can only be serialized to JSON") + val element: JsonElement = when (value) { + is SessionInputRequestChatInput -> output.json.encodeToJsonElement(SessionChatInputRequest.serializer(), value.value) + is SessionInputRequestToolConfirmation -> output.json.encodeToJsonElement(SessionToolConfirmationRequest.serializer(), value.value) + is SessionInputRequestToolClientExecution -> output.json.encodeToJsonElement(SessionToolClientExecutionRequest.serializer(), value.value) + is SessionInputRequestUnknown -> value.raw + } + output.encodeJsonElement(element) + } +} + @Serializable(with = ToolResultContentSerializer::class) sealed interface ToolResultContent { @JvmInline value class Text(val value: ToolResultTextContent) : ToolResultContent diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index ed90eda0..641b7c57 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -23,6 +23,12 @@ matching `## [X.Y.Z]` heading is missing from this file. for lightweight session-list presentation hints. - `StateAction::SessionActiveClientRemoved` (`SessionActiveClientRemovedAction`) to release a single active client by `client_id`. +- `SessionState.input_needed` — a session-level aggregate of outstanding input + requests across all chats (`SessionInputRequest` enum with + `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and + `SessionToolClientExecutionRequest` variants), plus the + `StateAction::SessionInputNeededSet` / `StateAction::SessionInputNeededRemoved` + actions and the `ToolCallConfirmationState` union. - `ahp-ws` TLS backend is now selectable via Cargo features: `native-tls`, `rustls-tls-native-roots` (default), and `rustls-tls-webpki-roots`. The crate no longer forces `tokio-tungstenite/native-tls` onto the dependency graph, so diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index a6fd9e09..02502280 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -16,9 +16,9 @@ use crate::state::{ ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ChatSummary, ConfirmationOption, Customization, ErrorInfo, McpServerState, Message, ModelSelection, - PendingMessageKind, ResponsePart, SessionActiveClient, TerminalClaim, TerminalInfo, TextRange, - ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, - ToolDefinition, ToolResultContent, UsageInfo, + PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest, TerminalClaim, + TerminalInfo, TextRange, ToolCallCancellationReason, ToolCallConfirmationReason, + ToolCallContributor, ToolCallResult, ToolDefinition, ToolResultContent, UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -84,6 +84,10 @@ pub enum ActionType { SessionActiveClientSet, #[serde(rename = "session/activeClientRemoved")] SessionActiveClientRemoved, + #[serde(rename = "session/inputNeededSet")] + SessionInputNeededSet, + #[serde(rename = "session/inputNeededRemoved")] + SessionInputNeededRemoved, #[serde(rename = "chat/pendingMessageSet")] ChatPendingMessageSet, #[serde(rename = "chat/pendingMessageRemoved")] @@ -823,6 +827,40 @@ pub struct SessionActiveClientRemovedAction { pub client_id: String, } +/// A session-level input request was added or updated. +/// +/// Upsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the +/// host dispatches this with the full {@link SessionInputRequest} to append a new +/// entry to {@link SessionState.inputNeeded} or replace the existing entry with +/// the same `id`. +/// +/// Server-originated: the host mirrors chat-level requests (elicitations, tool +/// confirmations, client-tool executions) into the session aggregate so clients +/// subscribed only to the session channel can discover them. Clients respond by +/// dispatching the ordinary `chat/*` action to the entry's `chat` channel — see +/// {@link SessionInputRequest}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionInputNeededSetAction { + /// The input request to add or update, matched by `id`. + pub request: SessionInputRequest, +} + +/// A session-level input request was removed. +/// +/// Removes the entry identified by `id` from +/// {@link SessionState.inputNeeded}; a no-op when no entry matches. +/// +/// Server-originated: the host dispatches this once the underlying request +/// resolves (the user answers, the tool call is confirmed, or the client +/// reports its result). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionInputNeededRemovedAction { + /// The `id` of the input request to remove. + pub id: String, +} + /// A pending message was set (upsert semantics: creates or replaces). /// /// For steering messages, this always replaces the single steering message. @@ -1530,6 +1568,10 @@ pub enum StateAction { SessionActiveClientSet(SessionActiveClientSetAction), #[serde(rename = "session/activeClientRemoved")] SessionActiveClientRemoved(SessionActiveClientRemovedAction), + #[serde(rename = "session/inputNeededSet")] + SessionInputNeededSet(SessionInputNeededSetAction), + #[serde(rename = "session/inputNeededRemoved")] + SessionInputNeededRemoved(SessionInputNeededRemovedAction), #[serde(rename = "chat/pendingMessageSet")] ChatPendingMessageSet(ChatPendingMessageSetAction), #[serde(rename = "chat/pendingMessageRemoved")] diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index e9cab5dc..722285fd 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -228,6 +228,24 @@ pub enum ChatInputResponseKind { Cancel, } +/// Discriminant for the kinds of outstanding input a session can surface in +/// {@link SessionState.inputNeeded}. +/// +/// This is a general/typological union (not a lifecycle), so the discriminant is +/// a `*Kind`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SessionInputRequestKind { + /// A user-facing elicitation mirrored from a chat's `inputRequests`. + #[serde(rename = "chatInput")] + ChatInput, + /// A tool call awaiting parameter- or result-confirmation. + #[serde(rename = "toolConfirmation")] + ToolConfirmation, + /// A running tool the session wants an active client to execute. + #[serde(rename = "toolClientExecution")] + ToolClientExecution, +} + /// How a turn ended. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum TurnState { @@ -1057,6 +1075,22 @@ pub struct SessionState { /// {@link /guide/changesets | Changesets} for an overview of the model. #[serde(default, skip_serializing_if = "Option::is_none")] pub changesets: Option>, + /// Outstanding input the session is blocked on, aggregated across every chat + /// so a client can discover and answer it from the session channel alone, + /// without subscribing to individual chats. + /// + /// Each entry is self-sufficient: it carries the owning chat's URI plus every + /// identifier the client needs to respond. A client answers by dispatching the + /// ordinary `chat/*` action to that chat's channel — see + /// {@link SessionInputRequest} for the per-variant response path. A present, + /// non-empty list implies {@link SessionStatus.InputNeeded} on + /// {@link SessionSummary.status}. + /// + /// Host-managed: the host upserts entries with `session/inputNeededSet` as + /// chats raise requests and removes them with `session/inputNeededRemoved` + /// once the underlying request resolves. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input_needed: Option>, /// Additional provider-specific metadata for this session. /// /// Clients MAY look for well-known keys here to provide enhanced UI. @@ -1091,6 +1125,92 @@ pub struct SessionActiveClient { pub customizations: Option>, } +/// A user-input elicitation surfaced at the session level, mirroring one entry +/// of the owning chat's {@link ChatState.inputRequests}. +/// +/// Respond by dispatching `chat/inputCompleted` (or syncing drafts with +/// `chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`}, +/// keyed by {@link ChatInputRequest.id | `request.id`}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionChatInputRequest { + /// Stable key for this entry, unique within the session's + /// {@link SessionState.inputNeeded} list. The host derives it however it likes + /// (for example from the chat URI plus the underlying request or tool-call + /// id); consumers MUST treat it as opaque. It is the key for the + /// `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + pub id: String, + /// The chat the underlying request lives in. This is the channel a client + /// dispatches its response to — it does not need to have subscribed to that + /// chat first. + pub chat: Uri, + /// The mirrored chat input request. + pub request: ChatInputRequest, +} + +/// A tool call blocked on confirmation — either parameter confirmation before +/// execution or result confirmation after — surfaced at the session level. +/// +/// Respond by dispatching `chat/toolCallConfirmed` (for +/// {@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed` +/// (for {@link ToolCallPendingResultConfirmationState}) to +/// {@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and +/// `toolCall.toolCallId`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionToolConfirmationRequest { + /// Stable key for this entry, unique within the session's + /// {@link SessionState.inputNeeded} list. The host derives it however it likes + /// (for example from the chat URI plus the underlying request or tool-call + /// id); consumers MUST treat it as opaque. It is the key for the + /// `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + pub id: String, + /// The chat the underlying request lives in. This is the channel a client + /// dispatches its response to — it does not need to have subscribed to that + /// chat first. + pub chat: Uri, + /// The turn the tool call belongs to. + pub turn_id: String, + /// The tool call awaiting confirmation. + pub tool_call: ToolCallConfirmationState, +} + +/// A running tool whose execution is delegated to an active client. Surfaced so +/// a client that provides the tool can pick up the work without subscribing to +/// the owning chat. +/// +/// The {@link toolCall} is always a {@link ToolCallRunningState} (a +/// {@link ToolCallState} in `running` status) whose +/// {@link ToolCallRunningState.contributor | `contributor`} is a client +/// {@link ToolCallClientContributor} whose `clientId` matches the denormalized +/// {@link clientId} here. Execute and report the result by dispatching +/// `chat/toolCallComplete` (and optionally streaming with +/// `chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat | +/// `chat`}, keyed by `turnId` and `toolCall.toolCallId`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionToolClientExecutionRequest { + /// Stable key for this entry, unique within the session's + /// {@link SessionState.inputNeeded} list. The host derives it however it likes + /// (for example from the chat URI plus the underlying request or tool-call + /// id); consumers MUST treat it as opaque. It is the key for the + /// `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + pub id: String, + /// The chat the underlying request lives in. This is the channel a client + /// dispatches its response to — it does not need to have subscribed to that + /// chat first. + pub chat: Uri, + /// The turn the tool call belongs to. + pub turn_id: String, + /// The `clientId` expected to execute the tool. Matches the `clientId` of the + /// tool call's client {@link ToolCallContributor}. + pub client_id: String, + /// The running tool call the session wants the owning client to execute. The + /// host only ever populates this with a {@link ToolCallRunningState} (i.e. a + /// {@link ToolCallState} in `running` status). + pub tool_call: ToolCallState, +} + /// Lightweight catalog entry summarizing one session. Surfaced via /// {@link RootChannelCommands.listSessions | `root/listSessions`} and /// `root/sessionAdded`/`root/sessionSummaryChanged` notifications. @@ -3456,6 +3576,20 @@ pub enum ToolCallState { Unknown(serde_json::Value), } +/// A tool call blocked on parameter- or result-confirmation. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "status")] +pub enum ToolCallConfirmationState { + #[serde(rename = "pending-confirmation")] + PendingConfirmation(ToolCallPendingConfirmationState), + #[serde(rename = "pending-result-confirmation")] + PendingResultConfirmation(ToolCallPendingResultConfirmationState), + /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. + /// Reducers treat this as a no-op. + #[serde(untagged)] + Unknown(serde_json::Value), +} + /// Who currently holds a terminal. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind")] @@ -3672,6 +3806,22 @@ pub enum ToolCallContributor { Unknown(serde_json::Value), } +/// One outstanding piece of input a session is blocked on, aggregated across all chats. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum SessionInputRequest { + #[serde(rename = "chatInput")] + ChatInput(SessionChatInputRequest), + #[serde(rename = "toolConfirmation")] + ToolConfirmation(SessionToolConfirmationRequest), + #[serde(rename = "toolClientExecution")] + ToolClientExecution(SessionToolClientExecutionRequest), + /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. + /// Reducers treat this as a no-op. + #[serde(untagged)] + Unknown(serde_json::Value), +} + /// The state payload of a snapshot — root, session, chat, terminal, /// changeset, resource-watch, or annotations state. /// diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 74e83755..721926e4 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -61,6 +61,7 @@ use ahp_types::state::{ ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, ErrorInfo, PendingMessage, PendingMessageKind, ResourceWatchState, ResponsePart, RootState, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, TerminalContentPart, + SessionInputRequest, TerminalState, TerminalUnclassifiedPart, ToolCallCancellationReason, ToolCallCancelledState, ToolCallCompletedState, ToolCallConfirmationReason, ToolCallContributor, ToolCallPendingConfirmationState, ToolCallPendingResultConfirmationState, ToolCallResponsePart, @@ -364,6 +365,15 @@ fn customization_id(c: &Customization) -> Option<&str> { } } +fn session_input_request_id(r: &SessionInputRequest) -> Option<&str> { + match r { + SessionInputRequest::ChatInput(x) => Some(x.id.as_str()), + SessionInputRequest::ToolConfirmation(x) => Some(x.id.as_str()), + SessionInputRequest::ToolClientExecution(x) => Some(x.id.as_str()), + SessionInputRequest::Unknown(_) => None, + } +} + fn child_id_of(c: &ChildCustomization) -> Option<&str> { match c { ChildCustomization::Agent(x) => Some(x.id.as_str()), @@ -662,6 +672,34 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - state.active_clients.remove(idx); ReduceOutcome::Applied } + StateAction::SessionInputNeededSet(a) => { + let Some(action_id) = session_input_request_id(&a.request) else { + return ReduceOutcome::NoOp; + }; + let list = state.input_needed.get_or_insert_with(Vec::new); + if let Some(idx) = list + .iter() + .position(|r| session_input_request_id(r) == Some(action_id)) + { + list[idx] = a.request.clone(); + } else { + list.push(a.request.clone()); + } + ReduceOutcome::Applied + } + StateAction::SessionInputNeededRemoved(a) => { + let Some(list) = state.input_needed.as_mut() else { + return ReduceOutcome::NoOp; + }; + let Some(idx) = list + .iter() + .position(|r| session_input_request_id(r) == Some(a.id.as_str())) + else { + return ReduceOutcome::NoOp; + }; + list.remove(idx); + ReduceOutcome::Applied + } StateAction::SessionCustomizationsChanged(a) => { state.customizations = Some(a.customizations.clone()); ReduceOutcome::Applied @@ -1578,6 +1616,7 @@ mod tests { config: None, customizations: None, changesets: None, + input_needed: None, meta: None, } } diff --git a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs index 6a272a8b..9accf8e3 100644 --- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs @@ -70,6 +70,7 @@ fn session_state(title: &str, resource: &str) -> SessionState { config: None, customizations: None, changesets: None, + input_needed: None, meta: None, } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index bc0edd10..93bf558e 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -35,6 +35,8 @@ public enum ActionType: String, Codable, Sendable { case sessionServerToolsChanged = "session/serverToolsChanged" case sessionActiveClientSet = "session/activeClientSet" case sessionActiveClientRemoved = "session/activeClientRemoved" + case sessionInputNeededSet = "session/inputNeededSet" + case sessionInputNeededRemoved = "session/inputNeededRemoved" case chatPendingMessageSet = "chat/pendingMessageSet" case chatPendingMessageRemoved = "chat/pendingMessageRemoved" case chatQueuedMessagesReordered = "chat/queuedMessagesReordered" @@ -1007,6 +1009,34 @@ public struct SessionActiveClientRemovedAction: Codable, Sendable { } } +public struct SessionInputNeededSetAction: Codable, Sendable { + public var type: ActionType + /// The input request to add or update, matched by `id`. + public var request: SessionInputRequest + + public init( + type: ActionType, + request: SessionInputRequest + ) { + self.type = type + self.request = request + } +} + +public struct SessionInputNeededRemovedAction: Codable, Sendable { + public var type: ActionType + /// The `id` of the input request to remove. + public var id: String + + public init( + type: ActionType, + id: String + ) { + self.type = type + self.id = id + } +} + public struct ChatPendingMessageSetAction: Codable, Sendable { public var type: ActionType /// Whether this is a steering or queued message @@ -1778,6 +1808,8 @@ public enum StateAction: Codable, Sendable { case sessionServerToolsChanged(SessionServerToolsChangedAction) case sessionActiveClientSet(SessionActiveClientSetAction) case sessionActiveClientRemoved(SessionActiveClientRemovedAction) + case sessionInputNeededSet(SessionInputNeededSetAction) + case sessionInputNeededRemoved(SessionInputNeededRemovedAction) case chatPendingMessageSet(ChatPendingMessageSetAction) case chatPendingMessageRemoved(ChatPendingMessageRemovedAction) case chatQueuedMessagesReordered(ChatQueuedMessagesReorderedAction) @@ -1896,6 +1928,10 @@ public enum StateAction: Codable, Sendable { self = .sessionActiveClientSet(try SessionActiveClientSetAction(from: decoder)) case "session/activeClientRemoved": self = .sessionActiveClientRemoved(try SessionActiveClientRemovedAction(from: decoder)) + case "session/inputNeededSet": + self = .sessionInputNeededSet(try SessionInputNeededSetAction(from: decoder)) + case "session/inputNeededRemoved": + self = .sessionInputNeededRemoved(try SessionInputNeededRemovedAction(from: decoder)) case "chat/pendingMessageSet": self = .chatPendingMessageSet(try ChatPendingMessageSetAction(from: decoder)) case "chat/pendingMessageRemoved": @@ -2016,6 +2052,8 @@ public enum StateAction: Codable, Sendable { case .sessionServerToolsChanged(let v): try v.encode(to: encoder) case .sessionActiveClientSet(let v): try v.encode(to: encoder) case .sessionActiveClientRemoved(let v): try v.encode(to: encoder) + case .sessionInputNeededSet(let v): try v.encode(to: encoder) + case .sessionInputNeededRemoved(let v): try v.encode(to: encoder) case .chatPendingMessageSet(let v): try v.encode(to: encoder) case .chatPendingMessageRemoved(let v): try v.encode(to: encoder) case .chatQueuedMessagesReordered(let v): try v.encode(to: encoder) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 3f6cdb1e..df883343 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -147,6 +147,20 @@ public enum ChatInputResponseKind: String, Codable, Sendable { case cancel = "cancel" } +/// Discriminant for the kinds of outstanding input a session can surface in +/// {@link SessionState.inputNeeded}. +/// +/// This is a general/typological union (not a lifecycle), so the discriminant is +/// a `*Kind`. +public enum SessionInputRequestKind: String, Codable, Sendable { + /// A user-facing elicitation mirrored from a chat's `inputRequests`. + case chatInput = "chatInput" + /// A tool call awaiting parameter- or result-confirmation. + case toolConfirmation = "toolConfirmation" + /// A running tool the session wants an active client to execute. + case toolClientExecution = "toolClientExecution" +} + /// How a turn ended. public enum TurnState: String, Codable, Sendable { case complete = "complete" @@ -993,6 +1007,21 @@ public struct SessionState: Codable, Sendable { /// before subscribing. See {@link Changeset} for the full shape and /// {@link /guide/changesets | Changesets} for an overview of the model. public var changesets: [Changeset]? + /// Outstanding input the session is blocked on, aggregated across every chat + /// so a client can discover and answer it from the session channel alone, + /// without subscribing to individual chats. + /// + /// Each entry is self-sufficient: it carries the owning chat's URI plus every + /// identifier the client needs to respond. A client answers by dispatching the + /// ordinary `chat/*` action to that chat's channel — see + /// {@link SessionInputRequest} for the per-variant response path. A present, + /// non-empty list implies {@link SessionStatus.InputNeeded} on + /// {@link SessionSummary.status}. + /// + /// Host-managed: the host upserts entries with `session/inputNeededSet` as + /// chats raise requests and removes them with `session/inputNeededRemoved` + /// once the underlying request resolves. + public var inputNeeded: [SessionInputRequest]? /// Additional provider-specific metadata for this session. /// /// Clients MAY look for well-known keys here to provide enhanced UI. @@ -1011,6 +1040,7 @@ public struct SessionState: Codable, Sendable { case config case customizations case changesets + case inputNeeded case meta = "_meta" } @@ -1025,6 +1055,7 @@ public struct SessionState: Codable, Sendable { config: SessionConfigState? = nil, customizations: [Customization]? = nil, changesets: [Changeset]? = nil, + inputNeeded: [SessionInputRequest]? = nil, meta: [String: AnyCodable]? = nil ) { self.summary = summary @@ -1037,6 +1068,7 @@ public struct SessionState: Codable, Sendable { self.config = config self.customizations = customizations self.changesets = changesets + self.inputNeeded = inputNeeded self.meta = meta } } @@ -1069,6 +1101,105 @@ public struct SessionActiveClient: Codable, Sendable { } } +public struct SessionChatInputRequest: Codable, Sendable { + /// Stable key for this entry, unique within the session's + /// {@link SessionState.inputNeeded} list. The host derives it however it likes + /// (for example from the chat URI plus the underlying request or tool-call + /// id); consumers MUST treat it as opaque. It is the key for the + /// `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + public var id: String + /// The chat the underlying request lives in. This is the channel a client + /// dispatches its response to — it does not need to have subscribed to that + /// chat first. + public var chat: String + public var kind: SessionInputRequestKind + /// The mirrored chat input request. + public var request: ChatInputRequest + + public init( + id: String, + chat: String, + kind: SessionInputRequestKind, + request: ChatInputRequest + ) { + self.id = id + self.chat = chat + self.kind = kind + self.request = request + } +} + +public struct SessionToolConfirmationRequest: Codable, Sendable { + /// Stable key for this entry, unique within the session's + /// {@link SessionState.inputNeeded} list. The host derives it however it likes + /// (for example from the chat URI plus the underlying request or tool-call + /// id); consumers MUST treat it as opaque. It is the key for the + /// `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + public var id: String + /// The chat the underlying request lives in. This is the channel a client + /// dispatches its response to — it does not need to have subscribed to that + /// chat first. + public var chat: String + public var kind: SessionInputRequestKind + /// The turn the tool call belongs to. + public var turnId: String + /// The tool call awaiting confirmation. + public var toolCall: ToolCallConfirmationState + + public init( + id: String, + chat: String, + kind: SessionInputRequestKind, + turnId: String, + toolCall: ToolCallConfirmationState + ) { + self.id = id + self.chat = chat + self.kind = kind + self.turnId = turnId + self.toolCall = toolCall + } +} + +public struct SessionToolClientExecutionRequest: Codable, Sendable { + /// Stable key for this entry, unique within the session's + /// {@link SessionState.inputNeeded} list. The host derives it however it likes + /// (for example from the chat URI plus the underlying request or tool-call + /// id); consumers MUST treat it as opaque. It is the key for the + /// `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + public var id: String + /// The chat the underlying request lives in. This is the channel a client + /// dispatches its response to — it does not need to have subscribed to that + /// chat first. + public var chat: String + public var kind: SessionInputRequestKind + /// The turn the tool call belongs to. + public var turnId: String + /// The `clientId` expected to execute the tool. Matches the `clientId` of the + /// tool call's client {@link ToolCallContributor}. + public var clientId: String + /// The running tool call the session wants the owning client to execute. The + /// host only ever populates this with a {@link ToolCallRunningState} (i.e. a + /// {@link ToolCallState} in `running` status). + public var toolCall: ToolCallState + + public init( + id: String, + chat: String, + kind: SessionInputRequestKind, + turnId: String, + clientId: String, + toolCall: ToolCallState + ) { + self.id = id + self.chat = chat + self.kind = kind + self.turnId = turnId + self.clientId = clientId + self.toolCall = toolCall + } +} + public struct SessionSummary: Codable, Sendable { /// Session URI public var resource: String @@ -4300,6 +4431,39 @@ public enum ToolCallState: Codable, Sendable { } } +public enum ToolCallConfirmationState: Codable, Sendable { + case pendingConfirmation(ToolCallPendingConfirmationState) + case pendingResultConfirmation(ToolCallPendingResultConfirmationState) + /// Unknown or future discriminant; the raw payload is preserved + /// and re-encoded verbatim for forward-compatibility. + case unknown(AnyCodable) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "status" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "pending-confirmation": + self = .pendingConfirmation(try ToolCallPendingConfirmationState(from: decoder)) + case "pending-result-confirmation": + self = .pendingResultConfirmation(try ToolCallPendingResultConfirmationState(from: decoder)) + default: + self = .unknown(try AnyCodable(from: decoder)) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .pendingConfirmation(let value): try value.encode(to: encoder) + case .pendingResultConfirmation(let value): try value.encode(to: encoder) + case .unknown(let value): try value.encode(to: encoder) + } + } +} + public enum TerminalClaim: Codable, Sendable { case client(TerminalClientClaim) case session(TerminalSessionClaim) @@ -4735,6 +4899,43 @@ public enum ToolCallContributor: Codable, Sendable { } } +public enum SessionInputRequest: Codable, Sendable { + case chatInput(SessionChatInputRequest) + case toolConfirmation(SessionToolConfirmationRequest) + case toolClientExecution(SessionToolClientExecutionRequest) + /// Unknown or future discriminant; the raw payload is preserved + /// and re-encoded verbatim for forward-compatibility. + case unknown(AnyCodable) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "chatInput": + self = .chatInput(try SessionChatInputRequest(from: decoder)) + case "toolConfirmation": + self = .toolConfirmation(try SessionToolConfirmationRequest(from: decoder)) + case "toolClientExecution": + self = .toolClientExecution(try SessionToolClientExecutionRequest(from: decoder)) + default: + self = .unknown(try AnyCodable(from: decoder)) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .chatInput(let value): try value.encode(to: encoder) + case .toolConfirmation(let value): try value.encode(to: encoder) + case .toolClientExecution(let value): try value.encode(to: encoder) + case .unknown(let value): try value.encode(to: encoder) + } + } +} + public enum ToolResultContent: Codable, Sendable { case text(ToolResultTextContent) case embeddedResource(ToolResultEmbeddedResourceContent) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 9ce944a4..a8a1702b 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -35,6 +35,16 @@ private func resolveSelectedOption(_ options: [ConfirmationOption]?, id: String? return options.first { $0.id == id } } +/// Extracts the stable `id` of a session input request, or `nil` for unknown variants. +private func sessionInputRequestID(_ r: SessionInputRequest) -> String? { + switch r { + case .chatInput(let x): return x.id + case .toolConfirmation(let x): return x.id + case .toolClientExecution(let x): return x.id + case .unknown: return nil + } +} + // MARK: - Root Reducer /// Pure reducer for root state. @@ -587,6 +597,26 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS next.activeClients.remove(at: idx) return next + case .sessionInputNeededSet(let a): + guard let id = sessionInputRequestID(a.request) else { return state } + var next = state + var list = next.inputNeeded ?? [] + if let idx = list.firstIndex(where: { sessionInputRequestID($0) == id }) { + list[idx] = a.request + } else { + list.append(a.request) + } + next.inputNeeded = list + return next + + case .sessionInputNeededRemoved(let a): + guard var list = state.inputNeeded, + let idx = list.firstIndex(where: { sessionInputRequestID($0) == a.id }) else { return state } + var next = state + list.remove(at: idx) + next.inputNeeded = list + return next + // ── Customizations ────────────────────────────────────────────────── case .sessionCustomizationsChanged(let a): diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index c6d857ac..a4cfe569 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -26,6 +26,12 @@ the tag matches the version pinned in [`VERSION`](VERSION). - `SessionActiveClientRemovedAction` (`StateAction.sessionActiveClientRemoved`, wire `session/activeClientRemoved`) to release a single active client by `clientId`. +- `SessionState.inputNeeded` — a session-level aggregate of outstanding input + requests across all chats (`SessionInputRequest` enum with + `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and + `SessionToolClientExecutionRequest` cases), plus the + `StateAction.sessionInputNeededSet` / `StateAction.sessionInputNeededRemoved` + actions and the `ToolCallConfirmationState` union. ### Changed diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index ab7e9ef7..3d3cdd96 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -29,6 +29,13 @@ hotfix escape hatch. - Exported `JsonPrimitive` type alias (`string | number | boolean | null`). - `SessionActiveClientRemovedAction` (`session/activeClientRemoved`) to release a single active client by `clientId`. +- `SessionState.inputNeeded` — a session-level aggregate of outstanding input + requests across all chats (`SessionInputRequest` union with + `SessionChatInputRequest`, `SessionToolConfirmationRequest`, and + `SessionToolClientExecutionRequest`), plus the `SessionInputNeededSetAction` + (`session/inputNeededSet`) and `SessionInputNeededRemovedAction` + (`session/inputNeededRemoved`) actions and the `ToolCallConfirmationState` + union. ### Changed diff --git a/docs/specification/session-channel.md b/docs/specification/session-channel.md index 0d5578ec..615d2ef8 100644 --- a/docs/specification/session-channel.md +++ b/docs/specification/session-channel.md @@ -14,7 +14,7 @@ Multiple session channels may be active simultaneously. Clients subscribe to eac ## State -Subscribers receive a [`SessionState`](/reference/session#sessionstate) snapshot containing the session summary, lifecycle phase, the catalog of [`chats`](/reference/session#sessionstate) belonging to this session, the optional [`defaultChat`](/reference/session#sessionstate) routing hint, model and active-client state, customizations, changesets, and per-session configuration. Per-conversation state (turns, streaming, tool calls, pending messages, input requests) lives on the [chat channel](./chat-channel). Refer to the [State Model guide](/guide/state-model) for a structural overview. +Subscribers receive a [`SessionState`](/reference/session#sessionstate) snapshot containing the session summary, lifecycle phase, the catalog of [`chats`](/reference/session#sessionstate) belonging to this session, the optional [`defaultChat`](/reference/session#sessionstate) routing hint, model and active-client state, customizations, changesets, the [`inputNeeded`](#aggregated-input-requests) aggregate, and per-session configuration. Per-conversation state (turns, streaming, tool calls, pending messages, input requests) lives on the [chat channel](./chat-channel). Refer to the [State Model guide](/guide/state-model) for a structural overview. ## Lifecycle @@ -71,6 +71,22 @@ The producer of the chat's own [`ChatState`](./chat-channel#state) is responsibl Sessions with a single chat satisfy all of the above trivially (the chat's values pass through). The rules only matter once a session carries multiple chats. +### Aggregated input requests + +A chat blocks on user input (an [elicitation](/guide/elicitation)) or on a tool confirmation deep inside its turn state. Discovering those blocks would normally require subscribing to every chat channel — impractical for a mobile app or a tool-providing client that only watches the session. + +[`SessionState.inputNeeded`](/reference/session#sessionstate) is a session-level roll-up of every outstanding block across all chats. The host upserts entries with `session/inputNeededSet` and removes them with `session/inputNeededRemoved` as the underlying chat-level requests appear and resolve. Whenever the list is non-empty the session's [`status`](#chat-aggregation) carries the `InputNeeded` bit. + +Each entry is a [`SessionInputRequest`](/reference/session#sessioninputrequest) — a discriminated union over `kind`: + +| `kind` | Carries | Respond by dispatching… | +|---|---|---| +| `chatInput` | the mirrored [`ChatInputRequest`](/reference/chat#chatinputrequest) | `chat/inputCompleted` (or `chat/inputAnswerChanged`) | +| `toolConfirmation` | a [`ToolCallConfirmationState`](/reference/chat#toolcallconfirmationstate) plus `turnId` | `chat/toolCallConfirmed` or `chat/toolCallResultConfirmed` | +| `toolClientExecution` | a [`ToolCallState`](/reference/chat#toolcallstate) in `running` status plus `turnId` and the owning `clientId` | `chat/toolCallComplete` (optionally `chat/toolCallContentChanged`) | + +Every entry carries the owning `chat` URI plus the identifiers (`request.id`, or `turnId` + `toolCall.toolCallId`) needed to construct the response. A client therefore answers by dispatching the ordinary `chat/*` action **to that chat's channel** — it does **not** need to have subscribed to the chat first. `inputNeeded` is a read/respond convenience surface, not a separate response protocol: the chat channel remains the source of truth and the host removes the aggregate entry once the chat-level request resolves. + ### Disposal ```jsonc diff --git a/schema/actions.schema.json b/schema/actions.schema.json index d61424a7..0395ca2f 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -446,6 +446,40 @@ "clientId" ] }, + "SessionInputNeededSetAction": { + "type": "object", + "description": "A session-level input request was added or updated.\n\nUpsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the\nhost dispatches this with the full {@link SessionInputRequest} to append a new\nentry to {@link SessionState.inputNeeded} or replace the existing entry with\nthe same `id`.\n\nServer-originated: the host mirrors chat-level requests (elicitations, tool\nconfirmations, client-tool executions) into the session aggregate so clients\nsubscribed only to the session channel can discover them. Clients respond by\ndispatching the ordinary `chat/*` action to the entry's `chat` channel — see\n{@link SessionInputRequest}.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionInputNeededSet" + }, + "request": { + "$ref": "#/$defs/SessionInputRequest", + "description": "The input request to add or update, matched by `id`." + } + }, + "required": [ + "type", + "request" + ] + }, + "SessionInputNeededRemovedAction": { + "type": "object", + "description": "A session-level input request was removed.\n\nRemoves the entry identified by `id` from\n{@link SessionState.inputNeeded}; a no-op when no entry matches.\n\nServer-originated: the host dispatches this once the underlying request\nresolves (the user answers, the tool call is confirmed, or the client\nreports its result).", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionInputNeededRemoved" + }, + "id": { + "type": "string", + "description": "The `id` of the input request to remove." + } + }, + "required": [ + "type", + "id" + ] + }, "SessionCustomizationsChangedAction": { "type": "object", "description": "The session's customizations have changed.\n\nFull-replacement semantics: the `customizations` array replaces the\nprevious `customizations` entirely.", @@ -2563,6 +2597,13 @@ }, "description": "Catalogue of changesets the server can produce for this session. Each\nentry advertises a subscribable view of file changes (uncommitted,\nsession-wide, per-turn, etc.) and the URI template the client expands\nbefore subscribing. See {@link Changeset} for the full shape and\n{@link /guide/changesets | Changesets} for an overview of the model." }, + "inputNeeded": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionInputRequest" + }, + "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -2608,6 +2649,120 @@ "tools" ] }, + "SessionInputRequestBase": { + "type": "object", + "description": "Fields common to every {@link SessionInputRequest} variant.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + } + }, + "required": [ + "id", + "chat" + ] + }, + "SessionChatInputRequest": { + "type": "object", + "description": "A user-input elicitation surfaced at the session level, mirroring one entry\nof the owning chat's {@link ChatState.inputRequests}.\n\nRespond by dispatching `chat/inputCompleted` (or syncing drafts with\n`chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`},\nkeyed by {@link ChatInputRequest.id | `request.id`}.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ChatInput" + }, + "request": { + "$ref": "#/$defs/ChatInputRequest", + "description": "The mirrored chat input request." + } + }, + "required": [ + "id", + "chat", + "kind", + "request" + ] + }, + "SessionToolConfirmationRequest": { + "type": "object", + "description": "A tool call blocked on confirmation — either parameter confirmation before\nexecution or result confirmation after — surfaced at the session level.\n\nRespond by dispatching `chat/toolCallConfirmed` (for\n{@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed`\n(for {@link ToolCallPendingResultConfirmationState}) to\n{@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and\n`toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolConfirmation" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallConfirmationState", + "description": "The tool call awaiting confirmation." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "toolCall" + ] + }, + "SessionToolClientExecutionRequest": { + "type": "object", + "description": "A running tool whose execution is delegated to an active client. Surfaced so\na client that provides the tool can pick up the work without subscribing to\nthe owning chat.\n\nThe {@link toolCall} is always a {@link ToolCallRunningState} (a\n{@link ToolCallState} in `running` status) whose\n{@link ToolCallRunningState.contributor | `contributor`} is a client\n{@link ToolCallClientContributor} whose `clientId` matches the denormalized\n{@link clientId} here. Execute and report the result by dispatching\n`chat/toolCallComplete` (and optionally streaming with\n`chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat |\n`chat`}, keyed by `turnId` and `toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolClientExecution" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "clientId": { + "type": "string", + "description": "The `clientId` expected to execute the tool. Matches the `clientId` of the\ntool call's client {@link ToolCallContributor}." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "The running tool call the session wants the owning client to execute. The\nhost only ever populates this with a {@link ToolCallRunningState} (i.e. a\n{@link ToolCallState} in `running` status)." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "clientId", + "toolCall" + ] + }, "ProjectInfo": { "type": "object", "description": "Server-owned project metadata for a session.", @@ -6011,6 +6166,21 @@ ], "description": "A primitive JSON value: a string, number, boolean, or `null`." }, + "SessionInputRequest": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/SessionChatInputRequest" + }, + { + "$ref": "#/$defs/SessionToolConfirmationRequest" + }, + { + "$ref": "#/$defs/SessionToolClientExecutionRequest" + } + ], + "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat**. The host removes the entry with `session/inputNeededRemoved`\nonce the underlying request resolves." + }, "ChildCustomizationType": { "oneOf": [ {}, @@ -6290,6 +6460,18 @@ ], "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." }, + "ToolCallConfirmationState": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/ToolCallPendingConfirmationState" + }, + { + "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + } + ], + "description": "The two tool-call states that block on a client confirmation: parameter\nconfirmation before execution ({@link ToolCallPendingConfirmationState}) and\nresult confirmation after execution\n({@link ToolCallPendingResultConfirmationState}).\n\nSurfaced at the session level by {@link SessionToolConfirmationRequest}." + }, "ToolResultContent": { "oneOf": [ {}, @@ -6391,6 +6573,12 @@ { "$ref": "#/$defs/SessionActiveClientRemovedAction" }, + { + "$ref": "#/$defs/SessionInputNeededSetAction" + }, + { + "$ref": "#/$defs/SessionInputNeededRemovedAction" + }, { "$ref": "#/$defs/SessionCustomizationsChangedAction" }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 8df2c20b..7c381061 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1909,6 +1909,13 @@ }, "description": "Catalogue of changesets the server can produce for this session. Each\nentry advertises a subscribable view of file changes (uncommitted,\nsession-wide, per-turn, etc.) and the URI template the client expands\nbefore subscribing. See {@link Changeset} for the full shape and\n{@link /guide/changesets | Changesets} for an overview of the model." }, + "inputNeeded": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionInputRequest" + }, + "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -1954,6 +1961,120 @@ "tools" ] }, + "SessionInputRequestBase": { + "type": "object", + "description": "Fields common to every {@link SessionInputRequest} variant.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + } + }, + "required": [ + "id", + "chat" + ] + }, + "SessionChatInputRequest": { + "type": "object", + "description": "A user-input elicitation surfaced at the session level, mirroring one entry\nof the owning chat's {@link ChatState.inputRequests}.\n\nRespond by dispatching `chat/inputCompleted` (or syncing drafts with\n`chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`},\nkeyed by {@link ChatInputRequest.id | `request.id`}.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ChatInput" + }, + "request": { + "$ref": "#/$defs/ChatInputRequest", + "description": "The mirrored chat input request." + } + }, + "required": [ + "id", + "chat", + "kind", + "request" + ] + }, + "SessionToolConfirmationRequest": { + "type": "object", + "description": "A tool call blocked on confirmation — either parameter confirmation before\nexecution or result confirmation after — surfaced at the session level.\n\nRespond by dispatching `chat/toolCallConfirmed` (for\n{@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed`\n(for {@link ToolCallPendingResultConfirmationState}) to\n{@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and\n`toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolConfirmation" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallConfirmationState", + "description": "The tool call awaiting confirmation." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "toolCall" + ] + }, + "SessionToolClientExecutionRequest": { + "type": "object", + "description": "A running tool whose execution is delegated to an active client. Surfaced so\na client that provides the tool can pick up the work without subscribing to\nthe owning chat.\n\nThe {@link toolCall} is always a {@link ToolCallRunningState} (a\n{@link ToolCallState} in `running` status) whose\n{@link ToolCallRunningState.contributor | `contributor`} is a client\n{@link ToolCallClientContributor} whose `clientId` matches the denormalized\n{@link clientId} here. Execute and report the result by dispatching\n`chat/toolCallComplete` (and optionally streaming with\n`chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat |\n`chat`}, keyed by `turnId` and `toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolClientExecution" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "clientId": { + "type": "string", + "description": "The `clientId` expected to execute the tool. Matches the `clientId` of the\ntool call's client {@link ToolCallContributor}." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "The running tool call the session wants the owning client to execute. The\nhost only ever populates this with a {@link ToolCallRunningState} (i.e. a\n{@link ToolCallState} in `running` status)." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "clientId", + "toolCall" + ] + }, "ProjectInfo": { "type": "object", "description": "Server-owned project metadata for a session.", @@ -5764,6 +5885,40 @@ "clientId" ] }, + "SessionInputNeededSetAction": { + "type": "object", + "description": "A session-level input request was added or updated.\n\nUpsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the\nhost dispatches this with the full {@link SessionInputRequest} to append a new\nentry to {@link SessionState.inputNeeded} or replace the existing entry with\nthe same `id`.\n\nServer-originated: the host mirrors chat-level requests (elicitations, tool\nconfirmations, client-tool executions) into the session aggregate so clients\nsubscribed only to the session channel can discover them. Clients respond by\ndispatching the ordinary `chat/*` action to the entry's `chat` channel — see\n{@link SessionInputRequest}.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionInputNeededSet" + }, + "request": { + "$ref": "#/$defs/SessionInputRequest", + "description": "The input request to add or update, matched by `id`." + } + }, + "required": [ + "type", + "request" + ] + }, + "SessionInputNeededRemovedAction": { + "type": "object", + "description": "A session-level input request was removed.\n\nRemoves the entry identified by `id` from\n{@link SessionState.inputNeeded}; a no-op when no entry matches.\n\nServer-originated: the host dispatches this once the underlying request\nresolves (the user answers, the tool call is confirmed, or the client\nreports its result).", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionInputNeededRemoved" + }, + "id": { + "type": "string", + "description": "The `id` of the input request to remove." + } + }, + "required": [ + "type", + "id" + ] + }, "SessionCustomizationsChangedAction": { "type": "object", "description": "The session's customizations have changed.\n\nFull-replacement semantics: the `customizations` array replaces the\nprevious `customizations` entirely.", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index d7e341bf..0b99fcc9 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -761,6 +761,13 @@ }, "description": "Catalogue of changesets the server can produce for this session. Each\nentry advertises a subscribable view of file changes (uncommitted,\nsession-wide, per-turn, etc.) and the URI template the client expands\nbefore subscribing. See {@link Changeset} for the full shape and\n{@link /guide/changesets | Changesets} for an overview of the model." }, + "inputNeeded": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionInputRequest" + }, + "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -806,6 +813,120 @@ "tools" ] }, + "SessionInputRequestBase": { + "type": "object", + "description": "Fields common to every {@link SessionInputRequest} variant.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + } + }, + "required": [ + "id", + "chat" + ] + }, + "SessionChatInputRequest": { + "type": "object", + "description": "A user-input elicitation surfaced at the session level, mirroring one entry\nof the owning chat's {@link ChatState.inputRequests}.\n\nRespond by dispatching `chat/inputCompleted` (or syncing drafts with\n`chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`},\nkeyed by {@link ChatInputRequest.id | `request.id`}.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ChatInput" + }, + "request": { + "$ref": "#/$defs/ChatInputRequest", + "description": "The mirrored chat input request." + } + }, + "required": [ + "id", + "chat", + "kind", + "request" + ] + }, + "SessionToolConfirmationRequest": { + "type": "object", + "description": "A tool call blocked on confirmation — either parameter confirmation before\nexecution or result confirmation after — surfaced at the session level.\n\nRespond by dispatching `chat/toolCallConfirmed` (for\n{@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed`\n(for {@link ToolCallPendingResultConfirmationState}) to\n{@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and\n`toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolConfirmation" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallConfirmationState", + "description": "The tool call awaiting confirmation." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "toolCall" + ] + }, + "SessionToolClientExecutionRequest": { + "type": "object", + "description": "A running tool whose execution is delegated to an active client. Surfaced so\na client that provides the tool can pick up the work without subscribing to\nthe owning chat.\n\nThe {@link toolCall} is always a {@link ToolCallRunningState} (a\n{@link ToolCallState} in `running` status) whose\n{@link ToolCallRunningState.contributor | `contributor`} is a client\n{@link ToolCallClientContributor} whose `clientId` matches the denormalized\n{@link clientId} here. Execute and report the result by dispatching\n`chat/toolCallComplete` (and optionally streaming with\n`chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat |\n`chat`}, keyed by `turnId` and `toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolClientExecution" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "clientId": { + "type": "string", + "description": "The `clientId` expected to execute the tool. Matches the `clientId` of the\ntool call's client {@link ToolCallContributor}." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "The running tool call the session wants the owning client to execute. The\nhost only ever populates this with a {@link ToolCallRunningState} (i.e. a\n{@link ToolCallState} in `running` status)." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "clientId", + "toolCall" + ] + }, "ProjectInfo": { "type": "object", "description": "Server-owned project metadata for a session.", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 7f72b1c6..7d06acc3 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -895,6 +895,13 @@ }, "description": "Catalogue of changesets the server can produce for this session. Each\nentry advertises a subscribable view of file changes (uncommitted,\nsession-wide, per-turn, etc.) and the URI template the client expands\nbefore subscribing. See {@link Changeset} for the full shape and\n{@link /guide/changesets | Changesets} for an overview of the model." }, + "inputNeeded": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionInputRequest" + }, + "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -940,6 +947,120 @@ "tools" ] }, + "SessionInputRequestBase": { + "type": "object", + "description": "Fields common to every {@link SessionInputRequest} variant.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + } + }, + "required": [ + "id", + "chat" + ] + }, + "SessionChatInputRequest": { + "type": "object", + "description": "A user-input elicitation surfaced at the session level, mirroring one entry\nof the owning chat's {@link ChatState.inputRequests}.\n\nRespond by dispatching `chat/inputCompleted` (or syncing drafts with\n`chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`},\nkeyed by {@link ChatInputRequest.id | `request.id`}.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ChatInput" + }, + "request": { + "$ref": "#/$defs/ChatInputRequest", + "description": "The mirrored chat input request." + } + }, + "required": [ + "id", + "chat", + "kind", + "request" + ] + }, + "SessionToolConfirmationRequest": { + "type": "object", + "description": "A tool call blocked on confirmation — either parameter confirmation before\nexecution or result confirmation after — surfaced at the session level.\n\nRespond by dispatching `chat/toolCallConfirmed` (for\n{@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed`\n(for {@link ToolCallPendingResultConfirmationState}) to\n{@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and\n`toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolConfirmation" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallConfirmationState", + "description": "The tool call awaiting confirmation." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "toolCall" + ] + }, + "SessionToolClientExecutionRequest": { + "type": "object", + "description": "A running tool whose execution is delegated to an active client. Surfaced so\na client that provides the tool can pick up the work without subscribing to\nthe owning chat.\n\nThe {@link toolCall} is always a {@link ToolCallRunningState} (a\n{@link ToolCallState} in `running` status) whose\n{@link ToolCallRunningState.contributor | `contributor`} is a client\n{@link ToolCallClientContributor} whose `clientId` matches the denormalized\n{@link clientId} here. Execute and report the result by dispatching\n`chat/toolCallComplete` (and optionally streaming with\n`chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat |\n`chat`}, keyed by `turnId` and `toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolClientExecution" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "clientId": { + "type": "string", + "description": "The `clientId` expected to execute the tool. Matches the `clientId` of the\ntool call's client {@link ToolCallContributor}." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "The running tool call the session wants the owning client to execute. The\nhost only ever populates this with a {@link ToolCallRunningState} (i.e. a\n{@link ToolCallState} in `running` status)." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "clientId", + "toolCall" + ] + }, "ProjectInfo": { "type": "object", "description": "Server-owned project metadata for a session.", diff --git a/schema/state.schema.json b/schema/state.schema.json index a47850dc..cc17be8a 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -672,6 +672,13 @@ }, "description": "Catalogue of changesets the server can produce for this session. Each\nentry advertises a subscribable view of file changes (uncommitted,\nsession-wide, per-turn, etc.) and the URI template the client expands\nbefore subscribing. See {@link Changeset} for the full shape and\n{@link /guide/changesets | Changesets} for an overview of the model." }, + "inputNeeded": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionInputRequest" + }, + "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A present,\nnon-empty list implies {@link SessionStatus.InputNeeded} on\n{@link SessionSummary.status}.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -717,6 +724,120 @@ "tools" ] }, + "SessionInputRequestBase": { + "type": "object", + "description": "Fields common to every {@link SessionInputRequest} variant.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + } + }, + "required": [ + "id", + "chat" + ] + }, + "SessionChatInputRequest": { + "type": "object", + "description": "A user-input elicitation surfaced at the session level, mirroring one entry\nof the owning chat's {@link ChatState.inputRequests}.\n\nRespond by dispatching `chat/inputCompleted` (or syncing drafts with\n`chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`},\nkeyed by {@link ChatInputRequest.id | `request.id`}.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ChatInput" + }, + "request": { + "$ref": "#/$defs/ChatInputRequest", + "description": "The mirrored chat input request." + } + }, + "required": [ + "id", + "chat", + "kind", + "request" + ] + }, + "SessionToolConfirmationRequest": { + "type": "object", + "description": "A tool call blocked on confirmation — either parameter confirmation before\nexecution or result confirmation after — surfaced at the session level.\n\nRespond by dispatching `chat/toolCallConfirmed` (for\n{@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed`\n(for {@link ToolCallPendingResultConfirmationState}) to\n{@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and\n`toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolConfirmation" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallConfirmationState", + "description": "The tool call awaiting confirmation." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "toolCall" + ] + }, + "SessionToolClientExecutionRequest": { + "type": "object", + "description": "A running tool whose execution is delegated to an active client. Surfaced so\na client that provides the tool can pick up the work without subscribing to\nthe owning chat.\n\nThe {@link toolCall} is always a {@link ToolCallRunningState} (a\n{@link ToolCallState} in `running` status) whose\n{@link ToolCallRunningState.contributor | `contributor`} is a client\n{@link ToolCallClientContributor} whose `clientId` matches the denormalized\n{@link clientId} here. Execute and report the result by dispatching\n`chat/toolCallComplete` (and optionally streaming with\n`chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat |\n`chat`}, keyed by `turnId` and `toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "$ref": "#/$defs/SessionInputRequestKind.ToolClientExecution" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "clientId": { + "type": "string", + "description": "The `clientId` expected to execute the tool. Matches the `clientId` of the\ntool call's client {@link ToolCallContributor}." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "The running tool call the session wants the owning client to execute. The\nhost only ever populates this with a {@link ToolCallRunningState} (i.e. a\n{@link ToolCallState} in `running` status)." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "clientId", + "toolCall" + ] + }, "ProjectInfo": { "type": "object", "description": "Server-owned project metadata for a session.", @@ -4120,6 +4241,21 @@ ], "description": "A primitive JSON value: a string, number, boolean, or `null`." }, + "SessionInputRequest": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/SessionChatInputRequest" + }, + { + "$ref": "#/$defs/SessionToolConfirmationRequest" + }, + { + "$ref": "#/$defs/SessionToolClientExecutionRequest" + } + ], + "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat**. The host removes the entry with `session/inputNeededRemoved`\nonce the underlying request resolves." + }, "ChildCustomizationType": { "oneOf": [ {}, @@ -4399,6 +4535,18 @@ ], "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." }, + "ToolCallConfirmationState": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/ToolCallPendingConfirmationState" + }, + { + "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + } + ], + "description": "The two tool-call states that block on a client confirmation: parameter\nconfirmation before execution ({@link ToolCallPendingConfirmationState}) and\nresult confirmation after execution\n({@link ToolCallPendingResultConfirmationState}).\n\nSurfaced at the session level by {@link SessionToolConfirmationRequest}." + }, "ToolResultContent": { "oneOf": [ {}, diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 014312ab..ecf681ee 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -642,7 +642,7 @@ function generateDiscriminatedUnion(cfg: UnionConfig): string { const STATE_ENUMS = [ 'PolicyState', 'SessionLifecycle', 'SessionStatus', 'ChatOriginKind', 'ChatInteractivity', 'PendingMessageKind', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', - 'ChatInputResponseKind', + 'ChatInputResponseKind', 'SessionInputRequestKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', @@ -664,6 +664,9 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'ConfigSchema' }, { name: 'SessionState' }, { name: 'SessionActiveClient' }, + { name: 'SessionChatInputRequest' }, + { name: 'SessionToolConfirmationRequest' }, + { name: 'SessionToolClientExecutionRequest' }, { name: 'SessionSummary' }, { name: 'ChangesSummary' }, { name: 'ChatState' }, @@ -794,6 +797,17 @@ const TOOL_CALL_STATE_UNION: UnionConfig = { unknown: true, }; +const TOOL_CALL_CONFIRMATION_STATE_UNION: UnionConfig = { + name: 'ToolCallConfirmationState', + discriminantField: 'status', + doc: 'ToolCallConfirmationState is a tool call blocked on parameter- or result-confirmation.', + variants: [ + { variantName: 'PendingConfirmation', innerType: 'ToolCallPendingConfirmationState', wireValue: 'pending-confirmation' }, + { variantName: 'PendingResultConfirmation', innerType: 'ToolCallPendingResultConfirmationState', wireValue: 'pending-result-confirmation' }, + ], + unknown: true, +}; + const TERMINAL_CLAIM_UNION: UnionConfig = { name: 'TerminalClaim', discriminantField: 'kind', @@ -950,6 +964,18 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { unknown: true, }; +const SESSION_INPUT_REQUEST_UNION: UnionConfig = { + name: 'SessionInputRequest', + discriminantField: 'kind', + doc: 'SessionInputRequest is one outstanding piece of input a session is blocked on, aggregated across all chats.', + variants: [ + { variantName: 'ChatInput', innerType: 'SessionChatInputRequest', wireValue: 'chatInput' }, + { variantName: 'ToolConfirmation', innerType: 'SessionToolConfirmationRequest', wireValue: 'toolConfirmation' }, + { variantName: 'ToolClientExecution', innerType: 'SessionToolClientExecutionRequest', wireValue: 'toolClientExecution' }, + ], + unknown: true, +}; + function generateChatOriginGo(): string { return `// ChatOrigin describes how a chat came into existence. type ChatOrigin struct { @@ -1168,6 +1194,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_STATE_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_CONFIRMATION_STATE_UNION)); + lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CLAIM_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CONTENT_PART_UNION)); @@ -1192,6 +1220,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); + lines.push(''); lines.push(generateChatOriginGo()); lines.push(''); lines.push(generateSnapshotState()); @@ -1248,6 +1278,8 @@ const ACTION_VARIANTS: { { type: 'session/serverToolsChanged', variantName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientSet', variantName: 'SessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', variantName: 'SessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, + { type: 'session/inputNeededSet', variantName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, + { type: 'session/inputNeededRemoved', variantName: 'SessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' }, { type: 'session/customizationsChanged', variantName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', variantName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, @@ -1884,6 +1916,8 @@ function checkExhaustiveness(project: Project): void { 'CustomizationLoadState', 'McpServerState', 'ToolCallContributor', + 'SessionInputRequest', + 'ToolCallConfirmationState', 'ReconnectResult', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 8201a3ed..cf0a4739 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -783,7 +783,7 @@ internal object ToolResultContentSerializer : KSerializer { const STATE_ENUMS = [ 'PolicyState', 'PendingMessageKind', 'SessionLifecycle', 'SessionStatus', 'ChatOriginKind', 'ChatInteractivity', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', - 'ChatInputResponseKind', + 'ChatInputResponseKind', 'SessionInputRequestKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', @@ -796,6 +796,7 @@ const STATE_STRUCTS = [ 'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SessionState', 'SessionActiveClient', + 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', 'SessionSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', 'MessageOrigin', 'ChatInputOption', @@ -866,6 +867,16 @@ const TOOL_CALL_STATE_UNION: UnionConfig = { unknown: true, }; +const TOOL_CALL_CONFIRMATION_STATE_UNION: UnionConfig = { + name: 'ToolCallConfirmationState', + discriminantField: 'status', + variants: [ + { caseName: 'PendingConfirmation', structName: 'ToolCallPendingConfirmationState', discriminantValue: 'pending-confirmation' }, + { caseName: 'PendingResultConfirmation', structName: 'ToolCallPendingResultConfirmationState', discriminantValue: 'pending-result-confirmation' }, + ], + unknown: true, +}; + const TERMINAL_CLAIM_UNION: UnionConfig = { name: 'TerminalClaim', discriminantField: 'kind', @@ -1054,6 +1065,17 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { unknown: true, }; +const SESSION_INPUT_REQUEST_UNION: UnionConfig = { + name: 'SessionInputRequest', + discriminantField: 'kind', + variants: [ + { caseName: 'ChatInput', structName: 'SessionChatInputRequest', discriminantValue: 'chatInput' }, + { caseName: 'ToolConfirmation', structName: 'SessionToolConfirmationRequest', discriminantValue: 'toolConfirmation' }, + { caseName: 'ToolClientExecution', structName: 'SessionToolClientExecutionRequest', discriminantValue: 'toolClientExecution' }, + ], + unknown: true, +}; + function generateStateFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; @@ -1097,6 +1119,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_STATE_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_CONFIRMATION_STATE_UNION)); + lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CLAIM_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CONTENT_PART_UNION)); @@ -1119,6 +1143,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); + lines.push(''); lines.push(generateToolResultContentUnion()); lines.push(''); lines.push(generateSnapshotState()); @@ -1163,6 +1189,8 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/serverToolsChanged', caseName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientSet', caseName: 'SessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', caseName: 'SessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, + { type: 'session/inputNeededSet', caseName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, + { type: 'session/inputNeededRemoved', caseName: 'SessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' }, { type: 'chat/pendingMessageSet', caseName: 'ChatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, { type: 'chat/pendingMessageRemoved', caseName: 'ChatPendingMessageRemoved', tsInterface: 'ChatPendingMessageRemovedAction' }, { type: 'chat/queuedMessagesReordered', caseName: 'ChatQueuedMessagesReordered', tsInterface: 'ChatQueuedMessagesReorderedAction' }, @@ -1872,6 +1900,8 @@ function checkExhaustiveness(project: Project): void { 'ChildCustomization', // CHILD_CUSTOMIZATION_UNION discriminated union 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union + 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union + 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ChildCustomizationType', // TS subset alias of CustomizationType; consumers reuse CustomizationType 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union 'AuthRequiredErrorData', // emitted by generateErrorsFile() diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 55747102..a5c063d6 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -652,7 +652,7 @@ function generateStructFromInterface( const STATE_ENUMS = [ 'PolicyState', 'PendingMessageKind', 'SessionLifecycle', 'SessionStatus', 'ChatOriginKind', 'ChatInteractivity', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', - 'ChatInputResponseKind', + 'ChatInputResponseKind', 'SessionInputRequestKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', @@ -698,6 +698,9 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'ChatSummary' }, { name: 'SessionState' }, { name: 'SessionActiveClient' }, + { name: 'SessionChatInputRequest', omitDiscriminants: true }, + { name: 'SessionToolConfirmationRequest', omitDiscriminants: true }, + { name: 'SessionToolClientExecutionRequest', omitDiscriminants: true }, { name: 'SessionSummary' }, { name: 'ChangesSummary' }, { name: 'ProjectInfo' }, @@ -825,6 +828,17 @@ const TOOL_CALL_STATE_UNION: UnionConfig = { unknown: true, }; +const TOOL_CALL_CONFIRMATION_STATE_UNION: UnionConfig = { + name: 'ToolCallConfirmationState', + discriminantField: 'status', + doc: 'A tool call blocked on parameter- or result-confirmation.', + variants: [ + { variantName: 'PendingConfirmation', innerType: 'ToolCallPendingConfirmationState', wireValue: 'pending-confirmation' }, + { variantName: 'PendingResultConfirmation', innerType: 'ToolCallPendingResultConfirmationState', wireValue: 'pending-result-confirmation' }, + ], + unknown: true, +}; + const TERMINAL_CLAIM_UNION: UnionConfig = { name: 'TerminalClaim', discriminantField: 'kind', @@ -986,6 +1000,18 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { unknown: true, }; +const SESSION_INPUT_REQUEST_UNION: UnionConfig = { + name: 'SessionInputRequest', + discriminantField: 'kind', + doc: 'One outstanding piece of input a session is blocked on, aggregated across all chats.', + variants: [ + { variantName: 'ChatInput', innerType: 'SessionChatInputRequest', wireValue: 'chatInput' }, + { variantName: 'ToolConfirmation', innerType: 'SessionToolConfirmationRequest', wireValue: 'toolConfirmation' }, + { variantName: 'ToolClientExecution', innerType: 'SessionToolClientExecutionRequest', wireValue: 'toolClientExecution' }, + ], + unknown: true, +}; + function generateChatOrigin(): string { return `/// How a chat came into existence. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -1076,6 +1102,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_STATE_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_CONFIRMATION_STATE_UNION)); + lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CLAIM_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CONTENT_PART_UNION)); @@ -1100,6 +1128,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); + lines.push(''); lines.push(generateSnapshotState()); lines.push(''); @@ -1150,6 +1180,8 @@ const ACTION_VARIANTS: { { type: 'session/serverToolsChanged', variantName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientSet', variantName: 'SessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', variantName: 'SessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, + { type: 'session/inputNeededSet', variantName: 'SessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, + { type: 'session/inputNeededRemoved', variantName: 'SessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' }, { type: 'chat/pendingMessageSet', variantName: 'ChatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, { type: 'chat/pendingMessageRemoved', variantName: 'ChatPendingMessageRemoved', tsInterface: 'ChatPendingMessageRemovedAction' }, { type: 'chat/queuedMessagesReordered', variantName: 'ChatQueuedMessagesReordered', tsInterface: 'ChatQueuedMessagesReorderedAction' }, @@ -1226,7 +1258,7 @@ pub struct ${scope}ToolCallConfirmedAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; - lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, Customization, ErrorInfo, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); + lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, Customization, ErrorInfo, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); lines.push(''); // ActionType enum @@ -1752,6 +1784,8 @@ function checkExhaustiveness(project: Project): void { 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union + 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union + 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ReconnectResult', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 90aac11a..77723813 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -539,7 +539,7 @@ function generatePartialStructFromInterface( const STATE_ENUMS = [ 'PolicyState', 'PendingMessageKind', 'SessionLifecycle', 'SessionStatus', 'ChatOriginKind', 'ChatInteractivity', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', - 'ChatInputResponseKind', + 'ChatInputResponseKind', 'SessionInputRequestKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', @@ -552,6 +552,7 @@ const STATE_STRUCTS = [ 'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SessionState', 'SessionActiveClient', + 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', 'SessionSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', 'MessageOrigin', 'ChatInputOption', @@ -628,6 +629,17 @@ const TOOL_CALL_STATE_UNION: UnionConfig = { ], }; +const TOOL_CALL_CONFIRMATION_STATE_UNION: UnionConfig = { + name: 'ToolCallConfirmationState', + discriminantField: 'status', + // Open union: mirrors TOOL_CALL_STATE_UNION's forward-compat policy. + allowUnknown: true, + variants: [ + { caseName: 'pendingConfirmation', structName: 'ToolCallPendingConfirmationState', discriminantValue: 'pending-confirmation' }, + { caseName: 'pendingResultConfirmation', structName: 'ToolCallPendingResultConfirmationState', discriminantValue: 'pending-result-confirmation' }, + ], +}; + const TERMINAL_CLAIM_UNION: UnionConfig = { name: 'TerminalClaim', discriminantField: 'kind', @@ -769,6 +781,18 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { ], }; +const SESSION_INPUT_REQUEST_UNION: UnionConfig = { + name: 'SessionInputRequest', + discriminantField: 'kind', + // Open union: future protocol versions may add new session input request kinds. + allowUnknown: true, + variants: [ + { caseName: 'chatInput', structName: 'SessionChatInputRequest', discriminantValue: 'chatInput' }, + { caseName: 'toolConfirmation', structName: 'SessionToolConfirmationRequest', discriminantValue: 'toolConfirmation' }, + { caseName: 'toolClientExecution', structName: 'SessionToolClientExecutionRequest', discriminantValue: 'toolClientExecution' }, + ], +}; + function generateToolResultContentUnion(): string { return `public enum ToolResultContent: Codable, Sendable { case text(ToolResultTextContent) @@ -1000,6 +1024,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_STATE_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_CONFIRMATION_STATE_UNION)); + lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CLAIM_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CONTENT_PART_UNION)); @@ -1022,6 +1048,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); + lines.push(''); lines.push(generateToolResultContentUnion()); lines.push(''); lines.push(generateSnapshotState()); @@ -1067,6 +1095,8 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/serverToolsChanged', caseName: 'sessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientSet', caseName: 'sessionActiveClientSet', tsInterface: 'SessionActiveClientSetAction' }, { type: 'session/activeClientRemoved', caseName: 'sessionActiveClientRemoved', tsInterface: 'SessionActiveClientRemovedAction' }, + { type: 'session/inputNeededSet', caseName: 'sessionInputNeededSet', tsInterface: 'SessionInputNeededSetAction' }, + { type: 'session/inputNeededRemoved', caseName: 'sessionInputNeededRemoved', tsInterface: 'SessionInputNeededRemovedAction' }, { type: 'chat/pendingMessageSet', caseName: 'chatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, { type: 'chat/pendingMessageRemoved', caseName: 'chatPendingMessageRemoved', tsInterface: 'ChatPendingMessageRemovedAction' }, { type: 'chat/queuedMessagesReordered', caseName: 'chatQueuedMessagesReordered', tsInterface: 'ChatQueuedMessagesReorderedAction' }, @@ -1890,6 +1920,8 @@ function checkExhaustiveness(project: Project): void { 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union + 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union + 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'AuthRequiredErrorData', // emitted by generateErrorsFile() 'PermissionDeniedErrorData', // emitted by generateErrorsFile() 'UnsupportedProtocolVersionErrorData', // emitted by generateErrorsFile() diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index 02a7c588..ce21c466 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -19,6 +19,8 @@ import type { SessionServerToolsChangedAction, SessionActiveClientSetAction, SessionActiveClientRemovedAction, + SessionInputNeededSetAction, + SessionInputNeededRemovedAction, SessionCustomizationsChangedAction, SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, @@ -116,6 +118,8 @@ export type SessionAction = | SessionServerToolsChangedAction | SessionActiveClientSetAction | SessionActiveClientRemovedAction + | SessionInputNeededSetAction + | SessionInputNeededRemovedAction | SessionCustomizationsChangedAction | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction @@ -151,6 +155,8 @@ export type ServerSessionAction = | SessionChatUpdatedAction | SessionDefaultChatChangedAction | SessionServerToolsChangedAction + | SessionInputNeededSetAction + | SessionInputNeededRemovedAction | SessionCustomizationsChangedAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction @@ -338,6 +344,8 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.SessionServerToolsChanged]: false, [ActionType.SessionActiveClientSet]: true, [ActionType.SessionActiveClientRemoved]: true, + [ActionType.SessionInputNeededSet]: false, + [ActionType.SessionInputNeededRemoved]: false, [ActionType.SessionCustomizationsChanged]: false, [ActionType.SessionCustomizationToggled]: true, [ActionType.SessionCustomizationUpdated]: false, diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 09961471..1772c72e 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -1110,6 +1110,20 @@ export type ToolCallState = | ToolCallCompletedState | ToolCallCancelledState; +/** + * The two tool-call states that block on a client confirmation: parameter + * confirmation before execution ({@link ToolCallPendingConfirmationState}) and + * result confirmation after execution + * ({@link ToolCallPendingResultConfirmationState}). + * + * Surfaced at the session level by {@link SessionToolConfirmationRequest}. + * + * @category Tool Call Types + */ +export type ToolCallConfirmationState = + | ToolCallPendingConfirmationState + | ToolCallPendingResultConfirmationState; + // ─── Tool Result Content ───────────────────────────────────────────────────── diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 1ecf0d1a..63e632dd 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -9,6 +9,7 @@ import type { ErrorInfo } from '../common/state.js'; import type { ToolDefinition, SessionActiveClient, + SessionInputRequest, Customization, McpServerState, AgentSelection, @@ -292,6 +293,50 @@ export interface SessionActiveClientRemovedAction { clientId: string; } +// ─── Input Needed Actions ──────────────────────────────────────────────────── + +/** + * A session-level input request was added or updated. + * + * Upsert semantics keyed by {@link SessionInputRequest.id | `request.id`}: the + * host dispatches this with the full {@link SessionInputRequest} to append a new + * entry to {@link SessionState.inputNeeded} or replace the existing entry with + * the same `id`. + * + * Server-originated: the host mirrors chat-level requests (elicitations, tool + * confirmations, client-tool executions) into the session aggregate so clients + * subscribed only to the session channel can discover them. Clients respond by + * dispatching the ordinary `chat/*` action to the entry's `chat` channel — see + * {@link SessionInputRequest}. + * + * @category Session Actions + * @version 1 + */ +export interface SessionInputNeededSetAction { + type: ActionType.SessionInputNeededSet; + /** The input request to add or update, matched by `id`. */ + request: SessionInputRequest; +} + +/** + * A session-level input request was removed. + * + * Removes the entry identified by `id` from + * {@link SessionState.inputNeeded}; a no-op when no entry matches. + * + * Server-originated: the host dispatches this once the underlying request + * resolves (the user answers, the tool call is confirmed, or the client + * reports its result). + * + * @category Session Actions + * @version 1 + */ +export interface SessionInputNeededRemovedAction { + type: ActionType.SessionInputNeededRemoved; + /** The `id` of the input request to remove. */ + id: string; +} + // ─── Customization Actions ─────────────────────────────────────────────────── /** diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index e860c725..6813b6fe 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -181,6 +181,33 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: return { ...state, activeClients: updated }; } + // ── Input Needed ──────────────────────────────────────────────────── + + case ActionType.SessionInputNeededSet: { + const list = state.inputNeeded ?? []; + const idx = list.findIndex(r => r.id === action.request.id); + if (idx < 0) { + return { ...state, inputNeeded: [...list, action.request] }; + } + const updated = list.slice(); + updated[idx] = action.request; + return { ...state, inputNeeded: updated }; + } + + case ActionType.SessionInputNeededRemoved: { + const list = state.inputNeeded; + if (!list) { + return state; + } + const idx = list.findIndex(r => r.id === action.id); + if (idx < 0) { + return state; + } + const updated = list.slice(); + updated.splice(idx, 1); + return { ...state, inputNeeded: updated }; + } + // ── Customizations ────────────────────────────────────────────────── case ActionType.SessionCustomizationsChanged: diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 76d25bd2..a4fe1642 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -6,7 +6,12 @@ import type { Changeset } from '../channels-changeset/state.js'; import type { AnnotationsSummary } from '../channels-annotations/state.js'; -import type { ChatSummary } from '../channels-chat/state.js'; +import type { + ChatSummary, + ChatInputRequest, + ToolCallConfirmationState, + ToolCallState, +} from '../channels-chat/state.js'; import type { ModelSelection } from '../channels-root/state.js'; import type { ConfigPropertySchema, @@ -121,6 +126,23 @@ export interface SessionState { * {@link /guide/changesets | Changesets} for an overview of the model. */ changesets?: Changeset[]; + /** + * Outstanding input the session is blocked on, aggregated across every chat + * so a client can discover and answer it from the session channel alone, + * without subscribing to individual chats. + * + * Each entry is self-sufficient: it carries the owning chat's URI plus every + * identifier the client needs to respond. A client answers by dispatching the + * ordinary `chat/*` action to that chat's channel — see + * {@link SessionInputRequest} for the per-variant response path. A present, + * non-empty list implies {@link SessionStatus.InputNeeded} on + * {@link SessionSummary.status}. + * + * Host-managed: the host upserts entries with `session/inputNeededSet` as + * chats raise requests and removes them with `session/inputNeededRemoved` + * once the underlying request resolves. + */ + inputNeeded?: SessionInputRequest[]; /** * Additional provider-specific metadata for this session. * @@ -158,6 +180,136 @@ export interface SessionActiveClient { customizations?: ClientPluginCustomization[]; } +// ─── Session Input Requests ────────────────────────────────────────────────── + +/** + * Discriminant for the kinds of outstanding input a session can surface in + * {@link SessionState.inputNeeded}. + * + * This is a general/typological union (not a lifecycle), so the discriminant is + * a `*Kind`. + * + * @category Session Input Types + */ +export const enum SessionInputRequestKind { + /** A user-facing elicitation mirrored from a chat's `inputRequests`. */ + ChatInput = 'chatInput', + /** A tool call awaiting parameter- or result-confirmation. */ + ToolConfirmation = 'toolConfirmation', + /** A running tool the session wants an active client to execute. */ + ToolClientExecution = 'toolClientExecution', +} + +/** + * Fields common to every {@link SessionInputRequest} variant. + * + * @category Session Input Types + */ +interface SessionInputRequestBase { + /** + * Stable key for this entry, unique within the session's + * {@link SessionState.inputNeeded} list. The host derives it however it likes + * (for example from the chat URI plus the underlying request or tool-call + * id); consumers MUST treat it as opaque. It is the key for the + * `session/inputNeededSet` / `session/inputNeededRemoved` upsert convention. + */ + id: string; + /** + * The chat the underlying request lives in. This is the channel a client + * dispatches its response to — it does not need to have subscribed to that + * chat first. + */ + chat: URI; +} + +/** + * A user-input elicitation surfaced at the session level, mirroring one entry + * of the owning chat's {@link ChatState.inputRequests}. + * + * Respond by dispatching `chat/inputCompleted` (or syncing drafts with + * `chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`}, + * keyed by {@link ChatInputRequest.id | `request.id`}. + * + * @category Session Input Types + */ +export interface SessionChatInputRequest extends SessionInputRequestBase { + kind: SessionInputRequestKind.ChatInput; + /** The mirrored chat input request. */ + request: ChatInputRequest; +} + +/** + * A tool call blocked on confirmation — either parameter confirmation before + * execution or result confirmation after — surfaced at the session level. + * + * Respond by dispatching `chat/toolCallConfirmed` (for + * {@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed` + * (for {@link ToolCallPendingResultConfirmationState}) to + * {@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and + * `toolCall.toolCallId`. + * + * @category Session Input Types + */ +export interface SessionToolConfirmationRequest extends SessionInputRequestBase { + kind: SessionInputRequestKind.ToolConfirmation; + /** The turn the tool call belongs to. */ + turnId: string; + /** The tool call awaiting confirmation. */ + toolCall: ToolCallConfirmationState; +} + +/** + * A running tool whose execution is delegated to an active client. Surfaced so + * a client that provides the tool can pick up the work without subscribing to + * the owning chat. + * + * The {@link toolCall} is always a {@link ToolCallRunningState} (a + * {@link ToolCallState} in `running` status) whose + * {@link ToolCallRunningState.contributor | `contributor`} is a client + * {@link ToolCallClientContributor} whose `clientId` matches the denormalized + * {@link clientId} here. Execute and report the result by dispatching + * `chat/toolCallComplete` (and optionally streaming with + * `chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat | + * `chat`}, keyed by `turnId` and `toolCall.toolCallId`. + * + * @category Session Input Types + */ +export interface SessionToolClientExecutionRequest extends SessionInputRequestBase { + kind: SessionInputRequestKind.ToolClientExecution; + /** The turn the tool call belongs to. */ + turnId: string; + /** + * The `clientId` expected to execute the tool. Matches the `clientId` of the + * tool call's client {@link ToolCallContributor}. + */ + clientId: string; + /** + * The running tool call the session wants the owning client to execute. The + * host only ever populates this with a {@link ToolCallRunningState} (i.e. a + * {@link ToolCallState} in `running` status). + */ + toolCall: ToolCallState; +} + +/** + * One outstanding piece of input a session is blocked on, aggregated across all + * chats in {@link SessionState.inputNeeded}. + * + * Each entry is self-sufficient: it carries the owning + * {@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed + * to construct the response, so a client can answer by dispatching the ordinary + * `chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`, + * `chat/toolCallComplete`, …) to that chat's channel **without having subscribed + * to the chat**. The host removes the entry with `session/inputNeededRemoved` + * once the underlying request resolves. + * + * @category Session Input Types + */ +export type SessionInputRequest = + | SessionChatInputRequest + | SessionToolConfirmationRequest + | SessionToolClientExecutionRequest; + /** * Server-owned project metadata for a session. * diff --git a/types/common/actions.ts b/types/common/actions.ts index 8687d706..f219898e 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -28,6 +28,8 @@ import type { SessionServerToolsChangedAction, SessionActiveClientSetAction, SessionActiveClientRemovedAction, + SessionInputNeededSetAction, + SessionInputNeededRemovedAction, SessionCustomizationsChangedAction, SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, @@ -139,6 +141,8 @@ export const enum ActionType { SessionServerToolsChanged = 'session/serverToolsChanged', SessionActiveClientSet = 'session/activeClientSet', SessionActiveClientRemoved = 'session/activeClientRemoved', + SessionInputNeededSet = 'session/inputNeededSet', + SessionInputNeededRemoved = 'session/inputNeededRemoved', ChatPendingMessageSet = 'chat/pendingMessageSet', ChatPendingMessageRemoved = 'chat/pendingMessageRemoved', ChatQueuedMessagesReordered = 'chat/queuedMessagesReordered', @@ -235,6 +239,8 @@ export type StateAction = | SessionServerToolsChangedAction | SessionActiveClientSetAction | SessionActiveClientRemovedAction + | SessionInputNeededSetAction + | SessionInputNeededRemovedAction | SessionCustomizationsChangedAction | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction diff --git a/types/index.ts b/types/index.ts index 88b2db44..6366784a 100644 --- a/types/index.ts +++ b/types/index.ts @@ -53,6 +53,7 @@ export type { ToolCallCompletedState, ToolCallCancelledState, ToolCallState, + ToolCallConfirmationState, ConfirmationOption, ToolDefinition, ToolAnnotations, @@ -65,6 +66,10 @@ export type { ToolResultTerminalContent, ToolResultSubagentContent, SessionActiveClient, + SessionInputRequest, + SessionChatInputRequest, + SessionToolConfirmationRequest, + SessionToolClientExecutionRequest, PendingMessage, ChatInputAnswer, ChatInputAnswerValue, @@ -111,6 +116,7 @@ export { PolicyState, SessionLifecycle, SessionStatus, + SessionInputRequestKind, ChatOriginKind, TurnState, MessageKind, @@ -168,6 +174,8 @@ export type { SessionServerToolsChangedAction, SessionActiveClientSetAction, SessionActiveClientRemovedAction, + SessionInputNeededSetAction, + SessionInputNeededRemovedAction, ChatPendingMessageSetAction, ChatPendingMessageRemovedAction, ChatQueuedMessagesReorderedAction, diff --git a/types/test-cases/reducers/223-session-inputneededset-adds-chat-input.json b/types/test-cases/reducers/223-session-inputneededset-adds-chat-input.json new file mode 100644 index 00000000..858d26ca --- /dev/null +++ b/types/test-cases/reducers/223-session-inputneededset-adds-chat-input.json @@ -0,0 +1,77 @@ +{ + "description": "session/inputNeededSet appends a chat-input request when inputNeeded is absent", + "reducer": "session", + "initial": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/inputNeededSet", + "request": { + "kind": "chatInput", + "id": "copilot:/test-session/chat-1#req-1", + "chat": "copilot:/test-session/chat-1", + "request": { + "id": "req-1", + "message": "Which environment?", + "questions": [ + { + "kind": "single-select", + "id": "env", + "message": "Pick an environment", + "options": [ + { "id": "dev", "label": "Dev" }, + { "id": "prod", "label": "Prod" } + ] + } + ] + } + } + } + ], + "expected": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "inputNeeded": [ + { + "kind": "chatInput", + "id": "copilot:/test-session/chat-1#req-1", + "chat": "copilot:/test-session/chat-1", + "request": { + "id": "req-1", + "message": "Which environment?", + "questions": [ + { + "kind": "single-select", + "id": "env", + "message": "Pick an environment", + "options": [ + { "id": "dev", "label": "Dev" }, + { "id": "prod", "label": "Prod" } + ] + } + ] + } + } + ] + } +} diff --git a/types/test-cases/reducers/224-session-inputneededset-appends-tool-confirmation.json b/types/test-cases/reducers/224-session-inputneededset-appends-tool-confirmation.json new file mode 100644 index 00000000..cdfa39a2 --- /dev/null +++ b/types/test-cases/reducers/224-session-inputneededset-appends-tool-confirmation.json @@ -0,0 +1,79 @@ +{ + "description": "session/inputNeededSet appends a tool-confirmation request to an existing inputNeeded list", + "reducer": "session", + "initial": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "inputNeeded": [ + { + "kind": "chatInput", + "id": "copilot:/test-session/chat-1#req-1", + "chat": "copilot:/test-session/chat-1", + "request": { "id": "req-1", "message": "Which environment?" } + } + ] + }, + "actions": [ + { + "type": "session/inputNeededSet", + "request": { + "kind": "toolConfirmation", + "id": "copilot:/test-session/chat-1#call-9", + "chat": "copilot:/test-session/chat-1", + "turnId": "turn-1", + "toolCall": { + "status": "pending-confirmation", + "toolCallId": "call-9", + "toolName": "write_file", + "displayName": "Write File", + "invocationMessage": "Write config.json", + "confirmationTitle": "Write file" + } + } + } + ], + "expected": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "inputNeeded": [ + { + "kind": "chatInput", + "id": "copilot:/test-session/chat-1#req-1", + "chat": "copilot:/test-session/chat-1", + "request": { "id": "req-1", "message": "Which environment?" } + }, + { + "kind": "toolConfirmation", + "id": "copilot:/test-session/chat-1#call-9", + "chat": "copilot:/test-session/chat-1", + "turnId": "turn-1", + "toolCall": { + "status": "pending-confirmation", + "toolCallId": "call-9", + "toolName": "write_file", + "displayName": "Write File", + "invocationMessage": "Write config.json", + "confirmationTitle": "Write file" + } + } + ] + } +} diff --git a/types/test-cases/reducers/225-session-inputneededset-replaces-existing.json b/types/test-cases/reducers/225-session-inputneededset-replaces-existing.json new file mode 100644 index 00000000..0d3cf8ba --- /dev/null +++ b/types/test-cases/reducers/225-session-inputneededset-replaces-existing.json @@ -0,0 +1,84 @@ +{ + "description": "session/inputNeededSet replaces an existing entry with the same id", + "reducer": "session", + "initial": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "inputNeeded": [ + { + "kind": "toolConfirmation", + "id": "copilot:/test-session/chat-1#call-9", + "chat": "copilot:/test-session/chat-1", + "turnId": "turn-1", + "toolCall": { + "status": "pending-confirmation", + "toolCallId": "call-9", + "toolName": "run_tool", + "displayName": "Run Tool", + "invocationMessage": "Run the client tool" + } + } + ] + }, + "actions": [ + { + "type": "session/inputNeededSet", + "request": { + "kind": "toolClientExecution", + "id": "copilot:/test-session/chat-1#call-9", + "chat": "copilot:/test-session/chat-1", + "turnId": "turn-1", + "clientId": "vscode-1", + "toolCall": { + "status": "running", + "toolCallId": "call-9", + "toolName": "run_tool", + "displayName": "Run Tool", + "invocationMessage": "Run the client tool", + "confirmed": "not-needed", + "contributor": { "kind": "client", "clientId": "vscode-1" } + } + } + } + ], + "expected": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "inputNeeded": [ + { + "kind": "toolClientExecution", + "id": "copilot:/test-session/chat-1#call-9", + "chat": "copilot:/test-session/chat-1", + "turnId": "turn-1", + "clientId": "vscode-1", + "toolCall": { + "status": "running", + "toolCallId": "call-9", + "toolName": "run_tool", + "displayName": "Run Tool", + "invocationMessage": "Run the client tool", + "confirmed": "not-needed", + "contributor": { "kind": "client", "clientId": "vscode-1" } + } + } + ] + } +} diff --git a/types/test-cases/reducers/226-session-inputneededremoved-removes-entry.json b/types/test-cases/reducers/226-session-inputneededremoved-removes-entry.json new file mode 100644 index 00000000..68cc96c9 --- /dev/null +++ b/types/test-cases/reducers/226-session-inputneededremoved-removes-entry.json @@ -0,0 +1,58 @@ +{ + "description": "session/inputNeededRemoved removes the entry with the matching id", + "reducer": "session", + "initial": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "inputNeeded": [ + { + "kind": "chatInput", + "id": "copilot:/test-session/chat-1#req-1", + "chat": "copilot:/test-session/chat-1", + "request": { "id": "req-1", "message": "Which environment?" } + }, + { + "kind": "chatInput", + "id": "copilot:/test-session/chat-1#req-2", + "chat": "copilot:/test-session/chat-1", + "request": { "id": "req-2", "message": "Confirm name?" } + } + ] + }, + "actions": [ + { + "type": "session/inputNeededRemoved", + "id": "copilot:/test-session/chat-1#req-1" + } + ], + "expected": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "inputNeeded": [ + { + "kind": "chatInput", + "id": "copilot:/test-session/chat-1#req-2", + "chat": "copilot:/test-session/chat-1", + "request": { "id": "req-2", "message": "Confirm name?" } + } + ] + } +} diff --git a/types/test-cases/reducers/227-session-inputneededremoved-no-op-unknown-id.json b/types/test-cases/reducers/227-session-inputneededremoved-no-op-unknown-id.json new file mode 100644 index 00000000..99981fa8 --- /dev/null +++ b/types/test-cases/reducers/227-session-inputneededremoved-no-op-unknown-id.json @@ -0,0 +1,52 @@ +{ + "description": "session/inputNeededRemoved is a no-op for an unknown id", + "reducer": "session", + "initial": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "inputNeeded": [ + { + "kind": "chatInput", + "id": "copilot:/test-session/chat-1#req-1", + "chat": "copilot:/test-session/chat-1", + "request": { "id": "req-1", "message": "Which environment?" } + } + ] + }, + "actions": [ + { + "type": "session/inputNeededRemoved", + "id": "copilot:/test-session/chat-1#unknown" + } + ], + "expected": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "inputNeeded": [ + { + "kind": "chatInput", + "id": "copilot:/test-session/chat-1#req-1", + "chat": "copilot:/test-session/chat-1", + "request": { "id": "req-1", "message": "Which environment?" } + } + ] + } +} diff --git a/types/test-cases/reducers/228-session-inputneededremoved-no-op-absent.json b/types/test-cases/reducers/228-session-inputneededremoved-no-op-absent.json new file mode 100644 index 00000000..c3a07439 --- /dev/null +++ b/types/test-cases/reducers/228-session-inputneededremoved-no-op-absent.json @@ -0,0 +1,36 @@ +{ + "description": "session/inputNeededRemoved is a no-op when inputNeeded is absent", + "reducer": "session", + "initial": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/inputNeededRemoved", + "id": "copilot:/test-session/chat-1#req-1" + } + ], + "expected": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "activeClients": [], + "chats": [] + } +} diff --git a/types/version/registry.ts b/types/version/registry.ts index b6ebd689..0d0039df 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -91,6 +91,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.SessionServerToolsChanged]: '0.1.0', [ActionType.SessionActiveClientSet]: '0.5.0', [ActionType.SessionActiveClientRemoved]: '0.5.0', + [ActionType.SessionInputNeededSet]: '0.5.0', + [ActionType.SessionInputNeededRemoved]: '0.5.0', [ActionType.SessionCustomizationsChanged]: '0.1.0', [ActionType.SessionCustomizationToggled]: '0.1.0', [ActionType.SessionCustomizationUpdated]: '0.1.0', From 7900eedf4bc80acd2505863ffe922223386a32ae Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 25 Jun 2026 12:59:00 -0700 Subject: [PATCH 2/2] sessions: run cargo fmt on rust reducer imports Reflow the `use crate::state::{...}` block in reducers.rs to satisfy `cargo fmt --all -- --check`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/rust/crates/ahp/src/reducers.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 721926e4..8adb645b 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -60,12 +60,12 @@ use ahp_types::state::{ ActiveTurn, AnnotationsState, ChangesetOperationStatus, ChangesetState, ChangesetStatus, ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, ErrorInfo, PendingMessage, PendingMessageKind, ResourceWatchState, ResponsePart, RootState, - SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, TerminalContentPart, - SessionInputRequest, - TerminalState, TerminalUnclassifiedPart, ToolCallCancellationReason, ToolCallCancelledState, - ToolCallCompletedState, ToolCallConfirmationReason, ToolCallContributor, - ToolCallPendingConfirmationState, ToolCallPendingResultConfirmationState, ToolCallResponsePart, - ToolCallRunningState, ToolCallState, ToolCallStreamingState, Turn, TurnState, + SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, + TerminalContentPart, TerminalState, TerminalUnclassifiedPart, ToolCallCancellationReason, + ToolCallCancelledState, ToolCallCompletedState, ToolCallConfirmationReason, + ToolCallContributor, ToolCallPendingConfirmationState, ToolCallPendingResultConfirmationState, + ToolCallResponsePart, ToolCallRunningState, ToolCallState, ToolCallStreamingState, Turn, + TurnState, }; /// What happened when an action was applied.